Merge "suppress unguarded-availability warning for only AServiceManager_waitForService" into main
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index dd60be7..414e50a 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -81,7 +81,10 @@
"framework-tethering.impl",
],
manifest: "AndroidManifestBase.xml",
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
}
// build tethering static library, used to compile both variants of the tethering.
@@ -215,7 +218,10 @@
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,
+ error_checks: ["NewApi"],
+ },
}
sdk {
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index ee44f3c..de9017a 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -80,6 +80,7 @@
first: {
jni_libs: [
"libservice-connectivity",
+ "libservice-thread-jni",
"libandroid_net_connectivity_com_android_net_module_util_jni",
],
native_shared_libs: [
diff --git a/Tethering/common/TetheringLib/api/lint-baseline.txt b/Tethering/common/TetheringLib/api/lint-baseline.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Tethering/common/TetheringLib/api/lint-baseline.txt
diff --git a/Tethering/common/TetheringLib/api/module-lib-lint-baseline.txt b/Tethering/common/TetheringLib/api/module-lib-lint-baseline.txt
new file mode 100644
index 0000000..1d09598
--- /dev/null
+++ b/Tethering/common/TetheringLib/api/module-lib-lint-baseline.txt
@@ -0,0 +1,23 @@
+// Baseline format: 1.0
+BroadcastBehavior: android.net.TetheringManager#ACTION_TETHER_STATE_CHANGED:
+ Field 'ACTION_TETHER_STATE_CHANGED' is missing @BroadcastBehavior
+
+
+RequiresPermission: android.net.TetheringManager#requestLatestTetheringEntitlementResult(int, boolean, java.util.concurrent.Executor, android.net.TetheringManager.OnTetheringEntitlementResultListener):
+ Method 'requestLatestTetheringEntitlementResult' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#startTethering(android.net.TetheringManager.TetheringRequest, java.util.concurrent.Executor, android.net.TetheringManager.StartTetheringCallback):
+ Method 'startTethering' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#startTethering(int, java.util.concurrent.Executor, android.net.TetheringManager.StartTetheringCallback):
+ Method 'startTethering' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#stopAllTethering():
+ Method 'stopAllTethering' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#stopTethering(int):
+ Method 'stopTethering' documentation mentions permissions already declared by @RequiresPermission
+
+
+SdkConstant: android.net.TetheringManager#ACTION_TETHER_STATE_CHANGED:
+ Field 'ACTION_TETHER_STATE_CHANGED' is missing @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+
+
+Todo: android.net.TetheringConstants:
+ Documentation mentions 'TODO'
diff --git a/Tethering/common/TetheringLib/api/system-lint-baseline.txt b/Tethering/common/TetheringLib/api/system-lint-baseline.txt
new file mode 100644
index 0000000..e678ce1
--- /dev/null
+++ b/Tethering/common/TetheringLib/api/system-lint-baseline.txt
@@ -0,0 +1,17 @@
+// Baseline format: 1.0
+BroadcastBehavior: android.net.TetheringManager#ACTION_TETHER_STATE_CHANGED:
+ Field 'ACTION_TETHER_STATE_CHANGED' is missing @BroadcastBehavior
+
+
+RequiresPermission: android.net.TetheringManager#requestLatestTetheringEntitlementResult(int, boolean, java.util.concurrent.Executor, android.net.TetheringManager.OnTetheringEntitlementResultListener):
+ Method 'requestLatestTetheringEntitlementResult' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#startTethering(android.net.TetheringManager.TetheringRequest, java.util.concurrent.Executor, android.net.TetheringManager.StartTetheringCallback):
+ Method 'startTethering' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#stopAllTethering():
+ Method 'stopAllTethering' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#stopTethering(int):
+ Method 'stopTethering' documentation mentions permissions already declared by @RequiresPermission
+
+
+SdkConstant: android.net.TetheringManager#ACTION_TETHER_STATE_CHANGED:
+ Field 'ACTION_TETHER_STATE_CHANGED' is missing @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 79d9a23..c065cd6 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -838,7 +838,7 @@
private void addInterfaceToNetwork(final int netId, @NonNull final String ifaceName) {
try {
- if (null != mRoutingCoordinator.value) {
+ if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
// TODO : remove this call in favor of using the LocalNetworkConfiguration
// correctly, which will let ConnectivityService do it automatically.
mRoutingCoordinator.value.addInterfaceToNetwork(netId, ifaceName);
@@ -852,7 +852,7 @@
private void addInterfaceForward(@NonNull final String fromIface,
@NonNull final String toIface) throws ServiceSpecificException, RemoteException {
- if (null != mRoutingCoordinator.value) {
+ if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
mRoutingCoordinator.value.addInterfaceForward(fromIface, toIface);
} else {
mNetd.tetherAddForward(fromIface, toIface);
@@ -862,7 +862,7 @@
private void removeInterfaceForward(@NonNull final String fromIface,
@NonNull final String toIface) {
- if (null != mRoutingCoordinator.value) {
+ if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
try {
mRoutingCoordinator.value.removeInterfaceForward(fromIface, toIface);
} catch (ServiceSpecificException e) {
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 2b14a42..4bd7e6a 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -1597,10 +1597,8 @@
@Override
public int hashCode() {
- // TODO: if this is ever used in production code, don't pass ifindices
- // to Objects.hash() to avoid autoboxing overhead.
- return Objects.hash(upstreamIfindex, downstreamIfindex, sourcePrefix, inDstMac,
- outSrcMac, outDstMac);
+ return 13 * upstreamIfindex + 41 * downstreamIfindex
+ + Objects.hash(sourcePrefix, inDstMac, outSrcMac, outDstMac);
}
@Override
@@ -1725,9 +1723,8 @@
@Override
public int hashCode() {
- // TODO: if this is ever used in production code, don't pass ifindices
- // to Objects.hash() to avoid autoboxing overhead.
- return Objects.hash(upstreamIfindex, downstreamIfindex, address, srcMac, dstMac);
+ return 13 * upstreamIfindex + 41 * downstreamIfindex
+ + Objects.hash(address, srcMac, dstMac);
}
@Override
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java b/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java
index e7dc757..9ef0f45 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java
@@ -19,18 +19,21 @@
import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_AIDL;
import android.annotation.NonNull;
+import android.annotation.TargetApi;
import android.hardware.tetheroffload.ForwardedStats;
import android.hardware.tetheroffload.IOffload;
import android.hardware.tetheroffload.ITetheringOffloadCallback;
import android.hardware.tetheroffload.NatTimeoutUpdate;
import android.hardware.tetheroffload.NetworkProtocol;
import android.hardware.tetheroffload.OffloadCallbackEvent;
+import android.os.Build;
import android.os.Handler;
import android.os.NativeHandle;
import android.os.ParcelFileDescriptor;
import android.os.ServiceManager;
import android.system.OsConstants;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.tethering.OffloadHardwareInterface.OffloadHalCallback;
@@ -40,6 +43,7 @@
/**
* The implementation of IOffloadHal which based on Stable AIDL interface
*/
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public class OffloadHalAidlImpl implements IOffloadHal {
private static final String TAG = OffloadHalAidlImpl.class.getSimpleName();
private static final String HAL_INSTANCE_NAME = IOffload.DESCRIPTOR + "/default";
@@ -52,6 +56,7 @@
private TetheringOffloadCallback mTetheringOffloadCallback;
+ @VisibleForTesting
public OffloadHalAidlImpl(int version, @NonNull IOffload offload, @NonNull Handler handler,
@NonNull SharedLog log) {
mOffloadVersion = version;
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java b/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
index e0a9878..71922f9 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
@@ -74,10 +74,7 @@
*/
public boolean initOffload(@NonNull NativeHandle handle1, @NonNull NativeHandle handle2,
@NonNull OffloadHalCallback callback) {
- final String logmsg = String.format("initOffload(%d, %d, %s)",
- handle1.getFileDescriptor().getInt$(), handle2.getFileDescriptor().getInt$(),
- (callback == null) ? "null"
- : "0x" + Integer.toHexString(System.identityHashCode(callback)));
+ final String logmsg = "initOffload()";
mOffloadHalCallback = callback;
mTetheringOffloadCallback = new TetheringOffloadCallback(
diff --git a/framework-t/api/OWNERS b/framework-t/api/OWNERS
index 607f85a..8ef735c 100644
--- a/framework-t/api/OWNERS
+++ b/framework-t/api/OWNERS
@@ -1,2 +1,3 @@
file:platform/packages/modules/Connectivity:main:/nearby/OWNERS
file:platform/packages/modules/Connectivity:main:/remoteauth/OWNERS
+file:platform/packages/modules/Connectivity:main:/thread/OWNERS
diff --git a/framework-t/api/module-lib-lint-baseline.txt b/framework-t/api/module-lib-lint-baseline.txt
index 3158bd4..6f954df 100644
--- a/framework-t/api/module-lib-lint-baseline.txt
+++ b/framework-t/api/module-lib-lint-baseline.txt
@@ -5,3 +5,17 @@
Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
BannedThrow: android.app.usage.NetworkStatsManager#queryTaggedSummary(android.net.NetworkTemplate, long, long):
Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
+
+
+MissingPermission: android.net.IpSecManager#startTunnelModeTransformMigration(android.net.IpSecTransform, java.net.InetAddress, java.net.InetAddress):
+ Feature field FEATURE_IPSEC_TUNNEL_MIGRATION required by method android.net.IpSecManager.startTunnelModeTransformMigration(android.net.IpSecTransform, java.net.InetAddress, java.net.InetAddress) is hidden or removed
+
+
+RequiresPermission: android.app.usage.NetworkStatsManager#registerUsageCallback(android.net.NetworkTemplate, long, java.util.concurrent.Executor, android.app.usage.NetworkStatsManager.UsageCallback):
+ Method 'registerUsageCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.EthernetManager#disableInterface(String, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+ Method 'disableInterface' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.EthernetManager#enableInterface(String, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+ Method 'enableInterface' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.EthernetManager#updateConfiguration(String, android.net.EthernetNetworkUpdateRequest, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+ Method 'updateConfiguration' documentation mentions permissions already declared by @RequiresPermission
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 23510e1..05cf9e8 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -418,7 +418,6 @@
package android.net.thread {
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ActiveOperationalDataset implements android.os.Parcelable {
- method @NonNull public static android.net.thread.ActiveOperationalDataset createRandomDataset();
method public int describeContents();
method @NonNull public static android.net.thread.ActiveOperationalDataset fromThreadTlvs(@NonNull byte[]);
method @NonNull public android.net.thread.OperationalDatasetTimestamp getActiveTimestamp();
@@ -493,10 +492,49 @@
}
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkController {
+ method public void createRandomizedDataset(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.thread.ActiveOperationalDataset,android.net.thread.ThreadNetworkException>);
method public int getThreadVersion();
+ method public static boolean isAttached(int);
+ method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void join(@NonNull android.net.thread.ActiveOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+ method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void leave(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+ method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void registerOperationalDatasetCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
+ method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.StateCallback);
+ method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void scheduleMigration(@NonNull android.net.thread.PendingOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+ method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
+ method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void unregisterStateCallback(@NonNull android.net.thread.ThreadNetworkController.StateCallback);
+ field public static final int DEVICE_ROLE_CHILD = 2; // 0x2
+ field public static final int DEVICE_ROLE_DETACHED = 1; // 0x1
+ field public static final int DEVICE_ROLE_LEADER = 4; // 0x4
+ field public static final int DEVICE_ROLE_ROUTER = 3; // 0x3
+ field public static final int DEVICE_ROLE_STOPPED = 0; // 0x0
field public static final int THREAD_VERSION_1_3 = 4; // 0x4
}
+ public static interface ThreadNetworkController.OperationalDatasetCallback {
+ method public void onActiveOperationalDatasetChanged(@Nullable android.net.thread.ActiveOperationalDataset);
+ method public default void onPendingOperationalDatasetChanged(@Nullable android.net.thread.PendingOperationalDataset);
+ }
+
+ public static interface ThreadNetworkController.StateCallback {
+ method public void onDeviceRoleChanged(int);
+ method public default void onPartitionIdChanged(long);
+ }
+
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public class ThreadNetworkException extends java.lang.Exception {
+ ctor public ThreadNetworkException(int, @NonNull String);
+ method public int getErrorCode();
+ field public static final int ERROR_ABORTED = 2; // 0x2
+ field public static final int ERROR_BUSY = 5; // 0x5
+ field public static final int ERROR_FAILED_PRECONDITION = 6; // 0x6
+ field public static final int ERROR_INTERNAL_ERROR = 1; // 0x1
+ field public static final int ERROR_REJECTED_BY_PEER = 8; // 0x8
+ field public static final int ERROR_RESOURCE_EXHAUSTED = 10; // 0xa
+ field public static final int ERROR_RESPONSE_BAD_FORMAT = 9; // 0x9
+ field public static final int ERROR_TIMEOUT = 3; // 0x3
+ field public static final int ERROR_UNAVAILABLE = 4; // 0x4
+ field public static final int ERROR_UNSUPPORTED_CHANNEL = 7; // 0x7
+ }
+
@FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkManager {
method @NonNull public java.util.List<android.net.thread.ThreadNetworkController> getAllThreadNetworkControllers();
}
diff --git a/framework-t/api/system-lint-baseline.txt b/framework-t/api/system-lint-baseline.txt
index c6055f6..4f7af87 100644
--- a/framework-t/api/system-lint-baseline.txt
+++ b/framework-t/api/system-lint-baseline.txt
@@ -7,6 +7,18 @@
Methods must not throw generic exceptions (`java.lang.Throwable`)
+MissingPermission: android.net.IpSecManager#startTunnelModeTransformMigration(android.net.IpSecTransform, java.net.InetAddress, java.net.InetAddress):
+ Feature field FEATURE_IPSEC_TUNNEL_MIGRATION required by method android.net.IpSecManager.startTunnelModeTransformMigration(android.net.IpSecTransform, java.net.InetAddress, java.net.InetAddress) is hidden or removed
+
+
+RequiresPermission: android.net.EthernetManager#disableInterface(String, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+ Method 'disableInterface' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.EthernetManager#enableInterface(String, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+ Method 'enableInterface' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.EthernetManager#updateConfiguration(String, android.net.EthernetNetworkUpdateRequest, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+ Method 'updateConfiguration' documentation mentions permissions already declared by @RequiresPermission
+
+
UnflaggedApi: android.nearby.CredentialElement#equals(Object):
New API must be flagged with @FlaggedApi: method android.nearby.CredentialElement.equals(Object)
UnflaggedApi: android.nearby.CredentialElement#hashCode():
diff --git a/framework/api/lint-baseline.txt b/framework/api/lint-baseline.txt
index 2f4004a..4465bcb 100644
--- a/framework/api/lint-baseline.txt
+++ b/framework/api/lint-baseline.txt
@@ -1,4 +1,19 @@
// Baseline format: 1.0
+BroadcastBehavior: android.net.ConnectivityManager#ACTION_BACKGROUND_DATA_SETTING_CHANGED:
+ Field 'ACTION_BACKGROUND_DATA_SETTING_CHANGED' is missing @BroadcastBehavior
+
+
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.app.PendingIntent):
+ Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.net.ConnectivityManager.NetworkCallback):
+ Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities#getOwnerUid():
+ Method 'getOwnerUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.http.BidirectionalStream.Builder#setTrafficStatsUid(int):
+ Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.http.UrlRequest.Builder#setTrafficStatsUid(int):
+ Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
+
+
VisiblySynchronized: android.net.NetworkInfo#toString():
Internal locks must not be exposed (synchronizing on this or class is still
- externally observable): method android.net.NetworkInfo.toString()
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index bfb4981..026d8a9 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -235,6 +235,7 @@
public final class VpnTransportInfo implements android.os.Parcelable android.net.TransportInfo {
ctor @Deprecated public VpnTransportInfo(int, @Nullable String);
+ method public long getApplicableRedactions();
method @Nullable public String getSessionId();
method @NonNull public android.net.VpnTransportInfo makeCopy(long);
}
diff --git a/framework/api/module-lib-lint-baseline.txt b/framework/api/module-lib-lint-baseline.txt
new file mode 100644
index 0000000..53a8c5e
--- /dev/null
+++ b/framework/api/module-lib-lint-baseline.txt
@@ -0,0 +1,33 @@
+// Baseline format: 1.0
+BroadcastBehavior: android.net.ConnectivityManager#ACTION_BACKGROUND_DATA_SETTING_CHANGED:
+ Field 'ACTION_BACKGROUND_DATA_SETTING_CHANGED' is missing @BroadcastBehavior
+
+
+RequiresPermission: android.net.ConnectivityManager#isTetheringSupported():
+ Method 'isTetheringSupported' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.app.PendingIntent):
+ Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.net.ConnectivityManager.NetworkCallback):
+ Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestRouteToHostAddress(int, java.net.InetAddress):
+ Method 'requestRouteToHostAddress' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.LinkProperties#getCaptivePortalApiUrl():
+ Method 'getCaptivePortalApiUrl' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.LinkProperties#getCaptivePortalData():
+ Method 'getCaptivePortalData' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities#getOwnerUid():
+ Method 'getOwnerUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities#getUnderlyingNetworks():
+ Method 'getUnderlyingNetworks' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities.Builder#setAllowedUids(java.util.Set<java.lang.Integer>):
+ Method 'setAllowedUids' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities.Builder#setSignalStrength(int):
+ Method 'setSignalStrength' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities.Builder#setUnderlyingNetworks(java.util.List<android.net.Network>):
+ Method 'setUnderlyingNetworks' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.NetworkRequest.Builder#setSignalStrength(int):
+ Method 'setSignalStrength' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.http.BidirectionalStream.Builder#setTrafficStatsUid(int):
+ Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.http.UrlRequest.Builder#setTrafficStatsUid(int):
+ Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
diff --git a/framework/api/system-lint-baseline.txt b/framework/api/system-lint-baseline.txt
index 9a97707..3ac97c0 100644
--- a/framework/api/system-lint-baseline.txt
+++ b/framework/api/system-lint-baseline.txt
@@ -1 +1,29 @@
// Baseline format: 1.0
+BroadcastBehavior: android.net.ConnectivityManager#ACTION_BACKGROUND_DATA_SETTING_CHANGED:
+ Field 'ACTION_BACKGROUND_DATA_SETTING_CHANGED' is missing @BroadcastBehavior
+
+
+RequiresPermission: android.net.ConnectivityManager#isTetheringSupported():
+ Method 'isTetheringSupported' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.app.PendingIntent):
+ Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.net.ConnectivityManager.NetworkCallback):
+ Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.LinkProperties#getCaptivePortalApiUrl():
+ Method 'getCaptivePortalApiUrl' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.LinkProperties#getCaptivePortalData():
+ Method 'getCaptivePortalData' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities#getOwnerUid():
+ Method 'getOwnerUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities#getUnderlyingNetworks():
+ Method 'getUnderlyingNetworks' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities.Builder#setSignalStrength(int):
+ Method 'setSignalStrength' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities.Builder#setUnderlyingNetworks(java.util.List<android.net.Network>):
+ Method 'setUnderlyingNetworks' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.NetworkRequest.Builder#setSignalStrength(int):
+ Method 'setSignalStrength' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.http.BidirectionalStream.Builder#setTrafficStatsUid(int):
+ Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.http.UrlRequest.Builder#setTrafficStatsUid(int):
+ Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
diff --git a/framework/src/android/net/BpfNetMapsReader.java b/framework/src/android/net/BpfNetMapsReader.java
index 37c58f0..4ab6d3e 100644
--- a/framework/src/android/net/BpfNetMapsReader.java
+++ b/framework/src/android/net/BpfNetMapsReader.java
@@ -17,6 +17,9 @@
package android.net;
import static android.net.BpfNetMapsConstants.CONFIGURATION_MAP_PATH;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
import static android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH;
import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
@@ -33,14 +36,15 @@
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;
import com.android.net.module.util.BpfMap;
import com.android.net.module.util.IBpfMap;
-import com.android.net.module.util.Struct;
import com.android.net.module.util.Struct.S32;
import com.android.net.module.util.Struct.U32;
+import com.android.net.module.util.Struct.U8;
/**
* A helper class to *read* java BpfMaps.
@@ -48,6 +52,8 @@
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU) // BPF maps were only mainlined in T
public class BpfNetMapsReader {
+ private static final String TAG = BpfNetMapsReader.class.getSimpleName();
+
// Locally store the handle of bpf maps. The FileDescriptors are statically cached inside the
// BpfMap implementation.
@@ -57,6 +63,7 @@
// Bpf map to store per uid traffic control configurations.
// See {@link UidOwnerValue} for more detail.
private final IBpfMap<S32, UidOwnerValue> mUidOwnerMap;
+ private final IBpfMap<S32, U8> mDataSaverEnabledMap;
private final Dependencies mDeps;
// Bitmaps for calculating whether a given uid is blocked by firewall chains.
@@ -104,6 +111,7 @@
mDeps = deps;
mConfigurationMap = mDeps.getConfigurationMap();
mUidOwnerMap = mDeps.getUidOwnerMap();
+ mDataSaverEnabledMap = mDeps.getDataSaverEnabledMap();
}
/**
@@ -130,6 +138,16 @@
throw new IllegalStateException("Cannot open uid owner map", e);
}
}
+
+ /** Get the data saver enabled map. */
+ public IBpfMap<S32, U8> getDataSaverEnabledMap() {
+ try {
+ return new BpfMap<>(DATA_SAVER_ENABLED_MAP_PATH, BpfMap.BPF_F_RDONLY, S32.class,
+ U8.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open data saver enabled map", e);
+ }
+ }
}
/**
@@ -171,12 +189,12 @@
* cause of the failure.
*/
public static boolean isChainEnabled(
- final IBpfMap<Struct.S32, Struct.U32> configurationMap, final int chain) {
+ final IBpfMap<S32, U32> configurationMap, final int chain) {
throwIfPreT("isChainEnabled is not available on pre-T devices");
final long match = getMatchByFirewallChain(chain);
try {
- final Struct.U32 config = configurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
+ final U32 config = configurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
return (config.val & match) != 0;
} catch (ErrnoException e) {
throw new ServiceSpecificException(e.errno,
@@ -195,14 +213,14 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
- public static int getUidRule(final IBpfMap<Struct.S32, UidOwnerValue> uidOwnerMap,
+ public static int getUidRule(final IBpfMap<S32, UidOwnerValue> uidOwnerMap,
final int chain, final int uid) {
throwIfPreT("getUidRule is not available on pre-T devices");
final long match = getMatchByFirewallChain(chain);
final boolean isAllowList = isFirewallAllowList(chain);
try {
- final UidOwnerValue uidMatch = uidOwnerMap.getValue(new Struct.S32(uid));
+ final UidOwnerValue uidMatch = uidOwnerMap.getValue(new S32(uid));
final boolean isMatchEnabled = uidMatch != null && (uidMatch.rule & match) != 0;
return isMatchEnabled == isAllowList ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
} catch (ErrnoException e) {
@@ -249,4 +267,29 @@
if ((uidMatch & HAPPY_BOX_MATCH) != 0) return false;
return isDataSaverEnabled;
}
+
+ /**
+ * Get Data Saver enabled or disabled
+ *
+ * @return whether Data Saver is enabled or disabled.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ 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) {
+ throw new ServiceSpecificException(e.errno, "Unable to get data saver: "
+ + Os.strerror(e.errno));
+ }
+ }
}
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 57ecf49..a934ddb 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -83,6 +83,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
import libcore.net.event.NetworkEventDispatcher;
@@ -6371,10 +6372,13 @@
final BpfNetMapsReader reader = BpfNetMapsReader.getInstance();
final boolean isDataSaverEnabled;
- // TODO: For U-QPR3+ devices, get data saver status from bpf configuration map directly.
- final DataSaverStatusTracker dataSaverStatusTracker =
- DataSaverStatusTracker.getInstance(mContext);
- isDataSaverEnabled = dataSaverStatusTracker.getDataSaverEnabled();
+ if (SdkLevel.isAtLeastV()) {
+ isDataSaverEnabled = reader.getDataSaverEnabled();
+ } else {
+ final DataSaverStatusTracker dataSaverStatusTracker =
+ DataSaverStatusTracker.getInstance(mContext);
+ isDataSaverEnabled = dataSaverStatusTracker.getDataSaverEnabled();
+ }
return reader.isUidNetworkingBlocked(uid, isNetworkMetered, isDataSaverEnabled);
}
diff --git a/framework/src/android/net/LocalNetworkConfig.java b/framework/src/android/net/LocalNetworkConfig.java
index fca7fd1..17b1064 100644
--- a/framework/src/android/net/LocalNetworkConfig.java
+++ b/framework/src/android/net/LocalNetworkConfig.java
@@ -62,11 +62,19 @@
return mUpstreamSelector;
}
- public @NonNull MulticastRoutingConfig getUpstreamMulticastRoutingConfig() {
+ /**
+ * Get the upstream multicast routing config
+ */
+ @NonNull
+ public MulticastRoutingConfig getUpstreamMulticastRoutingConfig() {
return mUpstreamMulticastRoutingConfig;
}
- public @NonNull MulticastRoutingConfig getDownstreamMulticastRoutingConfig() {
+ /**
+ * Get the downstream multicast routing config
+ */
+ @NonNull
+ public MulticastRoutingConfig getDownstreamMulticastRoutingConfig() {
return mDownstreamMulticastRoutingConfig;
}
@@ -82,6 +90,15 @@
dest.writeParcelable(mDownstreamMulticastRoutingConfig, flags);
}
+ @Override
+ public String toString() {
+ return "LocalNetworkConfig{"
+ + "UpstreamSelector=" + mUpstreamSelector
+ + ", UpstreamMulticastConfig=" + mUpstreamMulticastRoutingConfig
+ + ", DownstreamMulticastConfig=" + mDownstreamMulticastRoutingConfig
+ + '}';
+ }
+
public static final @NonNull Creator<LocalNetworkConfig> CREATOR = new Creator<>() {
public LocalNetworkConfig createFromParcel(Parcel in) {
final NetworkRequest upstreamSelector = in.readParcelable(null);
@@ -100,13 +117,13 @@
public static final class Builder {
@Nullable
- NetworkRequest mUpstreamSelector;
+ private NetworkRequest mUpstreamSelector;
@Nullable
- MulticastRoutingConfig mUpstreamMulticastRoutingConfig;
+ private MulticastRoutingConfig mUpstreamMulticastRoutingConfig;
@Nullable
- MulticastRoutingConfig mDownstreamMulticastRoutingConfig;
+ private MulticastRoutingConfig mDownstreamMulticastRoutingConfig;
/**
* Create a Builder
diff --git a/framework/src/android/net/MulticastRoutingConfig.java b/framework/src/android/net/MulticastRoutingConfig.java
index ebd9fc5..4a3e1be 100644
--- a/framework/src/android/net/MulticastRoutingConfig.java
+++ b/framework/src/android/net/MulticastRoutingConfig.java
@@ -21,6 +21,7 @@
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
+import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
@@ -30,7 +31,9 @@
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collections;
+import java.util.Objects;
import java.util.Set;
+import java.util.StringJoiner;
/**
* A class representing a configuration for multicast routing.
@@ -38,8 +41,8 @@
* Internal usage to Connectivity
* @hide
*/
-// TODO : @SystemApi
-public class MulticastRoutingConfig implements Parcelable {
+// @SystemApi(client = MODULE_LIBRARIES)
+public final class MulticastRoutingConfig implements Parcelable {
private static final String TAG = MulticastRoutingConfig.class.getSimpleName();
/** Do not forward any multicast packets. */
@@ -55,6 +58,7 @@
*/
public static final int FORWARD_WITH_MIN_SCOPE = 2;
+ /** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = { "FORWARD_" }, value = {
FORWARD_NONE,
@@ -68,6 +72,7 @@
*/
public static final int MULTICAST_SCOPE_NONE = -1;
+ /** @hide */
public static final MulticastRoutingConfig CONFIG_FORWARD_NONE =
new MulticastRoutingConfig(FORWARD_NONE, MULTICAST_SCOPE_NONE, null);
@@ -102,7 +107,7 @@
* Returns the minimal group address scope that is allowed for forwarding.
* If the forwarding mode is not FORWARD_WITH_MIN_SCOPE, will be MULTICAST_SCOPE_NONE.
*/
- public int getMinScope() {
+ public int getMinimumScope() {
return mMinScope;
}
@@ -111,7 +116,7 @@
* The list will be empty if the forwarding mode is not FORWARD_SELECTED.
*/
@NonNull
- public Set<Inet6Address> getMulticastListeningAddresses() {
+ public Set<Inet6Address> getListeningAddresses() {
return mListeningAddresses;
}
@@ -133,7 +138,7 @@
}
@Override
- public void writeToParcel(Parcel dest, int flags) {
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(mForwardingMode);
dest.writeInt(mMinScope);
dest.writeInt(mListeningAddresses.size());
@@ -147,9 +152,10 @@
return 0;
}
+ @NonNull
public static final Creator<MulticastRoutingConfig> CREATOR = new Creator<>() {
@Override
- public MulticastRoutingConfig createFromParcel(Parcel in) {
+ public MulticastRoutingConfig createFromParcel(@NonNull Parcel in) {
return new MulticastRoutingConfig(in);
}
@@ -159,43 +165,67 @@
}
};
- public static class Builder {
+ private static String forwardingModeToString(final int forwardingMode) {
+ switch (forwardingMode) {
+ case FORWARD_NONE: return "NONE";
+ case FORWARD_SELECTED: return "SELECTED";
+ case FORWARD_WITH_MIN_SCOPE: return "WITH_MIN_SCOPE";
+ default: return "UNKNOWN";
+ }
+ }
+
+ public static final class Builder {
@MulticastForwardingMode
private final int mForwardingMode;
private int mMinScope;
private final ArraySet<Inet6Address> mListeningAddresses;
- private Builder(@MulticastForwardingMode final int mode, int scope) {
+ // The two constructors with runtime checks for the mode and scope are arguably
+ // less convenient than three static factory methods, but API guidelines mandates
+ // that Builders are built with a constructor and not factory methods.
+ /**
+ * Create a new builder for forwarding mode FORWARD_NONE or FORWARD_SELECTED.
+ *
+ * <p>On a Builder for FORWARD_NONE, no properties can be set.
+ * <p>On a Builder for FORWARD_SELECTED, listening addresses can be added and removed
+ * but the minimum scope can't be set.
+ *
+ * @param mode {@link #FORWARD_NONE} or {@link #FORWARD_SELECTED}. Any other
+ * value will result in IllegalArgumentException.
+ * @see #Builder(int, int)
+ */
+ public Builder(@MulticastForwardingMode final int mode) {
+ if (FORWARD_NONE != mode && FORWARD_SELECTED != mode) {
+ if (FORWARD_WITH_MIN_SCOPE == mode) {
+ throw new IllegalArgumentException("FORWARD_WITH_MIN_SCOPE requires "
+ + "passing the scope as a second argument");
+ } else {
+ throw new IllegalArgumentException("Unknown forwarding mode : " + mode);
+ }
+ }
mForwardingMode = mode;
- mMinScope = scope;
+ mMinScope = MULTICAST_SCOPE_NONE;
mListeningAddresses = new ArraySet<>();
}
/**
- * Create a builder that forwards nothing.
- * No properties can be set on such a builder.
- */
- public static Builder newBuilderForwardingNone() {
- return new Builder(FORWARD_NONE, MULTICAST_SCOPE_NONE);
- }
-
- /**
- * Create a builder that forwards packets above a certain scope
+ * Create a new builder for forwarding mode FORWARD_WITH_MIN_SCOPE.
*
- * The scope can be changed on this builder, but not the listening addresses.
- * @param scope the initial scope
- */
- public static Builder newBuilderWithMinScope(final int scope) {
- return new Builder(FORWARD_WITH_MIN_SCOPE, scope);
- }
-
- /**
- * Create a builder that forwards a specified list of listening addresses.
+ * <p>On this Builder the scope can be set with {@link #setMinimumScope}, but
+ * listening addresses can't be added or removed.
*
- * Addresses can be added and removed from this builder, but the scope can't be set.
+ * @param mode Must be {@link #FORWARD_WITH_MIN_SCOPE}.
+ * @param scope the minimum scope for this multicast routing config.
+ * @see Builder#Builder(int)
*/
- public static Builder newBuilderWithListeningAddresses() {
- return new Builder(FORWARD_SELECTED, MULTICAST_SCOPE_NONE);
+ public Builder(@MulticastForwardingMode final int mode, int scope) {
+ if (FORWARD_WITH_MIN_SCOPE != mode) {
+ throw new IllegalArgumentException("Forwarding with a min scope must "
+ + "use forward mode FORWARD_WITH_MIN_SCOPE");
+ }
+ mForwardingMode = mode;
+ mMinScope = scope;
+ mListeningAddresses = new ArraySet<>();
}
/**
@@ -203,6 +233,7 @@
* This is only meaningful (indeed, allowed) for configs in FORWARD_WITH_MIN_SCOPE mode.
* @return this builder
*/
+ @NonNull
public Builder setMinimumScope(final int scope) {
if (FORWARD_WITH_MIN_SCOPE != mForwardingMode) {
throw new IllegalArgumentException("Can't set the scope on a builder in mode "
@@ -219,6 +250,7 @@
* If this address was already added, this is a no-op.
* @return this builder
*/
+ @NonNull
public Builder addListeningAddress(@NonNull final Inet6Address address) {
if (FORWARD_SELECTED != mForwardingMode) {
throw new IllegalArgumentException("Can't add an address on a builder in mode "
@@ -236,7 +268,8 @@
* If this address was not added, or was already removed, this is a no-op.
* @return this builder
*/
- public Builder removeListeningAddress(@NonNull final Inet6Address address) {
+ @NonNull
+ public Builder clearListeningAddress(@NonNull final Inet6Address address) {
if (FORWARD_SELECTED != mForwardingMode) {
throw new IllegalArgumentException("Can't remove an address on a builder in mode "
+ modeToString(mForwardingMode));
@@ -248,6 +281,7 @@
/**
* Build the config.
*/
+ @NonNull
public MulticastRoutingConfig build() {
return new MulticastRoutingConfig(mForwardingMode, mMinScope, mListeningAddresses);
}
@@ -261,4 +295,41 @@
default: return "unknown multicast routing mode " + mode;
}
}
+
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ } else if (!(other instanceof MulticastRoutingConfig)) {
+ return false;
+ } else {
+ final MulticastRoutingConfig otherConfig = (MulticastRoutingConfig) other;
+ return mForwardingMode == otherConfig.mForwardingMode
+ && mMinScope == otherConfig.mMinScope
+ && mListeningAddresses.equals(otherConfig.mListeningAddresses);
+ }
+ }
+
+ public int hashCode() {
+ return Objects.hash(mForwardingMode, mMinScope, mListeningAddresses);
+ }
+
+ public String toString() {
+ final StringJoiner resultJoiner = new StringJoiner(" ", "{", "}");
+
+ resultJoiner.add("ForwardingMode:");
+ resultJoiner.add(modeToString(mForwardingMode));
+
+ if (mForwardingMode == FORWARD_WITH_MIN_SCOPE) {
+ resultJoiner.add("MinScope:");
+ resultJoiner.add(Integer.toString(mMinScope));
+ }
+
+ if (mForwardingMode == FORWARD_SELECTED && !mListeningAddresses.isEmpty()) {
+ resultJoiner.add("ListeningAddresses: [");
+ resultJoiner.add(TextUtils.join(",", mListeningAddresses));
+ resultJoiner.add("]");
+ }
+
+ return resultJoiner.toString();
+ }
}
diff --git a/framework/src/android/net/QosSession.java b/framework/src/android/net/QosSession.java
index 25f3965..d1edae9 100644
--- a/framework/src/android/net/QosSession.java
+++ b/framework/src/android/net/QosSession.java
@@ -22,6 +22,9 @@
import android.os.Parcel;
import android.os.Parcelable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Provides identifying information of a QoS session. Sent to an application through
* {@link QosCallback}.
@@ -107,6 +110,7 @@
TYPE_EPS_BEARER,
TYPE_NR_BEARER,
})
+ @Retention(RetentionPolicy.SOURCE)
@interface QosSessionType {}
private QosSession(final Parcel in) {
diff --git a/nearby/framework/java/android/nearby/BroadcastRequest.java b/nearby/framework/java/android/nearby/BroadcastRequest.java
index 90f4d0f..6d6357d 100644
--- a/nearby/framework/java/android/nearby/BroadcastRequest.java
+++ b/nearby/framework/java/android/nearby/BroadcastRequest.java
@@ -88,6 +88,7 @@
* @hide
*/
@IntDef({MEDIUM_BLE})
+ @Retention(RetentionPolicy.SOURCE)
public @interface Medium {}
/**
diff --git a/nearby/framework/java/android/nearby/NearbyDevice.java b/nearby/framework/java/android/nearby/NearbyDevice.java
index e8fcc28..e7db0c5 100644
--- a/nearby/framework/java/android/nearby/NearbyDevice.java
+++ b/nearby/framework/java/android/nearby/NearbyDevice.java
@@ -25,6 +25,8 @@
import com.android.internal.util.Preconditions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -149,6 +151,7 @@
* @hide
*/
@IntDef({Medium.BLE, Medium.BLUETOOTH})
+ @Retention(RetentionPolicy.SOURCE)
public @interface Medium {
int BLE = 1;
int BLUETOOTH = 2;
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index a70b303..070a2b6 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -34,6 +34,8 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.WeakHashMap;
@@ -63,6 +65,7 @@
ScanStatus.SUCCESS,
ScanStatus.ERROR,
})
+ @Retention(RetentionPolicy.SOURCE)
public @interface ScanStatus {
// The undetermined status, some modules may be initializing. Retry is suggested.
int UNKNOWN = 0;
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index c74f229..ee5f25b 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -1645,12 +1645,14 @@
mContext, MdnsFeatureFlags.NSD_FORCE_DISABLE_MDNS_OFFLOAD))
.setIncludeInetAddressRecordsInProbing(mDeps.isFeatureEnabled(
mContext, MdnsFeatureFlags.INCLUDE_INET_ADDRESS_RECORDS_IN_PROBING))
- .setIsExpiredServicesRemovalEnabled(mDeps.isTrunkStableFeatureEnabled(
- MdnsFeatureFlags.NSD_EXPIRED_SERVICES_REMOVAL))
+ .setIsExpiredServicesRemovalEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_EXPIRED_SERVICES_REMOVAL))
+ .setIsLabelCountLimitEnabled(mDeps.isTetheringFeatureNotChickenedOut(
+ mContext, MdnsFeatureFlags.NSD_LIMIT_LABEL_COUNT))
.build();
mMdnsSocketClient =
new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider,
- LOGGER.forSubComponent("MdnsMultinetworkSocketClient"));
+ LOGGER.forSubComponent("MdnsMultinetworkSocketClient"), flags);
mMdnsDiscoveryManager = deps.makeMdnsDiscoveryManager(new ExecutorProvider(),
mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"), flags);
handler.post(() -> mMdnsSocketClient.setCallback(mMdnsDiscoveryManager));
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 6f7645e..0a6d8c1 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -36,6 +36,11 @@
public static final String NSD_EXPIRED_SERVICES_REMOVAL =
"nsd_expired_services_removal";
+ /**
+ * A feature flag to control whether the label count limit should be enabled.
+ */
+ public static final String NSD_LIMIT_LABEL_COUNT = "nsd_limit_label_count";
+
// Flag for offload feature
public final boolean mIsMdnsOffloadFeatureEnabled;
@@ -45,14 +50,20 @@
// Flag for expired services removal
public final boolean mIsExpiredServicesRemovalEnabled;
+ // Flag for label count limit
+ public final boolean mIsLabelCountLimitEnabled;
+
/**
* The constructor for {@link MdnsFeatureFlags}.
*/
public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
- boolean includeInetAddressRecordsInProbing, boolean isExpiredServicesRemovalEnabled) {
+ boolean includeInetAddressRecordsInProbing,
+ boolean isExpiredServicesRemovalEnabled,
+ boolean isLabelCountLimitEnabled) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
mIsExpiredServicesRemovalEnabled = isExpiredServicesRemovalEnabled;
+ mIsLabelCountLimitEnabled = isLabelCountLimitEnabled;
}
@@ -67,6 +78,7 @@
private boolean mIsMdnsOffloadFeatureEnabled;
private boolean mIncludeInetAddressRecordsInProbing;
private boolean mIsExpiredServicesRemovalEnabled;
+ private boolean mIsLabelCountLimitEnabled;
/**
* The constructor for {@link Builder}.
@@ -74,7 +86,8 @@
public Builder() {
mIsMdnsOffloadFeatureEnabled = false;
mIncludeInetAddressRecordsInProbing = false;
- mIsExpiredServicesRemovalEnabled = true; // Default enabled.
+ mIsExpiredServicesRemovalEnabled = false;
+ mIsLabelCountLimitEnabled = true; // Default enabled.
}
/**
@@ -109,11 +122,23 @@
}
/**
+ * Set whether the label count limit is enabled.
+ *
+ * @see #NSD_LIMIT_LABEL_COUNT
+ */
+ public Builder setIsLabelCountLimitEnabled(boolean isLabelCountLimitEnabled) {
+ mIsLabelCountLimitEnabled = isLabelCountLimitEnabled;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
*/
public MdnsFeatureFlags build() {
return new MdnsFeatureFlags(mIsMdnsOffloadFeatureEnabled,
- mIncludeInetAddressRecordsInProbing, mIsExpiredServicesRemovalEnabled);
+ mIncludeInetAddressRecordsInProbing,
+ mIsExpiredServicesRemovalEnabled,
+ mIsLabelCountLimitEnabled);
}
}
}
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 42a6b0d..62c37ad 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -65,11 +65,12 @@
private final MdnsProber mProber;
@NonNull
private final MdnsReplySender mReplySender;
-
@NonNull
private final SharedLog mSharedLog;
@NonNull
private final byte[] mPacketCreationBuffer;
+ @NonNull
+ private final MdnsFeatureFlags mMdnsFeatureFlags;
/**
* Callbacks called by {@link MdnsInterfaceAdvertiser} to report status updates.
@@ -213,6 +214,7 @@
mProber = deps.makeMdnsProber(sharedLog.getTag(), looper, mReplySender, mProbingCallback,
sharedLog);
mSharedLog = sharedLog;
+ mMdnsFeatureFlags = mdnsFeatureFlags;
}
/**
@@ -351,7 +353,7 @@
public void handlePacket(byte[] recvbuf, int length, InetSocketAddress src) {
final MdnsPacket packet;
try {
- packet = MdnsPacket.parse(new MdnsPacketReader(recvbuf, length));
+ packet = MdnsPacket.parse(new MdnsPacketReader(recvbuf, length, mMdnsFeatureFlags));
} catch (MdnsPacket.ParseException e) {
mSharedLog.e("Error parsing mDNS packet", e);
if (DBG) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
index 4ba6912..e7b0eaa 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -50,6 +50,7 @@
@NonNull private final Handler mHandler;
@NonNull private final MdnsSocketProvider mSocketProvider;
@NonNull private final SharedLog mSharedLog;
+ @NonNull private final MdnsFeatureFlags mMdnsFeatureFlags;
private final ArrayMap<MdnsServiceBrowserListener, InterfaceSocketCallback> mSocketRequests =
new ArrayMap<>();
@@ -58,11 +59,12 @@
private int mReceivedPacketNumber = 0;
public MdnsMultinetworkSocketClient(@NonNull Looper looper,
- @NonNull MdnsSocketProvider provider,
- @NonNull SharedLog sharedLog) {
+ @NonNull MdnsSocketProvider provider, @NonNull SharedLog sharedLog,
+ @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
mHandler = new Handler(looper);
mSocketProvider = provider;
mSharedLog = sharedLog;
+ mMdnsFeatureFlags = mdnsFeatureFlags;
}
private class InterfaceSocketCallback implements MdnsSocketProvider.SocketCallback {
@@ -239,7 +241,7 @@
final MdnsPacket response;
try {
- response = MdnsResponseDecoder.parseResponse(recvbuf, length);
+ response = MdnsResponseDecoder.parseResponse(recvbuf, length, mMdnsFeatureFlags);
} catch (MdnsPacket.ParseException e) {
if (e.code != MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE) {
mSharedLog.e(e.getMessage(), e);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketReader.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketReader.java
index aa38844..4917188 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketReader.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketReader.java
@@ -16,6 +16,7 @@
package com.android.server.connectivity.mdns;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.SparseArray;
@@ -33,21 +34,23 @@
private final byte[] buf;
private final int count;
private final SparseArray<LabelEntry> labelDictionary;
+ private final MdnsFeatureFlags mMdnsFeatureFlags;
private int pos;
private int limit;
/** Constructs a reader for the given packet. */
public MdnsPacketReader(DatagramPacket packet) {
- this(packet.getData(), packet.getLength());
+ this(packet.getData(), packet.getLength(), MdnsFeatureFlags.newBuilder().build());
}
/** Constructs a reader for the given packet. */
- public MdnsPacketReader(byte[] buffer, int length) {
+ public MdnsPacketReader(byte[] buffer, int length, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
buf = buffer;
count = length;
pos = 0;
limit = -1;
labelDictionary = new SparseArray<>(16);
+ mMdnsFeatureFlags = mdnsFeatureFlags;
}
/**
@@ -269,4 +272,4 @@
this.label = label;
}
}
-}
\ No newline at end of file
+}
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 73c1758..e34778f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -133,11 +133,6 @@
public final boolean isSharedName;
/**
- * Whether probing is still in progress for the record.
- */
- public boolean isProbing;
-
- /**
* Last time (as per SystemClock.elapsedRealtime) when advertised via multicast, 0 if never
*/
public long lastAdvertisedTimeMs;
@@ -148,12 +143,10 @@
*/
public long lastSentTimeMs;
- RecordInfo(NsdServiceInfo serviceInfo, T record, boolean sharedName,
- boolean probing) {
+ RecordInfo(NsdServiceInfo serviceInfo, T record, boolean sharedName) {
this.serviceInfo = serviceInfo;
this.record = record;
this.isSharedName = sharedName;
- this.isProbing = probing;
}
}
@@ -187,6 +180,11 @@
public int sentPacketCount = NO_PACKET;
/**
+ * Whether probing is still in progress.
+ */
+ private boolean isProbing;
+
+ /**
* Create a ServiceRegistration for dns-sd service registration (RFC6763).
*
* @param deviceHostname Hostname of the device (for the interface used)
@@ -209,7 +207,7 @@
false /* cacheFlush */,
NON_NAME_RECORDS_TTL_MILLIS,
serviceName),
- true /* sharedName */, true /* probing */);
+ true /* sharedName */);
if (subtype == null) {
this.ptrRecords = Collections.singletonList(ptrRecord);
@@ -226,7 +224,7 @@
false /* cacheFlush */,
NON_NAME_RECORDS_TTL_MILLIS,
serviceName),
- true /* sharedName */, true /* probing */);
+ true /* sharedName */);
this.ptrRecords = List.of(ptrRecord, subtypeRecord);
}
@@ -239,7 +237,7 @@
NAME_RECORDS_TTL_MILLIS, 0 /* servicePriority */, 0 /* serviceWeight */,
serviceInfo.getPort(),
deviceHostname),
- false /* sharedName */, true /* probing */);
+ false /* sharedName */);
txtRecord = new RecordInfo<>(
serviceInfo,
@@ -248,7 +246,7 @@
true /* cacheFlush */, // Service name is verified unique after probing
NON_NAME_RECORDS_TTL_MILLIS,
attrsToTextEntries(serviceInfo.getAttributes())),
- false /* sharedName */, true /* probing */);
+ false /* sharedName */);
final ArrayList<RecordInfo<?>> allRecords = new ArrayList<>(5);
allRecords.addAll(ptrRecords);
@@ -263,18 +261,18 @@
false /* cacheFlush */,
NON_NAME_RECORDS_TTL_MILLIS,
serviceType),
- true /* sharedName */, true /* probing */));
+ true /* sharedName */));
this.allRecords = Collections.unmodifiableList(allRecords);
this.repliedServiceCount = repliedServiceCount;
this.sentPacketCount = sentPacketCount;
+ this.isProbing = true;
}
void setProbing(boolean probing) {
- for (RecordInfo<?> info : allRecords) {
- info.isProbing = probing;
- }
+ this.isProbing = probing;
}
+
}
/**
@@ -292,7 +290,7 @@
true /* cacheFlush */,
NAME_RECORDS_TTL_MILLIS,
mDeviceHostname),
- false /* sharedName */, false /* probing */));
+ false /* sharedName */));
mGeneralRecords.add(new RecordInfo<>(
null /* serviceInfo */,
@@ -302,7 +300,7 @@
true /* cacheFlush */,
NAME_RECORDS_TTL_MILLIS,
addr.getAddress()),
- false /* sharedName */, false /* probing */));
+ false /* sharedName */));
}
}
@@ -485,7 +483,7 @@
// Add answers from each service
for (int i = 0; i < mServices.size(); i++) {
final ServiceRegistration registration = mServices.valueAt(i);
- if (registration.exiting) continue;
+ if (registration.exiting || registration.isProbing) continue;
if (addReplyFromService(question, registration.allRecords, registration.ptrRecords,
registration.srvRecord, registration.txtRecord, replyUnicast, now,
answerInfo, additionalAnswerRecords)) {
@@ -558,7 +556,6 @@
final int answersStartIndex = answerInfo.size();
for (RecordInfo<?> info : serviceRecords) {
- if (info.isProbing) continue;
/* RFC6762 6.: the record name must match the question name, the record rrtype
must match the question qtype unless the qtype is "ANY" (255) or the rrtype is
@@ -870,7 +867,7 @@
final ServiceRegistration registration = mServices.get(serviceId);
if (registration == null) return false;
- return registration.srvRecord.isProbing;
+ return registration.isProbing;
}
/**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
index e2288c1..05ad1be 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -33,6 +33,7 @@
/** An mDNS response. */
public class MdnsResponse {
+ public static final long EXPIRATION_NEVER = Long.MAX_VALUE;
private final List<MdnsRecord> records;
private final List<MdnsPointerRecord> pointerRecords;
private MdnsServiceRecord serviceRecord;
@@ -349,6 +350,21 @@
return serviceName;
}
+ /** Get the min remaining ttl time from received records */
+ public long getMinRemainingTtl(long now) {
+ long minRemainingTtl = EXPIRATION_NEVER;
+ // TODO: Check other records(A, AAAA, TXT) ttl time.
+ if (!hasServiceRecord()) {
+ return EXPIRATION_NEVER;
+ }
+ // Check ttl time.
+ long remainingTtl = serviceRecord.getRemainingTTL(now);
+ if (remainingTtl < minRemainingTtl) {
+ minRemainingTtl = remainingTtl;
+ }
+ return minRemainingTtl;
+ }
+
/**
* Tests if this response is a goodbye message. This will be true if a service record is present
* and any of the records have a TTL of 0.
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
index 050913f..b812bb4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
@@ -84,9 +84,9 @@
* @throws MdnsPacket.ParseException if a response packet could not be parsed.
*/
@NonNull
- public static MdnsPacket parseResponse(@NonNull byte[] recvbuf, int length)
- throws MdnsPacket.ParseException {
- MdnsPacketReader reader = new MdnsPacketReader(recvbuf, length);
+ public static MdnsPacket parseResponse(@NonNull byte[] recvbuf, int length,
+ @NonNull MdnsFeatureFlags mdnsFeatureFlags) throws MdnsPacket.ParseException {
+ final MdnsPacketReader reader = new MdnsPacketReader(recvbuf, length, mdnsFeatureFlags);
final MdnsPacket mdnsPacket;
try {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
index d3493c7..e9a41d1 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
@@ -16,16 +16,22 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsResponse.EXPIRATION_NEVER;
import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
import static com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase;
import static com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLowerCase;
+import static java.lang.Math.min;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Handler;
import android.os.Looper;
import android.util.ArrayMap;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@@ -67,8 +73,11 @@
}
}
/**
- * A map of cached services. Key is composed of service name, type and socket. Value is the
- * service which use the service type to discover from each socket.
+ * A map of cached services. Key is composed of service type and socket. Value is the list of
+ * services which are discovered from the given CacheKey.
+ * When the MdnsFeatureFlags#NSD_EXPIRED_SERVICES_REMOVAL flag is enabled, the lists are sorted
+ * by expiration time, with the earliest entries appearing first. This sorting allows the
+ * removal process to progress through the expiration check efficiently.
*/
@NonNull
private final ArrayMap<CacheKey, List<MdnsResponse>> mCachedServices = new ArrayMap<>();
@@ -82,10 +91,20 @@
private final Handler mHandler;
@NonNull
private final MdnsFeatureFlags mMdnsFeatureFlags;
+ @NonNull
+ private final MdnsUtils.Clock mClock;
+ private long mNextExpirationTime = EXPIRATION_NEVER;
public MdnsServiceCache(@NonNull Looper looper, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+ this(looper, mdnsFeatureFlags, new MdnsUtils.Clock());
+ }
+
+ @VisibleForTesting
+ MdnsServiceCache(@NonNull Looper looper, @NonNull MdnsFeatureFlags mdnsFeatureFlags,
+ @NonNull MdnsUtils.Clock clock) {
mHandler = new Handler(looper);
mMdnsFeatureFlags = mdnsFeatureFlags;
+ mClock = clock;
}
/**
@@ -97,6 +116,9 @@
@NonNull
public List<MdnsResponse> getCachedServices(@NonNull CacheKey cacheKey) {
ensureRunningOnHandlerThread(mHandler);
+ if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+ maybeRemoveExpiredServices(cacheKey, mClock.elapsedRealtime());
+ }
return mCachedServices.containsKey(cacheKey)
? Collections.unmodifiableList(new ArrayList<>(mCachedServices.get(cacheKey)))
: Collections.emptyList();
@@ -129,6 +151,9 @@
@Nullable
public MdnsResponse getCachedService(@NonNull String serviceName, @NonNull CacheKey cacheKey) {
ensureRunningOnHandlerThread(mHandler);
+ if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+ maybeRemoveExpiredServices(cacheKey, mClock.elapsedRealtime());
+ }
final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
if (responses == null) {
return null;
@@ -137,6 +162,16 @@
return response != null ? new MdnsResponse(response) : null;
}
+ static void insertResponseAndSortList(
+ List<MdnsResponse> responses, MdnsResponse response, long now) {
+ // binarySearch returns "the index of the search key, if it is contained in the list;
+ // otherwise, (-(insertion point) - 1)"
+ final int searchRes = Collections.binarySearch(responses, response,
+ // Sort the list by ttl.
+ (o1, o2) -> Long.compare(o1.getMinRemainingTtl(now), o2.getMinRemainingTtl(now)));
+ responses.add(searchRes >= 0 ? searchRes : (-searchRes - 1), response);
+ }
+
/**
* Add or update a service.
*
@@ -151,7 +186,15 @@
final MdnsResponse existing =
findMatchedResponse(responses, response.getServiceInstanceName());
responses.remove(existing);
- responses.add(response);
+ if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+ final long now = mClock.elapsedRealtime();
+ // Insert and sort service
+ insertResponseAndSortList(responses, response, now);
+ // Update the next expiration check time when a new service is added.
+ mNextExpirationTime = getNextExpirationTime(now);
+ } else {
+ responses.add(response);
+ }
}
/**
@@ -168,14 +211,25 @@
return null;
}
final Iterator<MdnsResponse> iterator = responses.iterator();
+ MdnsResponse removedResponse = null;
while (iterator.hasNext()) {
final MdnsResponse response = iterator.next();
if (equalsIgnoreDnsCase(serviceName, response.getServiceInstanceName())) {
iterator.remove();
- return response;
+ removedResponse = response;
+ break;
}
}
- return null;
+
+ if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+ // Remove the serviceType if no response.
+ if (responses.isEmpty()) {
+ mCachedServices.remove(cacheKey);
+ }
+ // Update the next expiration check time when a service is removed.
+ mNextExpirationTime = getNextExpirationTime(mClock.elapsedRealtime());
+ }
+ return removedResponse;
}
/**
@@ -203,6 +257,87 @@
mCallbacks.remove(cacheKey);
}
+ private void notifyServiceExpired(@NonNull CacheKey cacheKey,
+ @NonNull MdnsResponse previousResponse, @Nullable MdnsResponse newResponse) {
+ final ServiceExpiredCallback callback = mCallbacks.get(cacheKey);
+ if (callback == null) {
+ // The cached service is no listener.
+ return;
+ }
+ mHandler.post(()-> callback.onServiceRecordExpired(previousResponse, newResponse));
+ }
+
+ static List<MdnsResponse> removeExpiredServices(@NonNull List<MdnsResponse> responses,
+ long now) {
+ final List<MdnsResponse> removedResponses = new ArrayList<>();
+ final Iterator<MdnsResponse> iterator = responses.iterator();
+ while (iterator.hasNext()) {
+ final MdnsResponse response = iterator.next();
+ // TODO: Check other records (A, AAAA, TXT) ttl time and remove the record if it's
+ // expired. Then send service update notification.
+ if (!response.hasServiceRecord() || response.getMinRemainingTtl(now) > 0) {
+ // The responses are sorted by the service record ttl time. Break out of loop
+ // early if service is not expired or no service record.
+ break;
+ }
+ // Remove the ttl expired service.
+ iterator.remove();
+ removedResponses.add(response);
+ }
+ return removedResponses;
+ }
+
+ private long getNextExpirationTime(long now) {
+ if (mCachedServices.isEmpty()) {
+ return EXPIRATION_NEVER;
+ }
+
+ long minRemainingTtl = EXPIRATION_NEVER;
+ for (int i = 0; i < mCachedServices.size(); i++) {
+ minRemainingTtl = min(minRemainingTtl,
+ // The empty lists are not kept in the map, so there's always at least one
+ // element in the list. Therefore, it's fine to get the first element without a
+ // null check.
+ mCachedServices.valueAt(i).get(0).getMinRemainingTtl(now));
+ }
+ return minRemainingTtl == EXPIRATION_NEVER ? EXPIRATION_NEVER : now + minRemainingTtl;
+ }
+
+ /**
+ * Check whether the ttl time is expired on each service and notify to the listeners
+ */
+ private void maybeRemoveExpiredServices(CacheKey cacheKey, long now) {
+ ensureRunningOnHandlerThread(mHandler);
+ if (now < mNextExpirationTime) {
+ // Skip the check if ttl time is not expired.
+ return;
+ }
+
+ final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
+ if (responses == null) {
+ // No such services.
+ return;
+ }
+
+ final List<MdnsResponse> removedResponses = removeExpiredServices(responses, now);
+ if (removedResponses.isEmpty()) {
+ // No expired services.
+ return;
+ }
+
+ for (MdnsResponse previousResponse : removedResponses) {
+ notifyServiceExpired(cacheKey, previousResponse, null /* newResponse */);
+ }
+
+ // Remove the serviceType if no response.
+ if (responses.isEmpty()) {
+ mCachedServices.remove(cacheKey);
+ }
+
+ // Update next expiration time.
+ mNextExpirationTime = getNextExpirationTime(now);
+ }
+
/*** Callbacks for listening service expiration */
public interface ServiceExpiredCallback {
/*** Notify the service is expired */
@@ -210,5 +345,5 @@
@Nullable MdnsResponse newResponse);
}
- // TODO: check ttl expiration for each service and notify to the clients.
+ // TODO: Schedule a job to check ttl expiration for all services and notify to the clients.
}
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 0a03186..32f604e 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -312,8 +312,7 @@
this.searchOptions = searchOptions;
boolean hadReply = false;
if (listeners.put(listener, searchOptions) == null) {
- for (MdnsResponse existingResponse :
- serviceCache.getCachedServices(cacheKey)) {
+ for (MdnsResponse existingResponse : serviceCache.getCachedServices(cacheKey)) {
if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
final MdnsServiceInfo info =
buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
index d18a19b..82c8c5b 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
@@ -105,9 +105,10 @@
private AtomicInteger packetsCount;
@Nullable private Timer checkMulticastResponseTimer;
private final SharedLog sharedLog;
+ @NonNull private final MdnsFeatureFlags mdnsFeatureFlags;
public MdnsSocketClient(@NonNull Context context, @NonNull MulticastLock multicastLock,
- SharedLog sharedLog) {
+ SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
this.sharedLog = sharedLog;
this.context = context;
this.multicastLock = multicastLock;
@@ -116,6 +117,7 @@
} else {
unicastReceiverBuffer = null;
}
+ this.mdnsFeatureFlags = mdnsFeatureFlags;
}
@Override
@@ -454,7 +456,8 @@
final MdnsPacket response;
try {
- response = MdnsResponseDecoder.parseResponse(packet.getData(), packet.getLength());
+ response = MdnsResponseDecoder.parseResponse(
+ packet.getData(), packet.getLength(), mdnsFeatureFlags);
} catch (MdnsPacket.ParseException e) {
sharedLog.w(String.format("Error while decoding %s packet (%d): %d",
responseType, packetNumber, e.code));
diff --git a/service/Android.bp b/service/Android.bp
index 7ddf068..76741bc 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -110,7 +110,6 @@
],
srcs: [
":services.connectivity-netstats-jni-sources",
- "jni/com_android_server_BpfNetMaps.cpp",
"jni/com_android_server_connectivity_ClatCoordinator.cpp",
"jni/com_android_server_ServiceManagerWrapper.cpp",
"jni/com_android_server_TestNetworkService.cpp",
diff --git a/service/jni/com_android_server_BpfNetMaps.cpp b/service/jni/com_android_server_BpfNetMaps.cpp
deleted file mode 100644
index 29f6a60..0000000
--- a/service/jni/com_android_server_BpfNetMaps.cpp
+++ /dev/null
@@ -1,44 +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.
- */
-
-#include "bpf/BpfUtils.h"
-
-#include <jni.h>
-#include <nativehelper/JNIHelp.h>
-
-namespace android {
-
-static jint native_synchronizeKernelRCU(JNIEnv* env, jobject self) {
- return -bpf::synchronizeKernelRCU();
-}
-
-/*
- * JNI registration.
- */
-// clang-format off
-static const JNINativeMethod gMethods[] = {
- /* name, signature, funcPtr */
- {"native_synchronizeKernelRCU", "()I",
- (void*)native_synchronizeKernelRCU},
-};
-// clang-format on
-
-int register_com_android_server_BpfNetMaps(JNIEnv* env) {
- return jniRegisterNativeMethods(env, "android/net/connectivity/com/android/server/BpfNetMaps",
- gMethods, NELEM(gMethods));
-}
-
-}; // namespace android
diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp
index 5e89ad0..bb70d4f 100644
--- a/service/jni/onload.cpp
+++ b/service/jni/onload.cpp
@@ -22,7 +22,6 @@
namespace android {
int register_com_android_server_TestNetworkService(JNIEnv* env);
-int register_com_android_server_BpfNetMaps(JNIEnv* env);
int register_com_android_server_connectivity_ClatCoordinator(JNIEnv* env);
int register_android_server_net_NetworkStatsFactory(JNIEnv* env);
int register_android_server_net_NetworkStatsService(JNIEnv* env);
@@ -44,10 +43,6 @@
}
if (android::modules::sdklevel::IsAtLeastT()) {
- if (register_com_android_server_BpfNetMaps(env) < 0) {
- return JNI_ERR;
- }
-
if (register_com_android_server_connectivity_ClatCoordinator(env) < 0) {
return JNI_ERR;
}
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index b7e928d..ad9cfbe 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -106,9 +106,6 @@
// Use legacy netd for releases before T.
private static boolean sInitialized = false;
- private static final String BPF_NET_MAPS_FORCE_DISABLE_JAVA_BPF_MAP =
- "bpf_net_maps_force_disable_java_bpf_map";
-
// Lock for sConfigurationMap entry for UID_RULES_CONFIGURATION_KEY.
// This entry is not accessed by others.
// BpfNetMaps acquires this lock while sequence of read, modify, and write.
@@ -324,10 +321,16 @@
}
/**
- * Call synchronize_rcu()
+ * Synchronously call in to kernel to synchronize_rcu()
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public int synchronizeKernelRCU() {
- return native_synchronizeKernelRCU();
+ try {
+ BpfMap.synchronizeKernelRCU();
+ } catch (ErrnoException e) {
+ return -e.errno;
+ }
+ return 0;
}
/**
@@ -1061,7 +1064,4 @@
pw.decreaseIndent();
}
}
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- private static native int native_synchronizeKernelRCU();
}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 47e0bd9..ea6d37e 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -969,6 +969,9 @@
// Flag to optimize closing frozen app sockets by waiting for the cellular modem to wake up.
private final boolean mDelayDestroyFrozenSockets;
+ // Flag to allow SysUI to receive connectivity reports for wifi picker UI.
+ private final boolean mAllowSysUiConnectivityReports;
+
// Uids that ConnectivityService is pending to close sockets of.
private final Set<Integer> mPendingFrozenUids = new ArraySet<>();
@@ -1469,6 +1472,13 @@
}
/**
+ * @see DeviceConfigUtils#isTetheringFeatureNotChickenedOut
+ */
+ public boolean isFeatureNotChickenedOut(Context context, String name) {
+ return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context, name);
+ }
+
+ /**
* Get the BpfNetMaps implementation to use in ConnectivityService.
* @param netd a netd binder
* @return BpfNetMaps implementation.
@@ -1760,7 +1770,12 @@
mUserAllContext.registerReceiver(mPackageIntentReceiver, packageIntentFilter,
null /* broadcastPermission */, mHandler);
- mNetworkActivityTracker = new LegacyNetworkActivityTracker(mContext, mNetd, 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.
+ mTrackMultiNetworkActivities = mDeps.isAtLeastV();
+ mNetworkActivityTracker = new LegacyNetworkActivityTracker(mContext, mNetd, mHandler,
+ mTrackMultiNetworkActivities);
final NetdCallback netdCallback = new NetdCallback();
try {
@@ -1835,6 +1850,8 @@
&& mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION);
mDelayDestroyFrozenSockets = mDeps.isAtLeastU()
&& mDeps.isFeatureEnabled(context, DELAY_DESTROY_FROZEN_SOCKETS_VERSION);
+ mAllowSysUiConnectivityReports = mDeps.isFeatureNotChickenedOut(
+ mContext, ALLOW_SYSUI_CONNECTIVITY_REPORTS);
if (mDestroyFrozenSockets) {
final UidFrozenStateChangedCallback frozenStateChangedCallback =
new UidFrozenStateChangedCallback() {
@@ -3234,9 +3251,20 @@
private void handleReportNetworkActivity(final NetworkActivityParams params) {
mNetworkActivityTracker.handleReportNetworkActivity(params);
+ final boolean isCellNetworkActivity;
+ if (mTrackMultiNetworkActivities) {
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(params.label);
+ // nai could be null if netd receives a netlink message and calls the network
+ // activity change callback after the network is unregistered from ConnectivityService.
+ isCellNetworkActivity = nai != null
+ && nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
+ } else {
+ isCellNetworkActivity = params.label == TRANSPORT_CELLULAR;
+ }
+
if (mDelayDestroyFrozenSockets
&& params.isActive
- && params.label == TRANSPORT_CELLULAR
+ && isCellNetworkActivity
&& !mPendingFrozenUids.isEmpty()) {
closePendingFrozenSockets();
}
@@ -3297,6 +3325,10 @@
static final String DELAY_DESTROY_FROZEN_SOCKETS_VERSION =
"delay_destroy_frozen_sockets_version";
+ @VisibleForTesting
+ public static final String ALLOW_SYSUI_CONNECTIVITY_REPORTS =
+ "allow_sysui_connectivity_reports";
+
private void enforceInternetPermission() {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.INTERNET,
@@ -3460,6 +3492,11 @@
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
}
+ private boolean checkSystemBarServicePermission(int pid, int uid) {
+ return checkAnyPermissionOf(mContext, pid, uid,
+ android.Manifest.permission.STATUS_BAR_SERVICE);
+ }
+
private boolean checkNetworkSignalStrengthWakeupPermission(int pid, int uid) {
return checkAnyPermissionOf(mContext, pid, uid,
android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP,
@@ -4944,6 +4981,11 @@
if (wasDefault) {
mDefaultInetConditionPublished = 0;
}
+ if (mTrackMultiNetworkActivities) {
+ // If trackMultiNetworkActivities is disabled, ActivityTracker removes idleTimer when
+ // the network becomes no longer the default network.
+ mNetworkActivityTracker.removeDataActivityTracking(nai);
+ }
notifyIfacesChangedForNetworkStats();
// If this was a local network forwarded to some upstream, or if some local network was
// forwarded to this nai, then disable forwarding rules now.
@@ -4997,12 +5039,7 @@
}
if (mDefaultRequest == nri) {
- // TODO : make battery stats aware that since 2013 multiple interfaces may be
- // active at the same time. For now keep calling this with the default
- // network, because while incorrect this is the closest to the old (also
- // incorrect) behavior.
- mNetworkActivityTracker.updateDataActivityTracking(
- null /* newNetwork */, nai);
+ mNetworkActivityTracker.updateDefaultNetwork(null /* newNetwork */, nai);
maybeClosePendingFrozenSockets(null /* newNetwork */, nai);
ensureNetworkTransitionWakelock(nai.toShortString());
}
@@ -6225,8 +6262,10 @@
if (!networkFound) return;
if (underpinnedNetworkFound) {
+ final NetworkCapabilities underpinnedNc =
+ getNetworkCapabilitiesInternal(underpinnedNetwork);
mKeepaliveTracker.handleMonitorAutomaticKeepalive(ki,
- underpinnedNetwork.netId);
+ underpinnedNetwork.netId, underpinnedNc.getUids());
} else {
// If no underpinned network, then make sure the keepalive is running.
mKeepaliveTracker.handleMaybeResumeKeepalive(ki);
@@ -9006,6 +9045,9 @@
return;
}
+ if (VDBG) {
+ Log.v(TAG, "Update local network config " + nai.network.netId + " : " + newConfig);
+ }
final LocalNetworkConfig.Builder configBuilder = new LocalNetworkConfig.Builder();
// TODO : apply the diff for multicast routing.
configBuilder.setUpstreamMulticastRoutingConfig(
@@ -9620,7 +9662,7 @@
if (oldDefaultNetwork != null) {
mLingerMonitor.noteLingerDefaultNetwork(oldDefaultNetwork, newDefaultNetwork);
}
- mNetworkActivityTracker.updateDataActivityTracking(newDefaultNetwork, oldDefaultNetwork);
+ mNetworkActivityTracker.updateDefaultNetwork(newDefaultNetwork, oldDefaultNetwork);
maybeClosePendingFrozenSockets(newDefaultNetwork, oldDefaultNetwork);
mProxyTracker.setDefaultProxy(null != newDefaultNetwork
? newDefaultNetwork.linkProperties.getHttpProxy() : null);
@@ -10520,6 +10562,15 @@
SystemClock.elapsedRealtime(), mNascentDelayMs);
networkAgent.setInactive();
+ if (mTrackMultiNetworkActivities) {
+ // Start tracking activity of this network.
+ // This must be called before rematchAllNetworksAndRequests since the network
+ // should be tracked when the network becomes the default network.
+ // This method does not trigger any callbacks or broadcasts. Callbacks or broadcasts
+ // can be triggered later if this network becomes the default network.
+ mNetworkActivityTracker.setupDataActivityTracking(networkAgent);
+ }
+
// Consider network even though it is not yet validated.
rematchAllNetworksAndRequests();
@@ -11578,6 +11629,10 @@
if (checkNetworkStackPermission(callbackPid, callbackUid)) {
return true;
}
+ if (mAllowSysUiConnectivityReports
+ && checkSystemBarServicePermission(callbackPid, callbackUid)) {
+ return true;
+ }
// Administrator UIDs also contains the Owner UID
final int[] administratorUids = nai.networkCapabilities.getAdministratorUids();
@@ -11707,8 +11762,8 @@
*/
private static final class NetworkActivityParams {
public final boolean isActive;
- // Label used for idle timer. Transport type is used as label.
- // label is int since NMS was using the identifier as int, and it has not been changed
+ // If TrackMultiNetworkActivities is enabled, idleTimer label is netid.
+ // If TrackMultiNetworkActivities is disabled, idleTimer label is transport type.
public final int label;
public final long timestampNs;
// Uid represents the uid that was responsible for waking the radio.
@@ -11750,13 +11805,15 @@
}
}
+ private final boolean mTrackMultiNetworkActivities;
private final LegacyNetworkActivityTracker mNetworkActivityTracker;
/**
* Class used for updating network activity tracking with netd and notify network activity
* changes.
*/
- private static final class LegacyNetworkActivityTracker {
+ @VisibleForTesting
+ public static final class LegacyNetworkActivityTracker {
private static final int NO_UID = -1;
private final Context mContext;
private final INetd mNetd;
@@ -11768,8 +11825,14 @@
// If there is no default network, default network is considered active to keep the existing
// behavior. Initial value is used until first connect to the default network.
private volatile boolean mIsDefaultNetworkActive = true;
+ private Network mDefaultNetwork;
// Key is netId. Value is configured idle timer information.
private final SparseArray<IdleTimerParams> mActiveIdleTimers = new SparseArray<>();
+ private final boolean mTrackMultiNetworkActivities;
+ // Store netIds of Wi-Fi networks whose idletimers report that they are active
+ private final Set<Integer> mActiveWifiNetworks = new ArraySet<>();
+ // Store netIds of cellular networks whose idletimers report that they are active
+ private final Set<Integer> mActiveCellularNetworks = new ArraySet<>();
private static class IdleTimerParams {
public final int timeout;
@@ -11782,10 +11845,11 @@
}
LegacyNetworkActivityTracker(@NonNull Context context, @NonNull INetd netd,
- @NonNull Handler handler) {
+ @NonNull Handler handler, boolean trackMultiNetworkActivities) {
mContext = context;
mNetd = netd;
mHandler = handler;
+ mTrackMultiNetworkActivities = trackMultiNetworkActivities;
}
private void ensureRunningOnConnectivityServiceThread() {
@@ -11795,19 +11859,97 @@
}
}
- public void handleReportNetworkActivity(NetworkActivityParams activityParams) {
- ensureRunningOnConnectivityServiceThread();
+ /**
+ * Update network activity and call BatteryStats to update radio power state if the
+ * mobile or Wi-Fi activity is changed.
+ * LegacyNetworkActivityTracker considers the mobile network is active if at least one
+ * mobile network is active since BatteryStatsService only maintains a single power state
+ * for the mobile network.
+ * The Wi-Fi network is also the same.
+ *
+ * {@link #setupDataActivityTracking} and {@link #removeDataActivityTracking} use
+ * TRANSPORT_CELLULAR as the transportType argument if the network has both cell and Wi-Fi
+ * transports.
+ */
+ private void maybeUpdateRadioPowerState(final int netId, final int transportType,
+ final boolean isActive, final int uid) {
+ if (transportType != TRANSPORT_WIFI && transportType != TRANSPORT_CELLULAR) {
+ Log.e(TAG, "Unexpected transportType in maybeUpdateRadioPowerState: "
+ + transportType);
+ return;
+ }
+ final Set<Integer> activeNetworks = transportType == TRANSPORT_WIFI
+ ? mActiveWifiNetworks : mActiveCellularNetworks;
+
+ final boolean wasEmpty = activeNetworks.isEmpty();
+ if (isActive) {
+ activeNetworks.add(netId);
+ } else {
+ activeNetworks.remove(netId);
+ }
+
+ if (wasEmpty != activeNetworks.isEmpty()) {
+ updateRadioPowerState(isActive, transportType, uid);
+ }
+ }
+
+ private void handleDefaultNetworkActivity(final int transportType,
+ final boolean isActive, final long timestampNs) {
+ mIsDefaultNetworkActive = isActive;
+ sendDataActivityBroadcast(transportTypeToLegacyType(transportType),
+ isActive, timestampNs);
+ if (isActive) {
+ reportNetworkActive();
+ }
+ }
+
+ private void handleReportNetworkActivityWithNetIdLabel(
+ NetworkActivityParams activityParams) {
+ final int netId = activityParams.label;
+ final IdleTimerParams idleTimerParams = mActiveIdleTimers.get(netId);
+ if (idleTimerParams == null) {
+ // This network activity change is not tracked anymore
+ // This can happen if netd callback post activity change event message but idle
+ // timer is removed before processing this message.
+ return;
+ }
+ // TODO: if a network changes transports, storing the transport type in the
+ // IdleTimerParams is not correct. Consider getting it from the network's
+ // NetworkCapabilities instead.
+ final int transportType = idleTimerParams.transportType;
+ maybeUpdateRadioPowerState(netId, transportType,
+ activityParams.isActive, activityParams.uid);
+
+ if (mDefaultNetwork == null || mDefaultNetwork.netId != netId) {
+ // This activity change is not for the default network.
+ return;
+ }
+
+ handleDefaultNetworkActivity(transportType, activityParams.isActive,
+ activityParams.timestampNs);
+ }
+
+ private void handleReportNetworkActivityWithTransportTypeLabel(
+ NetworkActivityParams activityParams) {
if (mActiveIdleTimers.size() == 0) {
// This activity change is not for the current default network.
// This can happen if netd callback post activity change event message but
// the default network is lost before processing this message.
return;
}
- sendDataActivityBroadcast(transportTypeToLegacyType(activityParams.label),
- activityParams.isActive, activityParams.timestampNs);
- mIsDefaultNetworkActive = activityParams.isActive;
- if (mIsDefaultNetworkActive) {
- reportNetworkActive();
+ handleDefaultNetworkActivity(activityParams.label, activityParams.isActive,
+ activityParams.timestampNs);
+ }
+
+ /**
+ * Handle network activity change
+ */
+ public void handleReportNetworkActivity(NetworkActivityParams activityParams) {
+ ensureRunningOnConnectivityServiceThread();
+ if (mTrackMultiNetworkActivities) {
+ handleReportNetworkActivityWithNetIdLabel(activityParams);
+ } else {
+ handleReportNetworkActivityWithTransportTypeLabel(activityParams);
}
}
@@ -11864,6 +12006,30 @@
}
/**
+ * Get idle timer label
+ */
+ @VisibleForTesting
+ public static int getIdleTimerLabel(final boolean trackMultiNetworkActivities,
+ final int netId, final int transportType) {
+ return trackMultiNetworkActivities ? netId : transportType;
+ }
+
+ private boolean maybeCreateIdleTimer(
+ String iface, int netId, int timeout, int transportType) {
+ if (timeout <= 0 || iface == null) return false;
+ try {
+ final String label = Integer.toString(getIdleTimerLabel(
+ mTrackMultiNetworkActivities, netId, transportType));
+ mNetd.idletimerAddInterface(iface, timeout, label);
+ mActiveIdleTimers.put(netId, new IdleTimerParams(timeout, transportType));
+ return true;
+ } catch (Exception e) {
+ loge("Exception in createIdleTimer", e);
+ return false;
+ }
+ }
+
+ /**
* Setup data activity tracking for the given network.
*
* Every {@code setupDataActivityTracking} should be paired with a
@@ -11872,13 +12038,17 @@
* @return true if the idleTimer is added to the network, false otherwise
*/
private boolean setupDataActivityTracking(NetworkAgentInfo networkAgent) {
+ ensureRunningOnConnectivityServiceThread();
final String iface = networkAgent.linkProperties.getInterfaceName();
final int netId = networkAgent.network().netId;
final int timeout;
final int type;
- if (networkAgent.networkCapabilities.hasTransport(
+ if (!networkAgent.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_VPN)) {
+ // Do not track VPN network.
+ return false;
+ } else if (networkAgent.networkCapabilities.hasTransport(
NetworkCapabilities.TRANSPORT_CELLULAR)) {
timeout = Settings.Global.getInt(mContext.getContentResolver(),
ConnectivitySettingsManager.DATA_ACTIVITY_TIMEOUT_MOBILE,
@@ -11894,25 +12064,21 @@
return false; // do not track any other networks
}
- updateRadioPowerState(true /* isActive */, type);
-
- if (timeout > 0 && iface != null) {
- try {
- mActiveIdleTimers.put(netId, new IdleTimerParams(timeout, type));
- mNetd.idletimerAddInterface(iface, timeout, Integer.toString(type));
- return true;
- } catch (Exception e) {
- // You shall not crash!
- loge("Exception in setupDataActivityTracking " + e);
- }
+ final boolean hasIdleTimer = maybeCreateIdleTimer(iface, netId, timeout, type);
+ if (hasIdleTimer || !mTrackMultiNetworkActivities) {
+ // If trackMultiNetwork is disabled, NetworkActivityTracker updates radio power
+ // state in all cases. If trackMultiNetwork is enabled, it updates radio power
+ // state only about a network that has an idletimer.
+ maybeUpdateRadioPowerState(netId, type, true /* isActive */, NO_UID);
}
- return false;
+ return hasIdleTimer;
}
/**
* Remove data activity tracking when network disconnects.
*/
- private void removeDataActivityTracking(NetworkAgentInfo networkAgent) {
+ public void removeDataActivityTracking(NetworkAgentInfo networkAgent) {
+ ensureRunningOnConnectivityServiceThread();
final String iface = networkAgent.linkProperties.getInterfaceName();
final int netId = networkAgent.network().netId;
final NetworkCapabilities caps = networkAgent.networkCapabilities;
@@ -11920,7 +12086,10 @@
if (iface == null) return;
final int type;
- if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+ if (!networkAgent.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_VPN)) {
+ // Do not track VPN network.
+ return;
+ } else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
type = NetworkCapabilities.TRANSPORT_CELLULAR;
} else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
type = NetworkCapabilities.TRANSPORT_WIFI;
@@ -11929,16 +12098,17 @@
}
try {
- updateRadioPowerState(false /* isActive */, type);
+ maybeUpdateRadioPowerState(netId, type, false /* isActive */, NO_UID);
final IdleTimerParams params = mActiveIdleTimers.get(netId);
if (params == null) {
// IdleTimer is not added if the configured timeout is 0 or negative value
return;
}
mActiveIdleTimers.remove(netId);
- // The call fails silently if no idle timer setup for this interface
- mNetd.idletimerRemoveInterface(iface, params.timeout,
- Integer.toString(params.transportType));
+ final String label = Integer.toString(getIdleTimerLabel(
+ mTrackMultiNetworkActivities, netId, params.transportType));
+ // The call fails silently if no idle timer setup for this interface
+ mNetd.idletimerRemoveInterface(iface, params.timeout, label);
} catch (Exception e) {
// You shall not crash!
loge("Exception in removeDataActivityTracking " + e);
@@ -11948,12 +12118,15 @@
private void updateDefaultNetworkActivity(NetworkAgentInfo defaultNetwork,
boolean hasIdleTimer) {
if (defaultNetwork != null) {
+ mDefaultNetwork = defaultNetwork.network();
mIsDefaultNetworkActive = true;
- // Callbacks are called only when the network has the idle timer.
- if (hasIdleTimer) {
+ // If only the default network is tracked, callbacks are called only when the
+ // network has the idle timer.
+ if (mTrackMultiNetworkActivities || hasIdleTimer) {
reportNetworkActive();
}
} else {
+ mDefaultNetwork = null;
// If there is no default network, default network is considered active to keep the
// existing behavior.
mIsDefaultNetworkActive = true;
@@ -11961,29 +12134,34 @@
}
/**
- * Update data activity tracking when network state is updated.
+ * Update the default network this class tracks the activity of.
*/
- public void updateDataActivityTracking(NetworkAgentInfo newNetwork,
+ public void updateDefaultNetwork(NetworkAgentInfo newNetwork,
NetworkAgentInfo oldNetwork) {
ensureRunningOnConnectivityServiceThread();
+ // If TrackMultiNetworkActivities is enabled, devices add idleTimer when the network is
+ // first connected and remove when the network is disconnected.
+ // If TrackMultiNetworkActivities is disabled, devices add idleTimer when the network
+ // becomes the default network and remove when the network becomes no longer the default
+ // network.
boolean hasIdleTimer = false;
- if (newNetwork != null) {
+ if (!mTrackMultiNetworkActivities && newNetwork != null) {
hasIdleTimer = setupDataActivityTracking(newNetwork);
}
updateDefaultNetworkActivity(newNetwork, hasIdleTimer);
- if (oldNetwork != null) {
+ if (!mTrackMultiNetworkActivities && oldNetwork != null) {
removeDataActivityTracking(oldNetwork);
}
}
- private void updateRadioPowerState(boolean isActive, int transportType) {
+ private void updateRadioPowerState(boolean isActive, int transportType, int uid) {
final BatteryStatsManager bs = mContext.getSystemService(BatteryStatsManager.class);
switch (transportType) {
case NetworkCapabilities.TRANSPORT_CELLULAR:
- bs.reportMobileRadioPowerState(isActive, NO_UID);
+ bs.reportMobileRadioPowerState(isActive, uid);
break;
case NetworkCapabilities.TRANSPORT_WIFI:
- bs.reportWifiRadioPowerState(isActive, NO_UID);
+ bs.reportWifiRadioPowerState(isActive, uid);
break;
default:
logw("Untracked transport type:" + transportType);
@@ -12003,7 +12181,9 @@
}
public void dump(IndentingPrintWriter pw) {
+ pw.print("mTrackMultiNetworkActivities="); pw.println(mTrackMultiNetworkActivities);
pw.print("mIsDefaultNetworkActive="); pw.println(mIsDefaultNetworkActive);
+ pw.print("mDefaultNetwork="); pw.println(mDefaultNetwork);
pw.println("Idle timers:");
try {
for (int i = 0; i < mActiveIdleTimers.size(); i++) {
@@ -12012,11 +12192,13 @@
pw.print(" timeout="); pw.print(params.timeout);
pw.print(" type="); pw.println(params.transportType);
}
+ pw.println("WiFi active networks: " + mActiveWifiNetworks);
+ pw.println("Cellular active networks: " + mActiveCellularNetworks);
} catch (Exception e) {
- // mActiveIdleTimers should only be accessed from handler thread, except dump().
- // As dump() is never called in normal usage, it would be needlessly expensive
- // to lock the collection only for its benefit.
- // Also, mActiveIdleTimers is not expected to be updated frequently.
+ // mActiveIdleTimers, mActiveWifiNetworks, and mActiveCellularNetworks should only
+ // be accessed from handler thread, except dump(). As dump() is never called in
+ // normal usage, it would be needlessly expensive to lock the collection only for
+ // its benefit. Also, they are not expected to be updated frequently.
// So catching the exception and logging.
pw.println("Failed to dump NetworkActivityTracker: " + e);
}
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index bba132f..8036ae9 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -53,6 +53,7 @@
import android.util.LocalLog;
import android.util.Log;
import android.util.Pair;
+import android.util.Range;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
@@ -77,6 +78,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
/**
* Manages automatic on/off socket keepalive requests.
@@ -373,26 +375,27 @@
* Determine if any state transition is needed for the specific automatic keepalive.
*/
public void handleMonitorAutomaticKeepalive(@NonNull final AutomaticOnOffKeepalive ki,
- final int vpnNetId) {
+ final int vpnNetId, @NonNull Set<Range<Integer>> vpnUidRanges) {
// Might happen if the automatic keepalive was removed by the app just as the alarm fires.
if (!mAutomaticOnOffKeepalives.contains(ki)) return;
if (STATE_ALWAYS_ON == ki.mAutomaticOnOffState) {
throw new IllegalStateException("Should not monitor non-auto keepalive");
}
- handleMonitorTcpConnections(ki, vpnNetId);
+ handleMonitorTcpConnections(ki, vpnNetId, vpnUidRanges);
}
/**
* Determine if disable or re-enable keepalive is needed or not based on TCP sockets status.
*/
- private void handleMonitorTcpConnections(@NonNull AutomaticOnOffKeepalive ki, int vpnNetId) {
+ private void handleMonitorTcpConnections(@NonNull AutomaticOnOffKeepalive ki, int vpnNetId,
+ @NonNull Set<Range<Integer>> vpnUidRanges) {
// Might happen if the automatic keepalive was removed by the app just as the alarm fires.
if (!mAutomaticOnOffKeepalives.contains(ki)) return;
if (STATE_ALWAYS_ON == ki.mAutomaticOnOffState) {
throw new IllegalStateException("Should not monitor non-auto keepalive");
}
- if (!isAnyTcpSocketConnected(vpnNetId)) {
+ if (!isAnyTcpSocketConnected(vpnNetId, vpnUidRanges)) {
// No TCP socket exists. Stop keepalive if ENABLED, and remain SUSPENDED if currently
// SUSPENDED.
if (ki.mAutomaticOnOffState == STATE_ENABLED) {
@@ -744,7 +747,7 @@
}
@VisibleForTesting
- boolean isAnyTcpSocketConnected(int netId) {
+ boolean isAnyTcpSocketConnected(int netId, @NonNull Set<Range<Integer>> vpnUidRanges) {
FileDescriptor fd = null;
try {
@@ -757,7 +760,8 @@
// Send request for each IP family
for (final int family : ADDRESS_FAMILIES) {
- if (isAnyTcpSocketConnectedForFamily(fd, family, networkMark, networkMask)) {
+ if (isAnyTcpSocketConnectedForFamily(
+ fd, family, networkMark, networkMask, vpnUidRanges)) {
return true;
}
}
@@ -771,7 +775,8 @@
}
private boolean isAnyTcpSocketConnectedForFamily(FileDescriptor fd, int family, int networkMark,
- int networkMask) throws ErrnoException, InterruptedIOException {
+ int networkMask, @NonNull Set<Range<Integer>> vpnUidRanges)
+ throws ErrnoException, InterruptedIOException {
ensureRunningOnHandlerThread();
// Build SocketDiag messages and cache it.
if (mSockDiagMsg.get(family) == null) {
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 0bcb757..6325b46 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -96,12 +96,17 @@
srcs: [
"framework/**/DnsPacket.java",
"framework/**/DnsPacketUtils.java",
+ "framework/**/DnsSvcbPacket.java",
+ "framework/**/DnsSvcbRecord.java",
+ "framework/**/HexDump.java",
+ "framework/**/NetworkStackConstants.java",
],
sdk_version: "module_current",
visibility: [
"//packages/services/Iwlan:__subpackages__",
],
libs: [
+ "androidx.annotation_annotation",
"framework-annotations-lib",
"framework-connectivity.stubs.module_lib",
],
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
index 595ac74..d622427 100644
--- a/staticlibs/device/com/android/net/module/util/BpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -239,6 +239,11 @@
return Struct.parse(mValueClass, buffer);
}
+ /** Synchronize Kernel RCU */
+ public static void synchronizeKernelRCU() throws ErrnoException {
+ nativeSynchronizeKernelRCU();
+ }
+
private static native int nativeBpfFdGet(String path, int mode, int keySize, int valueSize)
throws ErrnoException, NullPointerException;
@@ -260,4 +265,6 @@
private native boolean nativeFindMapEntry(int fd, byte[] key, byte[] value)
throws ErrnoException;
+
+ private static native void nativeSynchronizeKernelRCU() throws ErrnoException;
}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsPacket.java b/staticlibs/framework/com/android/net/module/util/DnsPacket.java
index 0dcdf1e..63106a1 100644
--- a/staticlibs/framework/com/android/net/module/util/DnsPacket.java
+++ b/staticlibs/framework/com/android/net/module/util/DnsPacket.java
@@ -56,6 +56,7 @@
*/
// TODO: Define the constant as a public constant in DnsResolver since it can never change.
private static final int TYPE_CNAME = 5;
+ public static final int TYPE_SVCB = 64;
/**
* Thrown when parsing packet failed.
@@ -282,7 +283,7 @@
* @param buf ByteBuffer input of record, must be in network byte order
* (which is the default).
*/
- private DnsRecord(@RecordType int rType, @NonNull ByteBuffer buf)
+ protected DnsRecord(@RecordType int rType, @NonNull ByteBuffer buf)
throws BufferUnderflowException, ParseException {
Objects.requireNonNull(buf);
this.rType = rType;
@@ -326,6 +327,8 @@
// Return a DnsRecord instance by default for backward compatibility, this is useful
// when a partner supports new type of DnsRecord but does not inherit DnsRecord.
switch (nsType) {
+ case TYPE_SVCB:
+ return new DnsSvcbRecord(rType, buf);
default:
return new DnsRecord(rType, buf);
}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java b/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java
new file mode 100644
index 0000000..d298599
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java
@@ -0,0 +1,170 @@
+/*
+ * 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;
+
+import static android.net.DnsResolver.TYPE_A;
+import static android.net.DnsResolver.TYPE_AAAA;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A class for a DNS SVCB response packet.
+ *
+ * @hide
+ */
+public class DnsSvcbPacket extends DnsPacket {
+ public static final int TYPE_SVCB = 64;
+
+ private static final String TAG = DnsSvcbPacket.class.getSimpleName();
+
+ /**
+ * Creates a DnsSvcbPacket object from the given wire-format DNS packet.
+ */
+ private DnsSvcbPacket(@NonNull byte[] data) throws DnsPacket.ParseException {
+ // If data is null, ParseException will be thrown.
+ super(data);
+
+ final int questions = mHeader.getRecordCount(QDSECTION);
+ if (questions != 1) {
+ throw new DnsPacket.ParseException("Unexpected question count " + questions);
+ }
+ final int nsType = mRecords[QDSECTION].get(0).nsType;
+ if (nsType != TYPE_SVCB) {
+ throw new DnsPacket.ParseException("Unexpected query type " + nsType);
+ }
+ }
+
+ /**
+ * Returns true if the DnsSvcbPacket is a DNS response.
+ */
+ public boolean isResponse() {
+ return mHeader.isResponse();
+ }
+
+ /**
+ * Returns whether the given protocol alpn is supported.
+ */
+ public boolean isSupported(@NonNull String alpn) {
+ return findSvcbRecord(alpn) != null;
+ }
+
+ /**
+ * Returns the TargetName associated with the given protocol alpn.
+ * If the alpn is not supported, a null is returned.
+ */
+ @Nullable
+ public String getTargetName(@NonNull String alpn) {
+ final DnsSvcbRecord record = findSvcbRecord(alpn);
+ return (record != null) ? record.getTargetName() : null;
+ }
+
+ /**
+ * Returns the TargetName that associated with the given protocol alpn.
+ * If the alpn is not supported, -1 is returned.
+ */
+ public int getPort(@NonNull String alpn) {
+ final DnsSvcbRecord record = findSvcbRecord(alpn);
+ return (record != null) ? record.getPort() : -1;
+ }
+
+ /**
+ * Returns the IP addresses that support the given protocol alpn.
+ * If the alpn is not supported, an empty list is returned.
+ */
+ @NonNull
+ public List<InetAddress> getAddresses(@NonNull String alpn) {
+ final DnsSvcbRecord record = findSvcbRecord(alpn);
+ if (record == null) return Collections.EMPTY_LIST;
+
+ // As per draft-ietf-dnsop-svcb-https-10#section-7.4 and draft-ietf-add-ddr-10#section-4,
+ // if A/AAAA records are available in the Additional section, use the IP addresses in
+ // those records instead of the IP addresses in ipv4hint/ipv6hint.
+ final List<InetAddress> out = getAddressesFromAdditionalSection();
+ if (out.size() > 0) return out;
+
+ return record.getAddresses();
+ }
+
+ /**
+ * Returns the value of SVCB key dohpath that associated with the given protocol alpn.
+ * If the alpn is not supported, a null is returned.
+ */
+ @Nullable
+ public String getDohPath(@NonNull String alpn) {
+ final DnsSvcbRecord record = findSvcbRecord(alpn);
+ return (record != null) ? record.getDohPath() : null;
+ }
+
+ /**
+ * Returns the DnsSvcbRecord associated with the given protocol alpn.
+ * If the alpn is not supported, a null is returned.
+ */
+ @Nullable
+ private DnsSvcbRecord findSvcbRecord(@NonNull String alpn) {
+ for (final DnsRecord record : mRecords[ANSECTION]) {
+ if (record instanceof DnsSvcbRecord) {
+ final DnsSvcbRecord svcbRecord = (DnsSvcbRecord) record;
+ if (svcbRecord.getAlpns().contains(alpn)) {
+ return svcbRecord;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the IP addresses in additional section.
+ */
+ @NonNull
+ private List<InetAddress> getAddressesFromAdditionalSection() {
+ final List<InetAddress> out = new ArrayList<InetAddress>();
+ if (mHeader.getRecordCount(ARSECTION) == 0) {
+ return out;
+ }
+ for (final DnsRecord record : mRecords[ARSECTION]) {
+ if (record.nsType != TYPE_A && record.nsType != TYPE_AAAA) {
+ Log.d(TAG, "Found type other than A/AAAA in Additional section: " + record.nsType);
+ continue;
+ }
+ try {
+ out.add(InetAddress.getByAddress(record.getRR()));
+ } catch (UnknownHostException e) {
+ Log.w(TAG, "Failed to parse address");
+ }
+ }
+ return out;
+ }
+
+ /**
+ * Creates a DnsSvcbPacket object from the given wire-format DNS answer.
+ */
+ public static DnsSvcbPacket fromResponse(@NonNull byte[] data) throws DnsPacket.ParseException {
+ DnsSvcbPacket out = new DnsSvcbPacket(data);
+ if (!out.isResponse()) {
+ throw new DnsPacket.ParseException("Not an answer packet");
+ }
+ return out;
+ }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java b/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java
new file mode 100644
index 0000000..935cdf6
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java
@@ -0,0 +1,539 @@
+/*
+ * 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;
+
+import static android.net.DnsResolver.CLASS_IN;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+import static com.android.net.module.util.DnsPacket.ParseException;
+
+import android.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ShortBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.StringJoiner;
+
+/**
+ * A class for an SVCB record.
+ * https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml
+ * @hide
+ */
+@VisibleForTesting(visibility = PACKAGE)
+public final class DnsSvcbRecord extends DnsPacket.DnsRecord {
+ /**
+ * The following SvcParamKeys KEY_* are defined in
+ * https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml.
+ */
+
+ // The SvcParamKey "mandatory". The associated implementation of SvcParam is SvcParamMandatory.
+ private static final int KEY_MANDATORY = 0;
+
+ // The SvcParamKey "alpn". The associated implementation of SvcParam is SvcParamAlpn.
+ private static final int KEY_ALPN = 1;
+
+ // The SvcParamKey "no-default-alpn". The associated implementation of SvcParam is
+ // SvcParamNoDefaultAlpn.
+ private static final int KEY_NO_DEFAULT_ALPN = 2;
+
+ // The SvcParamKey "port". The associated implementation of SvcParam is SvcParamPort.
+ private static final int KEY_PORT = 3;
+
+ // The SvcParamKey "ipv4hint". The associated implementation of SvcParam is SvcParamIpv4Hint.
+ private static final int KEY_IPV4HINT = 4;
+
+ // The SvcParamKey "ech". The associated implementation of SvcParam is SvcParamEch.
+ private static final int KEY_ECH = 5;
+
+ // The SvcParamKey "ipv6hint". The associated implementation of SvcParam is SvcParamIpv6Hint.
+ private static final int KEY_IPV6HINT = 6;
+
+ // The SvcParamKey "dohpath". The associated implementation of SvcParam is SvcParamDohPath.
+ private static final int KEY_DOHPATH = 7;
+
+ // The minimal size of a SvcParam.
+ // https://www.ietf.org/archive/id/draft-ietf-dnsop-svcb-https-12.html#name-rdata-wire-format
+ private static final int MINSVCPARAMSIZE = 4;
+
+ private static final String TAG = DnsSvcbRecord.class.getSimpleName();
+
+ private final int mSvcPriority;
+
+ @NonNull
+ private final String mTargetName;
+
+ @NonNull
+ private final SparseArray<SvcParam> mAllSvcParams = new SparseArray<>();
+
+ @VisibleForTesting(visibility = PACKAGE)
+ public DnsSvcbRecord(@DnsPacket.RecordType int rType, @NonNull ByteBuffer buff)
+ throws IllegalStateException, ParseException {
+ super(rType, buff);
+ if (nsType != DnsPacket.TYPE_SVCB) {
+ throw new IllegalStateException("incorrect nsType: " + nsType);
+ }
+ if (nsClass != CLASS_IN) {
+ throw new ParseException("incorrect nsClass: " + nsClass);
+ }
+
+ // DNS Record in Question Section doesn't have Rdata.
+ if (rType == DnsPacket.QDSECTION) {
+ mSvcPriority = 0;
+ mTargetName = "";
+ return;
+ }
+
+ final byte[] rdata = getRR();
+ if (rdata == null) {
+ throw new ParseException("SVCB rdata is empty");
+ }
+
+ final ByteBuffer buf = ByteBuffer.wrap(rdata).asReadOnlyBuffer();
+ mSvcPriority = Short.toUnsignedInt(buf.getShort());
+ mTargetName = DnsPacketUtils.DnsRecordParser.parseName(buf, 0 /* Parse depth */,
+ false /* isNameCompressionSupported */);
+
+ if (mTargetName.length() > DnsPacket.DnsRecord.MAXNAMESIZE) {
+ throw new ParseException(
+ "Failed to parse SVCB target name, name size is too long: "
+ + mTargetName.length());
+ }
+ while (buf.remaining() >= MINSVCPARAMSIZE) {
+ final SvcParam svcParam = parseSvcParam(buf);
+ final int key = svcParam.getKey();
+ if (mAllSvcParams.get(key) != null) {
+ throw new ParseException("Invalid DnsSvcbRecord, key " + key + " is repeated");
+ }
+ mAllSvcParams.put(key, svcParam);
+ }
+ if (buf.hasRemaining()) {
+ throw new ParseException("Invalid DnsSvcbRecord. Got "
+ + buf.remaining() + " remaining bytes after parsing");
+ }
+ }
+
+ /**
+ * Returns the TargetName.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ @NonNull
+ public String getTargetName() {
+ return mTargetName;
+ }
+
+ /**
+ * Returns an unmodifiable list of alpns from SvcParam alpn.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ @NonNull
+ public List<String> getAlpns() {
+ final SvcParamAlpn sp = (SvcParamAlpn) mAllSvcParams.get(KEY_ALPN);
+ final List<String> list = (sp != null) ? sp.getValue() : Collections.EMPTY_LIST;
+ return Collections.unmodifiableList(list);
+ }
+
+ /**
+ * Returns the port number from SvcParam port.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ public int getPort() {
+ final SvcParamPort sp = (SvcParamPort) mAllSvcParams.get(KEY_PORT);
+ return (sp != null) ? sp.getValue() : -1;
+ }
+
+ /**
+ * Returns a list of the IP addresses from both of SvcParam ipv4hint and ipv6hint.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ @NonNull
+ public List<InetAddress> getAddresses() {
+ final List<InetAddress> out = new ArrayList<>();
+ final SvcParamIpHint sp4 = (SvcParamIpHint) mAllSvcParams.get(KEY_IPV4HINT);
+ if (sp4 != null) {
+ out.addAll(sp4.getValue());
+ }
+ final SvcParamIpHint sp6 = (SvcParamIpHint) mAllSvcParams.get(KEY_IPV6HINT);
+ if (sp6 != null) {
+ out.addAll(sp6.getValue());
+ }
+ return out;
+ }
+
+ /**
+ * Returns the doh path from SvcParam dohPath.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ @NonNull
+ public String getDohPath() {
+ final SvcParamDohPath sp = (SvcParamDohPath) mAllSvcParams.get(KEY_DOHPATH);
+ return (sp != null) ? sp.getValue() : "";
+ }
+
+ @Override
+ public String toString() {
+ if (rType == DnsPacket.QDSECTION) {
+ return dName + " IN SVCB";
+ }
+
+ final StringJoiner sj = new StringJoiner(" ");
+ for (int i = 0; i < mAllSvcParams.size(); i++) {
+ sj.add(mAllSvcParams.valueAt(i).toString());
+ }
+ return dName + " " + ttl + " IN SVCB " + mSvcPriority + " " + mTargetName + " "
+ + sj.toString();
+ }
+
+ private static SvcParam parseSvcParam(@NonNull ByteBuffer buf) throws ParseException {
+ try {
+ final int key = Short.toUnsignedInt(buf.getShort());
+ switch (key) {
+ case KEY_MANDATORY: return new SvcParamMandatory(buf);
+ case KEY_ALPN: return new SvcParamAlpn(buf);
+ case KEY_NO_DEFAULT_ALPN: return new SvcParamNoDefaultAlpn(buf);
+ case KEY_PORT: return new SvcParamPort(buf);
+ case KEY_IPV4HINT: return new SvcParamIpv4Hint(buf);
+ case KEY_ECH: return new SvcParamEch(buf);
+ case KEY_IPV6HINT: return new SvcParamIpv6Hint(buf);
+ case KEY_DOHPATH: return new SvcParamDohPath(buf);
+ default: return new SvcParamGeneric(key, buf);
+ }
+ } catch (BufferUnderflowException e) {
+ throw new ParseException("Malformed packet", e);
+ }
+ }
+
+ /**
+ * The base class for all SvcParam.
+ */
+ private abstract static class SvcParam<T> {
+ private final int mKey;
+
+ SvcParam(int key) {
+ mKey = key;
+ }
+
+ int getKey() {
+ return mKey;
+ }
+
+ abstract T getValue();
+ }
+
+ private static class SvcParamMandatory extends SvcParam<short[]> {
+ private final short[] mValue;
+
+ private SvcParamMandatory(@NonNull ByteBuffer buf) throws BufferUnderflowException,
+ ParseException {
+ super(KEY_MANDATORY);
+ // The caller already read 2 bytes for SvcParamKey.
+ final int len = Short.toUnsignedInt(buf.getShort());
+ final ByteBuffer svcParamValue = sliceAndAdvance(buf, len);
+ mValue = SvcParamValueUtil.toShortArray(svcParamValue);
+ if (mValue.length == 0) {
+ throw new ParseException("mandatory value must be non-empty");
+ }
+ }
+
+ @Override
+ short[] getValue() {
+ /* Not yet implemented */
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ final StringJoiner valueJoiner = new StringJoiner(",");
+ for (short key : mValue) {
+ valueJoiner.add(toKeyName(key));
+ }
+ return toKeyName(getKey()) + "=" + valueJoiner.toString();
+ }
+ }
+
+ private static class SvcParamAlpn extends SvcParam<List<String>> {
+ private final List<String> mValue;
+
+ SvcParamAlpn(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+ super(KEY_ALPN);
+ // The caller already read 2 bytes for SvcParamKey.
+ final int len = Short.toUnsignedInt(buf.getShort());
+ final ByteBuffer svcParamValue = sliceAndAdvance(buf, len);
+ mValue = SvcParamValueUtil.toStringList(svcParamValue);
+ if (mValue.isEmpty()) {
+ throw new ParseException("alpn value must be non-empty");
+ }
+ }
+
+ @Override
+ List<String> getValue() {
+ return Collections.unmodifiableList(mValue);
+ }
+
+ @Override
+ public String toString() {
+ return toKeyName(getKey()) + "=" + TextUtils.join(",", mValue);
+ }
+ }
+
+ private static class SvcParamNoDefaultAlpn extends SvcParam<Void> {
+ SvcParamNoDefaultAlpn(@NonNull ByteBuffer buf) throws BufferUnderflowException,
+ ParseException {
+ super(KEY_NO_DEFAULT_ALPN);
+ // The caller already read 2 bytes for SvcParamKey.
+ final int len = buf.getShort();
+ if (len != 0) {
+ throw new ParseException("no-default-alpn value must be empty");
+ }
+ }
+
+ @Override
+ Void getValue() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return toKeyName(getKey());
+ }
+ }
+
+ private static class SvcParamPort extends SvcParam<Integer> {
+ private final int mValue;
+
+ SvcParamPort(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+ super(KEY_PORT);
+ // The caller already read 2 bytes for SvcParamKey.
+ final int len = buf.getShort();
+ if (len != Short.BYTES) {
+ throw new ParseException("key port len is not 2 but " + len);
+ }
+ mValue = Short.toUnsignedInt(buf.getShort());
+ }
+
+ @Override
+ Integer getValue() {
+ return mValue;
+ }
+
+ @Override
+ public String toString() {
+ return toKeyName(getKey()) + "=" + mValue;
+ }
+ }
+
+ private static class SvcParamIpHint extends SvcParam<List<InetAddress>> {
+ private final List<InetAddress> mValue;
+
+ private SvcParamIpHint(int key, @NonNull ByteBuffer buf, int addrLen) throws
+ BufferUnderflowException, ParseException {
+ super(key);
+ // The caller already read 2 bytes for SvcParamKey.
+ final int len = Short.toUnsignedInt(buf.getShort());
+ final ByteBuffer svcParamValue = sliceAndAdvance(buf, len);
+ mValue = SvcParamValueUtil.toInetAddressList(svcParamValue, addrLen);
+ if (mValue.isEmpty()) {
+ throw new ParseException(toKeyName(getKey()) + " value must be non-empty");
+ }
+ }
+
+ @Override
+ List<InetAddress> getValue() {
+ return Collections.unmodifiableList(mValue);
+ }
+
+ @Override
+ public String toString() {
+ final StringJoiner valueJoiner = new StringJoiner(",");
+ for (InetAddress ip : mValue) {
+ valueJoiner.add(ip.getHostAddress());
+ }
+ return toKeyName(getKey()) + "=" + valueJoiner.toString();
+ }
+ }
+
+ private static class SvcParamIpv4Hint extends SvcParamIpHint {
+ SvcParamIpv4Hint(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+ super(KEY_IPV4HINT, buf, NetworkStackConstants.IPV4_ADDR_LEN);
+ }
+ }
+
+ private static class SvcParamIpv6Hint extends SvcParamIpHint {
+ SvcParamIpv6Hint(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+ super(KEY_IPV6HINT, buf, NetworkStackConstants.IPV6_ADDR_LEN);
+ }
+ }
+
+ private static class SvcParamEch extends SvcParamGeneric {
+ SvcParamEch(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+ super(KEY_ECH, buf);
+ }
+ }
+
+ private static class SvcParamDohPath extends SvcParam<String> {
+ private final String mValue;
+
+ SvcParamDohPath(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+ super(KEY_DOHPATH);
+ // The caller already read 2 bytes for SvcParamKey.
+ final int len = Short.toUnsignedInt(buf.getShort());
+ final byte[] value = new byte[len];
+ buf.get(value);
+ mValue = new String(value, StandardCharsets.UTF_8);
+ }
+
+ @Override
+ String getValue() {
+ return mValue;
+ }
+
+ @Override
+ public String toString() {
+ return toKeyName(getKey()) + "=" + mValue;
+ }
+ }
+
+ // For other unrecognized and unimplemented SvcParams, they are stored as SvcParamGeneric.
+ private static class SvcParamGeneric extends SvcParam<byte[]> {
+ private final byte[] mValue;
+
+ SvcParamGeneric(int key, @NonNull ByteBuffer buf) throws BufferUnderflowException,
+ ParseException {
+ super(key);
+ // The caller already read 2 bytes for SvcParamKey.
+ final int len = Short.toUnsignedInt(buf.getShort());
+ mValue = new byte[len];
+ buf.get(mValue);
+ }
+
+ @Override
+ byte[] getValue() {
+ /* Not yet implemented */
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder out = new StringBuilder();
+ out.append(toKeyName(getKey()));
+ if (mValue != null && mValue.length > 0) {
+ out.append("=");
+ out.append(HexDump.toHexString(mValue));
+ }
+ return out.toString();
+ }
+ }
+
+ private static String toKeyName(int key) {
+ switch (key) {
+ case KEY_MANDATORY: return "mandatory";
+ case KEY_ALPN: return "alpn";
+ case KEY_NO_DEFAULT_ALPN: return "no-default-alpn";
+ case KEY_PORT: return "port";
+ case KEY_IPV4HINT: return "ipv4hint";
+ case KEY_ECH: return "ech";
+ case KEY_IPV6HINT: return "ipv6hint";
+ case KEY_DOHPATH: return "dohpath";
+ default: return "key" + key;
+ }
+ }
+
+ /**
+ * Returns a read-only ByteBuffer (with position = 0, limit = `length`, and capacity = `length`)
+ * sliced from `buf`'s current position, and moves the position of `buf` by `length`.
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public static ByteBuffer sliceAndAdvance(@NonNull ByteBuffer buf, int length)
+ throws BufferUnderflowException {
+ if (buf.remaining() < length) {
+ throw new BufferUnderflowException();
+ }
+ final int pos = buf.position();
+
+ // `out` equals to `buf.slice(pos, length)` that is supported in API level 34.
+ final ByteBuffer out = ((ByteBuffer) buf.slice().limit(length)).slice();
+
+ buf.position(pos + length);
+ return out.asReadOnlyBuffer();
+ }
+
+ // A utility to convert the byte array of SvcParamValue to other types.
+ private static class SvcParamValueUtil {
+ // Refer to draft-ietf-dnsop-svcb-https-10#section-7.1 for the wire format of alpn.
+ @NonNull
+ private static List<String> toStringList(@NonNull ByteBuffer buf)
+ throws BufferUnderflowException, ParseException {
+ final List<String> out = new ArrayList<>();
+ while (buf.hasRemaining()) {
+ final int alpnLen = Byte.toUnsignedInt(buf.get());
+ if (alpnLen == 0) {
+ throw new ParseException("alpn should not be an empty string");
+ }
+ final byte[] alpn = new byte[alpnLen];
+ buf.get(alpn);
+ out.add(new String(alpn, StandardCharsets.UTF_8));
+ }
+ return out;
+ }
+
+ // Refer to draft-ietf-dnsop-svcb-https-10#section-7.5 for the wire format of SvcParamKey
+ // "mandatory".
+ @NonNull
+ private static short[] toShortArray(@NonNull ByteBuffer buf)
+ throws BufferUnderflowException, ParseException {
+ if (buf.remaining() % Short.BYTES != 0) {
+ throw new ParseException("Can't parse whole byte array");
+ }
+ final ShortBuffer sb = buf.asShortBuffer();
+ final short[] out = new short[sb.remaining()];
+ sb.get(out);
+ return out;
+ }
+
+ // Refer to draft-ietf-dnsop-svcb-https-10#section-7.4 for the wire format of ipv4hint and
+ // ipv6hint.
+ @NonNull
+ private static List<InetAddress> toInetAddressList(@NonNull ByteBuffer buf, int addrLen)
+ throws BufferUnderflowException, ParseException {
+ if (buf.remaining() % addrLen != 0) {
+ throw new ParseException("Can't parse whole byte array");
+ }
+
+ final List<InetAddress> out = new ArrayList<>();
+ final byte[] addr = new byte[addrLen];
+ while (buf.remaining() >= addrLen) {
+ buf.get(addr);
+ try {
+ out.add(InetAddress.getByAddress(addr));
+ } catch (UnknownHostException e) {
+ throw new ParseException("Can't parse byte array as an IP address");
+ }
+ }
+ return out;
+ }
+ }
+}
diff --git a/staticlibs/native/bpf_headers/BpfRingbufTest.cpp b/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
index 6c0841c..e4de812 100644
--- a/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
+++ b/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
@@ -72,12 +72,15 @@
auto result = BpfRingbuf<uint64_t>::Create(mRingbufPath.c_str());
ASSERT_RESULT_OK(result);
+ EXPECT_TRUE(result.value()->isEmpty());
for (int i = 0; i < n; i++) {
RunProgram();
}
+ EXPECT_FALSE(result.value()->isEmpty());
EXPECT_THAT(result.value()->ConsumeAll(callback), HasValue(n));
+ EXPECT_TRUE(result.value()->isEmpty());
EXPECT_EQ(output, TEST_RINGBUF_MAGIC_NUM);
EXPECT_EQ(run_count, n);
}
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
index 5d7eb0d..3fede3c 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
@@ -78,7 +78,7 @@
Result<Key> getFirstKey() const {
Key firstKey;
if (getFirstMapKey(mMapFd, &firstKey)) {
- return ErrnoErrorf("Get firstKey map {} failed", mMapFd.get());
+ return ErrnoErrorf("BpfMap::getFirstKey() failed");
}
return firstKey;
}
@@ -86,7 +86,7 @@
Result<Key> getNextKey(const Key& key) const {
Key nextKey;
if (getNextMapKey(mMapFd, &key, &nextKey)) {
- return ErrnoErrorf("Get next key of map {} failed", mMapFd.get());
+ return ErrnoErrorf("BpfMap::getNextKey() failed");
}
return nextKey;
}
@@ -94,7 +94,7 @@
Result<Value> readValue(const Key key) const {
Value value;
if (findMapEntry(mMapFd, &key, &value)) {
- return ErrnoErrorf("Read value of map {} failed", mMapFd.get());
+ return ErrnoErrorf("BpfMap::readValue() failed");
}
return value;
}
@@ -243,14 +243,14 @@
Result<void> writeValue(const Key& key, const Value& value, uint64_t flags) {
if (writeToMapEntry(mMapFd, &key, &value, flags)) {
- return ErrnoErrorf("Write to map {} failed", mMapFd.get());
+ return ErrnoErrorf("BpfMap::writeValue() failed");
}
return {};
}
Result<void> deleteValue(const Key& key) {
if (deleteMapEntry(mMapFd, &key)) {
- return ErrnoErrorf("Delete entry from map {} failed", mMapFd.get());
+ return ErrnoErrorf("BpfMap::deleteValue() failed");
}
return {};
}
@@ -280,7 +280,7 @@
if (map_flags & BPF_F_RDONLY) abort();
mMapFd.reset(createMap(map_type, sizeof(Key), sizeof(Value), max_entries,
map_flags));
- if (!mMapFd.ok()) return ErrnoErrorf("Unable to create map.");
+ if (!mMapFd.ok()) return ErrnoErrorf("BpfMap::resetMap() failed");
abortOnMismatch(/* writable */ true);
return {};
}
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
index dd1504c..9aff790 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
@@ -39,6 +39,8 @@
mProducerPos = nullptr;
}
+ bool isEmpty(void);
+
protected:
// Non-initializing constructor, used by Create.
BpfRingbufBase(size_t value_size) : mValueSize(value_size) {}
@@ -197,6 +199,13 @@
return {};
}
+inline bool BpfRingbufBase::isEmpty(void) {
+ uint32_t prod_pos = mProducerPos->load(std::memory_order_acquire);
+ // Only userspace writes to mConsumerPos, so no need to use std::memory_order_acquire
+ uint64_t cons_pos = mConsumerPos->load(std::memory_order_relaxed);
+ return (cons_pos & 0xFFFFFFFF) == prod_pos;
+}
+
inline base::Result<int> BpfRingbufBase::ConsumeAll(
const std::function<void(const void*)>& callback) {
int64_t count = 0;
diff --git a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
index f93d6e1..b92f107 100644
--- a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
@@ -15,6 +15,8 @@
*/
#include <errno.h>
+#include <linux/pfkeyv2.h>
+#include <sys/socket.h>
#include <jni.h>
#include <nativehelper/JNIHelp.h>
#include <nativehelper/ScopedLocalRef.h>
@@ -117,6 +119,22 @@
return throwIfNotEnoent(env, "nativeFindMapEntry", ret, errno);
}
+static void com_android_net_module_util_BpfMap_nativeSynchronizeKernelRCU(JNIEnv *env,
+ jclass clazz) {
+ const int pfSocket = socket(AF_KEY, SOCK_RAW | SOCK_CLOEXEC, PF_KEY_V2);
+
+ if (pfSocket < 0) {
+ jniThrowErrnoException(env, "nativeSynchronizeKernelRCU:socket", errno);
+ return;
+ }
+
+ if (close(pfSocket)) {
+ jniThrowErrnoException(env, "nativeSynchronizeKernelRCU:close", errno);
+ return;
+ }
+ return;
+}
+
/*
* JNI registration.
*/
@@ -132,6 +150,8 @@
(void*) com_android_net_module_util_BpfMap_nativeGetNextMapKey },
{ "nativeFindMapEntry", "(I[B[B)Z",
(void*) com_android_net_module_util_BpfMap_nativeFindMapEntry },
+ { "nativeSynchronizeKernelRCU", "()V",
+ (void*) com_android_net_module_util_BpfMap_nativeSynchronizeKernelRCU },
};
diff --git a/staticlibs/netd/Android.bp b/staticlibs/netd/Android.bp
index 65b3b09..56565ed 100644
--- a/staticlibs/netd/Android.bp
+++ b/staticlibs/netd/Android.bp
@@ -21,7 +21,7 @@
sdk_version: "system_current",
min_sdk_version: "30",
static_libs: [
- "netd_aidl_interface-V13-java",
+ "netd_aidl_interface-V14-java",
],
apex_available: [
"//apex_available:platform", // used from services.net
@@ -44,7 +44,7 @@
cc_library_static {
name: "netd_aidl_interface-lateststable-ndk",
whole_static_libs: [
- "netd_aidl_interface-V13-ndk",
+ "netd_aidl_interface-V14-ndk",
],
apex_available: [
"com.android.resolv",
@@ -55,12 +55,12 @@
cc_defaults {
name: "netd_aidl_interface_lateststable_cpp_static",
- static_libs: ["netd_aidl_interface-V13-cpp"],
+ static_libs: ["netd_aidl_interface-V14-cpp"],
}
cc_defaults {
name: "netd_aidl_interface_lateststable_cpp_shared",
- shared_libs: ["netd_aidl_interface-V13-cpp"],
+ shared_libs: ["netd_aidl_interface-V14-cpp"],
}
aidl_interface {
@@ -162,8 +162,13 @@
version: "13",
imports: [],
},
+ {
+ version: "14",
+ imports: [],
+ },
],
+ frozen: true,
}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/14/.hash
new file mode 100644
index 0000000..0bf7bde
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/.hash
@@ -0,0 +1 @@
+50bce96bc8d5811ed952950df30ec503f8a561ed
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetd.aidl
new file mode 100644
index 0000000..8ccefb2
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetd.aidl
@@ -0,0 +1,259 @@
+/**
+ * Copyright (c) 2016, 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;
+/* @hide */
+interface INetd {
+ boolean isAlive();
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isAllowlist, in int[] uids);
+ boolean bandwidthEnableDataSaver(boolean enable);
+ /**
+ * @deprecated use networkCreate() instead.
+ */
+ void networkCreatePhysical(int netId, int permission);
+ /**
+ * @deprecated use networkCreate() instead.
+ */
+ void networkCreateVpn(int netId, boolean secure);
+ void networkDestroy(int netId);
+ void networkAddInterface(int netId, in @utf8InCpp String iface);
+ void networkRemoveInterface(int netId, in @utf8InCpp String iface);
+ void networkAddUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+ void networkRemoveUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+ void networkRejectNonSecureVpn(boolean add, in android.net.UidRangeParcel[] uidRanges);
+ void socketDestroy(in android.net.UidRangeParcel[] uidRanges, in int[] exemptUids);
+ boolean tetherApplyDnsInterfaces();
+ android.net.TetherStatsParcel[] tetherGetStats();
+ void interfaceAddAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+ void interfaceDelAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+ @utf8InCpp String getProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter);
+ void setProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter, in @utf8InCpp String value);
+ void ipSecSetEncapSocketOwner(in ParcelFileDescriptor socket, int newUid);
+ int ipSecAllocateSpi(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+ void ipSecAddSecurityAssociation(int transformId, int mode, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int underlyingNetId, int spi, int markValue, int markMask, in @utf8InCpp String authAlgo, in byte[] authKey, in int authTruncBits, in @utf8InCpp String cryptAlgo, in byte[] cryptKey, in int cryptTruncBits, in @utf8InCpp String aeadAlgo, in byte[] aeadKey, in int aeadIcvBits, int encapType, int encapLocalPort, int encapRemotePort, int interfaceId);
+ void ipSecDeleteSecurityAssociation(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi, int markValue, int markMask, int interfaceId);
+ void ipSecApplyTransportModeTransform(in ParcelFileDescriptor socket, int transformId, int direction, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+ void ipSecRemoveTransportModeTransform(in ParcelFileDescriptor socket);
+ void ipSecAddSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+ void ipSecUpdateSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+ void ipSecDeleteSecurityPolicy(int transformId, int selAddrFamily, int direction, int markValue, int markMask, int interfaceId);
+ void ipSecAddTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+ void ipSecUpdateTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+ void ipSecRemoveTunnelInterface(in @utf8InCpp String deviceName);
+ void wakeupAddInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+ void wakeupDelInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+ void setIPv6AddrGenMode(in @utf8InCpp String ifName, int mode);
+ void idletimerAddInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+ void idletimerRemoveInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+ void strictUidCleartextPenalty(int uid, int policyPenalty);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd control plane moved to the mainline module starting in T. See ClatCoordinator.
+ */
+ @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd control plane moved to the mainline module starting in T. See ClatCoordinator.
+ */
+ void clatdStop(in @utf8InCpp String ifName);
+ boolean ipfwdEnabled();
+ @utf8InCpp String[] ipfwdGetRequesterList();
+ void ipfwdEnableForwarding(in @utf8InCpp String requester);
+ void ipfwdDisableForwarding(in @utf8InCpp String requester);
+ void ipfwdAddInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+ void ipfwdRemoveInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+ void bandwidthSetInterfaceQuota(in @utf8InCpp String ifName, long bytes);
+ void bandwidthRemoveInterfaceQuota(in @utf8InCpp String ifName);
+ void bandwidthSetInterfaceAlert(in @utf8InCpp String ifName, long bytes);
+ void bandwidthRemoveInterfaceAlert(in @utf8InCpp String ifName);
+ void bandwidthSetGlobalAlert(long bytes);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthAddNaughtyApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthRemoveNaughtyApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthAddNiceApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthRemoveNiceApp(int uid);
+ void tetherStart(in @utf8InCpp String[] dhcpRanges);
+ void tetherStop();
+ boolean tetherIsEnabled();
+ void tetherInterfaceAdd(in @utf8InCpp String ifName);
+ void tetherInterfaceRemove(in @utf8InCpp String ifName);
+ @utf8InCpp String[] tetherInterfaceList();
+ void tetherDnsSet(int netId, in @utf8InCpp String[] dnsAddrs);
+ @utf8InCpp String[] tetherDnsList();
+ void networkAddRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+ void networkRemoveRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+ void networkAddLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+ void networkRemoveLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+ int networkGetDefault();
+ void networkSetDefault(int netId);
+ void networkClearDefault();
+ void networkSetPermissionForNetwork(int netId, int permission);
+ void networkSetPermissionForUser(int permission, in int[] uids);
+ void networkClearPermissionForUser(in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void trafficSetNetPermForUids(int permission, in int[] uids);
+ void networkSetProtectAllow(int uid);
+ void networkSetProtectDeny(int uid);
+ boolean networkCanProtect(int uid);
+ void firewallSetFirewallType(int firewalltype);
+ void firewallSetInterfaceRule(in @utf8InCpp String ifName, int firewallRule);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallSetUidRule(int childChain, int uid, int firewallRule);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallEnableChildChain(int childChain, boolean enable);
+ @utf8InCpp String[] interfaceGetList();
+ android.net.InterfaceConfigurationParcel interfaceGetCfg(in @utf8InCpp String ifName);
+ void interfaceSetCfg(in android.net.InterfaceConfigurationParcel cfg);
+ void interfaceSetIPv6PrivacyExtensions(in @utf8InCpp String ifName, boolean enable);
+ void interfaceClearAddrs(in @utf8InCpp String ifName);
+ void interfaceSetEnableIPv6(in @utf8InCpp String ifName, boolean enable);
+ void interfaceSetMtu(in @utf8InCpp String ifName, int mtu);
+ void tetherAddForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+ void tetherRemoveForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+ void setTcpRWmemorySize(in @utf8InCpp String rmemValues, in @utf8InCpp String wmemValues);
+ void registerUnsolicitedEventListener(android.net.INetdUnsolicitedEventListener listener);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallRemoveUidInterfaceRules(in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void trafficSwapActiveStatsMap();
+ IBinder getOemNetd();
+ void tetherStartWithConfiguration(in android.net.TetherConfigParcel config);
+ android.net.MarkMaskParcel getFwmarkForNetwork(int netId);
+ void networkAddRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+ void networkUpdateRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+ void networkRemoveRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ void tetherOffloadRuleAdd(in android.net.TetherOffloadRuleParcel rule);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ void tetherOffloadRuleRemove(in android.net.TetherOffloadRuleParcel rule);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ android.net.TetherStatsParcel[] tetherOffloadGetStats();
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ void tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ android.net.TetherStatsParcel tetherOffloadGetAndClearStats(int ifIndex);
+ void networkCreate(in android.net.NativeNetworkConfig config);
+ void networkAddUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+ void networkRemoveUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+ void ipSecMigrate(in android.net.IpSecMigrateInfoParcel migrateInfo);
+ void setNetworkAllowlist(in android.net.netd.aidl.NativeUidRangeConfig[] allowedNetworks);
+ const int IPV4 = 4;
+ const int IPV6 = 6;
+ const int CONF = 1;
+ const int NEIGH = 2;
+ const String IPSEC_INTERFACE_PREFIX = "ipsec";
+ const int IPV6_ADDR_GEN_MODE_EUI64 = 0;
+ const int IPV6_ADDR_GEN_MODE_NONE = 1;
+ const int IPV6_ADDR_GEN_MODE_STABLE_PRIVACY = 2;
+ const int IPV6_ADDR_GEN_MODE_RANDOM = 3;
+ const int IPV6_ADDR_GEN_MODE_DEFAULT = 0;
+ const int PENALTY_POLICY_ACCEPT = 1;
+ const int PENALTY_POLICY_LOG = 2;
+ const int PENALTY_POLICY_REJECT = 3;
+ const int CLAT_MARK = 0xdeadc1a7;
+ const int LOCAL_NET_ID = 99;
+ const int DUMMY_NET_ID = 51;
+ const int UNREACHABLE_NET_ID = 52;
+ const String NEXTHOP_NONE = "";
+ const String NEXTHOP_UNREACHABLE = "unreachable";
+ const String NEXTHOP_THROW = "throw";
+ const int PERMISSION_NONE = 0;
+ const int PERMISSION_NETWORK = 1;
+ const int PERMISSION_SYSTEM = 2;
+ const int NO_PERMISSIONS = 0;
+ const int PERMISSION_INTERNET = 4;
+ const int PERMISSION_UPDATE_DEVICE_STATS = 8;
+ const int PERMISSION_UNINSTALLED = (-1) /* -1 */;
+ /**
+ * @deprecated use FIREWALL_ALLOWLIST.
+ */
+ const int FIREWALL_WHITELIST = 0;
+ const int FIREWALL_ALLOWLIST = 0;
+ /**
+ * @deprecated use FIREWALL_DENYLIST.
+ */
+ const int FIREWALL_BLACKLIST = 1;
+ const int FIREWALL_DENYLIST = 1;
+ const int FIREWALL_RULE_ALLOW = 1;
+ const int FIREWALL_RULE_DENY = 2;
+ const int FIREWALL_CHAIN_NONE = 0;
+ const int FIREWALL_CHAIN_DOZABLE = 1;
+ const int FIREWALL_CHAIN_STANDBY = 2;
+ const int FIREWALL_CHAIN_POWERSAVE = 3;
+ const int FIREWALL_CHAIN_RESTRICTED = 4;
+ const String IF_STATE_UP = "up";
+ const String IF_STATE_DOWN = "down";
+ const String IF_FLAG_BROADCAST = "broadcast";
+ const String IF_FLAG_LOOPBACK = "loopback";
+ const String IF_FLAG_POINTOPOINT = "point-to-point";
+ const String IF_FLAG_RUNNING = "running";
+ const String IF_FLAG_MULTICAST = "multicast";
+ const int IPSEC_DIRECTION_IN = 0;
+ const int IPSEC_DIRECTION_OUT = 1;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetdUnsolicitedEventListener.aidl
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2018, 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;
+/* @hide */
+interface INetdUnsolicitedEventListener {
+ oneway void onInterfaceClassActivityChanged(boolean isActive, int timerLabel, long timestampNs, int uid);
+ oneway void onQuotaLimitReached(@utf8InCpp String alertName, @utf8InCpp String ifName);
+ oneway void onInterfaceDnsServerInfo(@utf8InCpp String ifName, long lifetimeS, in @utf8InCpp String[] servers);
+ oneway void onInterfaceAddressUpdated(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+ oneway void onInterfaceAddressRemoved(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+ oneway void onInterfaceAdded(@utf8InCpp String ifName);
+ oneway void onInterfaceRemoved(@utf8InCpp String ifName);
+ oneway void onInterfaceChanged(@utf8InCpp String ifName, boolean up);
+ oneway void onInterfaceLinkStateChanged(@utf8InCpp String ifName, boolean up);
+ oneway void onRouteChanged(boolean updated, @utf8InCpp String route, @utf8InCpp String gateway, @utf8InCpp String ifName);
+ oneway void onStrictCleartextDetected(int uid, @utf8InCpp String hex);
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/InterfaceConfigurationParcel.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 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;
+/* @hide */
+parcelable InterfaceConfigurationParcel {
+ @utf8InCpp String ifName;
+ @utf8InCpp String hwAddr;
+ @utf8InCpp String ipv4Addr;
+ int prefixLength;
+ @utf8InCpp String[] flags;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/IpSecMigrateInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/IpSecMigrateInfoParcel.aidl
new file mode 100644
index 0000000..975a261
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/IpSecMigrateInfoParcel.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;
+@JavaOnlyImmutable
+parcelable IpSecMigrateInfoParcel {
+ int requestId;
+ int selAddrFamily;
+ int direction;
+ @utf8InCpp String oldSourceAddress;
+ @utf8InCpp String oldDestinationAddress;
+ @utf8InCpp String newSourceAddress;
+ @utf8InCpp String newDestinationAddress;
+ int interfaceId;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/MarkMaskParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 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;
+/* @hide */
+parcelable MarkMaskParcel {
+ int mark;
+ int mask;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkConfig.aidl
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 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;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeNetworkConfig {
+ int netId;
+ android.net.NativeNetworkType networkType = android.net.NativeNetworkType.PHYSICAL;
+ int permission;
+ boolean secure;
+ android.net.NativeVpnType vpnType = android.net.NativeVpnType.PLATFORM;
+ boolean excludeLocalRoutes = false;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..e77a143
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkType.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 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;
+@Backing(type="int")
+enum NativeNetworkType {
+ PHYSICAL = 0,
+ VIRTUAL = 1,
+ PHYSICAL_LOCAL = 2,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeVpnType.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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;
+@Backing(type="int")
+enum NativeVpnType {
+ SERVICE = 1,
+ PLATFORM = 2,
+ LEGACY = 3,
+ OEM = 4,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/RouteInfoParcel.aidl
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// 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;
+parcelable RouteInfoParcel {
+ @utf8InCpp String destination;
+ @utf8InCpp String ifName;
+ @utf8InCpp String nextHop;
+ int mtu;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherConfigParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 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;
+/* @hide */
+parcelable TetherConfigParcel {
+ boolean usingLegacyDnsProxy;
+ @utf8InCpp String[] dhcpRanges;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherOffloadRuleParcel.aidl
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// 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;
+/* @hide */
+parcelable TetherOffloadRuleParcel {
+ int inputInterfaceIndex;
+ int outputInterfaceIndex;
+ byte[] destination;
+ int prefixLength;
+ byte[] srcL2Address;
+ byte[] dstL2Address;
+ int pmtu = 1500;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherStatsParcel.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 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;
+/* @hide */
+parcelable TetherStatsParcel {
+ @utf8InCpp String iface;
+ long rxBytes;
+ long rxPackets;
+ long txBytes;
+ long txPackets;
+ int ifIndex = 0;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/UidRangeParcel.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 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;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable UidRangeParcel {
+ int start;
+ int stop;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/netd/aidl/NativeUidRangeConfig.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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.netd.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeUidRangeConfig {
+ int netId;
+ android.net.UidRangeParcel[] uidRanges;
+ int subPriority;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
index 3507784..8ccefb2 100644
--- a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
@@ -35,6 +35,9 @@
/* @hide */
interface INetd {
boolean isAlive();
+ /**
+ * @deprecated unimplemented on T+.
+ */
boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isAllowlist, in int[] uids);
boolean bandwidthEnableDataSaver(boolean enable);
/**
@@ -95,9 +98,21 @@
void bandwidthSetInterfaceAlert(in @utf8InCpp String ifName, long bytes);
void bandwidthRemoveInterfaceAlert(in @utf8InCpp String ifName);
void bandwidthSetGlobalAlert(long bytes);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void bandwidthAddNaughtyApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void bandwidthRemoveNaughtyApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void bandwidthAddNiceApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void bandwidthRemoveNiceApp(int uid);
void tetherStart(in @utf8InCpp String[] dhcpRanges);
void tetherStop();
@@ -117,13 +132,22 @@
void networkSetPermissionForNetwork(int netId, int permission);
void networkSetPermissionForUser(int permission, in int[] uids);
void networkClearPermissionForUser(in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void trafficSetNetPermForUids(int permission, in int[] uids);
void networkSetProtectAllow(int uid);
void networkSetProtectDeny(int uid);
boolean networkCanProtect(int uid);
void firewallSetFirewallType(int firewalltype);
void firewallSetInterfaceRule(in @utf8InCpp String ifName, int firewallRule);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void firewallSetUidRule(int childChain, int uid, int firewallRule);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void firewallEnableChildChain(int childChain, boolean enable);
@utf8InCpp String[] interfaceGetList();
android.net.InterfaceConfigurationParcel interfaceGetCfg(in @utf8InCpp String ifName);
@@ -136,8 +160,17 @@
void tetherRemoveForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
void setTcpRWmemorySize(in @utf8InCpp String rmemValues, in @utf8InCpp String wmemValues);
void registerUnsolicitedEventListener(android.net.INetdUnsolicitedEventListener listener);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void firewallRemoveUidInterfaceRules(in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void trafficSwapActiveStatsMap();
IBinder getOemNetd();
void tetherStartWithConfiguration(in android.net.TetherConfigParcel config);
@@ -196,7 +229,7 @@
const int NO_PERMISSIONS = 0;
const int PERMISSION_INTERNET = 4;
const int PERMISSION_UPDATE_DEVICE_STATS = 8;
- const int PERMISSION_UNINSTALLED = (-1);
+ const int PERMISSION_UNINSTALLED = (-1) /* -1 */;
/**
* @deprecated use FIREWALL_ALLOWLIST.
*/
diff --git a/staticlibs/netd/binder/android/net/INetd.aidl b/staticlibs/netd/binder/android/net/INetd.aidl
index 27d9a03..ee27e84 100644
--- a/staticlibs/netd/binder/android/net/INetd.aidl
+++ b/staticlibs/netd/binder/android/net/INetd.aidl
@@ -47,6 +47,7 @@
* @param isAllowlist Whether this is an allowlist or denylist chain.
* @param uids The list of UIDs to allow/deny.
* @return true if the chain was successfully replaced, false otherwise.
+ * @deprecated unimplemented on T+.
*/
boolean firewallReplaceUidChain(in @utf8InCpp String chainName,
boolean isAllowlist,
@@ -683,6 +684,7 @@
* @param uid uid of target app
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void bandwidthAddNaughtyApp(int uid);
@@ -692,6 +694,7 @@
* @param uid uid of target app
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void bandwidthRemoveNaughtyApp(int uid);
@@ -701,6 +704,7 @@
* @param uid uid of target app
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void bandwidthAddNiceApp(int uid);
@@ -710,6 +714,7 @@
* @param uid uid of target app
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void bandwidthRemoveNiceApp(int uid);
@@ -983,6 +988,7 @@
* PERMISSION_UPDATE_DEVICE_STATS. If the permission is NO_PERMISSIONS, then
* revoke all permissions for the uids.
* @param uids uid of users to grant permission
+ * @deprecated unimplemented on T+.
*/
void trafficSetNetPermForUids(int permission, in int[] uids);
@@ -1071,6 +1077,7 @@
* @param firewallRule either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void firewallSetUidRule(int childChain, int uid, int firewallRule);
@@ -1081,6 +1088,7 @@
* @param enable whether to enable or disable child chain.
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void firewallEnableChildChain(int childChain, boolean enable);
@@ -1212,6 +1220,7 @@
* @param uids an array of UIDs which the filtering rules will be set
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
@@ -1224,6 +1233,7 @@
* @param uids an array of UIDs from which the filtering rules will be removed
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void firewallRemoveUidInterfaceRules(in int[] uids);
@@ -1231,6 +1241,7 @@
* Request netd to change the current active network stats map.
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void trafficSwapActiveStatsMap();
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketTest.java
index 28e183a..88d9e1e 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketTest.java
@@ -203,6 +203,30 @@
"test.com", CLASS_IN, 0 /* ttl */, "example.com"));
}
+ /** Verifies that the type of implementation returned from DnsRecord#parse is correct */
+ @Test
+ public void testDnsRecordParse() throws IOException {
+ final byte[] svcbQuestionRecord = new byte[] {
+ 0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0x03, 'c', 'o', 'm', 0x00, /* Name */
+ 0x00, 0x40, /* Type */
+ 0x00, 0x01, /* Class */
+ };
+ assertTrue(DnsPacket.DnsRecord.parse(DnsPacket.QDSECTION,
+ ByteBuffer.wrap(svcbQuestionRecord)) instanceof DnsSvcbRecord);
+
+ final byte[] svcbAnswerRecord = new byte[] {
+ 0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0x03, 'c', 'o', 'm', 0x00, /* Name */
+ 0x00, 0x40, /* Type */
+ 0x00, 0x01, /* Class */
+ 0x00, 0x00, 0x01, 0x2b, /* TTL */
+ 0x00, 0x0b, /* Data length */
+ 0x00, 0x01, /* SvcPriority */
+ 0x03, 'd', 'o', 't', 0x03, 'c', 'o', 'm', 0x00, /* TargetName */
+ };
+ assertTrue(DnsPacket.DnsRecord.parse(DnsPacket.ANSECTION,
+ ByteBuffer.wrap(svcbAnswerRecord)) instanceof DnsSvcbRecord);
+ }
+
/**
* Verifies ttl/rData error handling when parsing
* {@link DnsPacket.DnsRecord} from bytes.
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java
new file mode 100644
index 0000000..d59795f
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java
@@ -0,0 +1,608 @@
+/*
+ * 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;
+
+import static android.net.DnsResolver.CLASS_IN;
+import static android.net.DnsResolver.TYPE_A;
+import static android.net.DnsResolver.TYPE_AAAA;
+
+import static com.android.net.module.util.DnsPacket.TYPE_SVCB;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.annotation.NonNull;
+import android.net.InetAddresses;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class DnsSvcbPacketTest {
+ private static final short TEST_TRANSACTION_ID = 0x4321;
+ private static final byte[] TEST_DNS_RESPONSE_HEADER_FLAG = new byte[] { (byte) 0x81, 0x00 };
+
+ // A common DNS SVCB Question section with Name = "_dns.resolver.arpa".
+ private static final byte[] TEST_DNS_SVCB_QUESTION_SECTION = new byte[] {
+ 0x04, '_', 'd', 'n', 's', 0x08, 'r', 'e', 's', 'o', 'l', 'v', 'e', 'r',
+ 0x04, 'a', 'r', 'p', 'a', 0x00, 0x00, 0x40, 0x00, 0x01,
+ };
+
+ // mandatory=ipv4hint,alpn,key333
+ private static final byte[] TEST_SVC_PARAM_MANDATORY = new byte[] {
+ 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, 0x01, 0x01, 0x4d,
+ };
+
+ // alpn=doq
+ private static final byte[] TEST_SVC_PARAM_ALPN_DOQ = new byte[] {
+ 0x00, 0x01, 0x00, 0x04, 0x03, 'd', 'o', 'q'
+ };
+
+ // alpn=h2,http/1.1
+ private static final byte[] TEST_SVC_PARAM_ALPN_HTTPS = new byte[] {
+ 0x00, 0x01, 0x00, 0x0c, 0x02, 'h', '2',
+ 0x08, 'h', 't', 't', 'p', '/', '1', '.', '1',
+ };
+
+ // no-default-alpn
+ private static final byte[] TEST_SVC_PARAM_NO_DEFAULT_ALPN = new byte[] {
+ 0x00, 0x02, 0x00, 0x00,
+ };
+
+ // port=5353
+ private static final byte[] TEST_SVC_PARAM_PORT = new byte[] {
+ 0x00, 0x03, 0x00, 0x02, 0x14, (byte) 0xe9,
+ };
+
+ // ipv4hint=1.2.3.4,6.7.8.9
+ private static final byte[] TEST_SVC_PARAM_IPV4HINT_1 = new byte[] {
+ 0x00, 0x04, 0x00, 0x08, 0x01, 0x02, 0x03, 0x04, 0x06, 0x07, 0x08, 0x09,
+ };
+
+ // ipv4hint=4.3.2.1
+ private static final byte[] TEST_SVC_PARAM_IPV4HINT_2 = new byte[] {
+ 0x00, 0x04, 0x00, 0x04, 0x04, 0x03, 0x02, 0x01,
+ };
+
+ // ech=aBcDe
+ private static final byte[] TEST_SVC_PARAM_ECH = new byte[] {
+ 0x00, 0x05, 0x00, 0x05, 'a', 'B', 'c', 'D', 'e',
+ };
+
+ // ipv6hint=2001:db8::1
+ private static final byte[] TEST_SVC_PARAM_IPV6HINT = new byte[] {
+ 0x00, 0x06, 0x00, 0x10, 0x20, 0x01, 0x0d, (byte) 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+ };
+
+ // dohpath=/some-path{?dns}
+ private static final byte[] TEST_SVC_PARAM_DOHPATH = new byte[] {
+ 0x00, 0x07, 0x00, 0x10,
+ '/', 's', 'o', 'm', 'e', '-', 'p', 'a', 't', 'h', '{', '?', 'd', 'n', 's', '}',
+ };
+
+ // key12345=1A2B0C
+ private static final byte[] TEST_SVC_PARAM_GENERIC_WITH_VALUE = new byte[] {
+ 0x30, 0x39, 0x00, 0x03, 0x1a, 0x2b, 0x0c,
+ };
+
+ // key12346
+ private static final byte[] TEST_SVC_PARAM_GENERIC_WITHOUT_VALUE = new byte[] {
+ 0x30, 0x3a, 0x00, 0x00,
+ };
+
+ private static byte[] makeDnsResponseHeaderAsByteArray(int qdcount, int ancount, int nscount,
+ int arcount) {
+ final ByteBuffer buffer = ByteBuffer.wrap(new byte[12]);
+ buffer.putShort(TEST_TRANSACTION_ID); /* Transaction ID */
+ buffer.put(TEST_DNS_RESPONSE_HEADER_FLAG); /* Flags */
+ buffer.putShort((short) qdcount);
+ buffer.putShort((short) ancount);
+ buffer.putShort((short) nscount);
+ buffer.putShort((short) arcount);
+ return buffer.array();
+ }
+
+ private static DnsSvcbRecord makeDnsSvcbRecordFromByteArray(@NonNull byte[] data)
+ throws IOException {
+ return new DnsSvcbRecord(DnsPacket.ANSECTION, ByteBuffer.wrap(data));
+ }
+
+ private static DnsSvcbRecord makeDnsSvcbRecordWithSingleSvcParam(@NonNull byte[] svcParam)
+ throws IOException {
+ return makeDnsSvcbRecordFromByteArray(new TestDnsRecordByteArrayBuilder()
+ .setRRType(TYPE_SVCB)
+ .setTargetName("test.com")
+ .addRdata(svcParam)
+ .build());
+ }
+
+ // Converts a Short to a byte array in big endian.
+ private static byte[] shortToByteArray(short value) {
+ return new byte[] { (byte) (value >> 8), (byte) value };
+ }
+
+ private static byte[] getRemainingByteArray(@NonNull ByteBuffer buffer) {
+ final byte[] out = new byte[buffer.remaining()];
+ buffer.get(out);
+ return out;
+ }
+
+ // A utility to make a DNS record as byte array.
+ private static class TestDnsRecordByteArrayBuilder {
+ private static final byte[] NAME_COMPRESSION_POINTER = new byte[] { (byte) 0xc0, 0x0c };
+
+ private final String mRRName = "dns.com";
+ private short mRRType = 0;
+ private final short mRRClass = CLASS_IN;
+ private final int mRRTtl = 10;
+ private int mRdataLen = 0;
+ private final ArrayList<byte[]> mRdata = new ArrayList<>();
+ private String mTargetName = null;
+ private short mSvcPriority = 1;
+ private boolean mNameCompression = false;
+
+ TestDnsRecordByteArrayBuilder setNameCompression(boolean value) {
+ mNameCompression = value;
+ return this;
+ }
+
+ TestDnsRecordByteArrayBuilder setRRType(int value) {
+ mRRType = (short) value;
+ return this;
+ }
+
+ TestDnsRecordByteArrayBuilder setTargetName(@NonNull String value) throws IOException {
+ mTargetName = value;
+ return this;
+ }
+
+ TestDnsRecordByteArrayBuilder setSvcPriority(int value) {
+ mSvcPriority = (short) value;
+ return this;
+ }
+
+ TestDnsRecordByteArrayBuilder addRdata(@NonNull byte[] value) {
+ mRdata.add(value);
+ mRdataLen += value.length;
+ return this;
+ }
+
+ byte[] build() throws IOException {
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ final byte[] name = mNameCompression ? NAME_COMPRESSION_POINTER
+ : DnsPacketUtils.DnsRecordParser.domainNameToLabels(mRRName);
+ os.write(name);
+ os.write(shortToByteArray(mRRType));
+ os.write(shortToByteArray(mRRClass));
+ os.write(HexDump.toByteArray(mRRTtl));
+ if (mTargetName == null) {
+ os.write(shortToByteArray((short) mRdataLen));
+ } else {
+ final byte[] targetNameLabels =
+ DnsPacketUtils.DnsRecordParser.domainNameToLabels(mTargetName);
+ mRdataLen += (Short.BYTES + targetNameLabels.length);
+ os.write(shortToByteArray((short) mRdataLen));
+ os.write(shortToByteArray(mSvcPriority));
+ os.write(targetNameLabels);
+ }
+ for (byte[] data : mRdata) {
+ os.write(data);
+ }
+ return os.toByteArray();
+ }
+ }
+
+ @Test
+ public void testSliceAndAdvance() throws Exception {
+ final ByteBuffer buffer = ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9});
+ final ByteBuffer slice1 = DnsSvcbRecord.sliceAndAdvance(buffer, 3);
+ final ByteBuffer slice2 = DnsSvcbRecord.sliceAndAdvance(buffer, 4);
+ assertEquals(0, slice1.position());
+ assertEquals(3, slice1.capacity());
+ assertEquals(3, slice1.remaining());
+ assertTrue(slice1.isReadOnly());
+ assertArrayEquals(new byte[] {1, 2, 3}, getRemainingByteArray(slice1));
+ assertEquals(0, slice2.position());
+ assertEquals(4, slice2.capacity());
+ assertEquals(4, slice2.remaining());
+ assertTrue(slice2.isReadOnly());
+ assertArrayEquals(new byte[] {4, 5, 6, 7}, getRemainingByteArray(slice2));
+
+ // Nothing is read if out-of-bound access happens.
+ assertThrows(BufferUnderflowException.class,
+ () -> DnsSvcbRecord.sliceAndAdvance(buffer, 5));
+ assertEquals(7, buffer.position());
+ assertEquals(9, buffer.capacity());
+ assertEquals(2, buffer.remaining());
+ assertArrayEquals(new byte[] {8, 9}, getRemainingByteArray(buffer));
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamMandatory() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_MANDATORY);
+ // Check the content returned from toString() for now because the getter function for
+ // this SvcParam hasn't been implemented.
+ // TODO(b/240259333): Consider adding DnsSvcbRecord.isMandatory(String alpn) when needed.
+ assertTrue(record.toString().contains("ipv4hint"));
+ assertTrue(record.toString().contains("alpn"));
+ assertTrue(record.toString().contains("key333"));
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamAlpn() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_ALPN_HTTPS);
+ assertEquals(Arrays.asList("h2", "http/1.1"), record.getAlpns());
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamNoDefaultAlpn() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(
+ TEST_SVC_PARAM_NO_DEFAULT_ALPN);
+ // Check the content returned from toString() for now because the getter function for
+ // this SvcParam hasn't been implemented.
+ // TODO(b/240259333): Consider adding DnsSvcbRecord.hasNoDefaultAlpn() when needed.
+ assertTrue(record.toString().contains("no-default-alpn"));
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamPort() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_PORT);
+ assertEquals(5353, record.getPort());
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamIpv4Hint() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_IPV4HINT_2);
+ assertEquals(Arrays.asList(InetAddresses.parseNumericAddress("4.3.2.1")),
+ record.getAddresses());
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamEch() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_ECH);
+ // Check the content returned from toString() for now because the getter function for
+ // this SvcParam hasn't been implemented.
+ // TODO(b/240259333): Consider adding DnsSvcbRecord.getEch() when needed.
+ assertTrue(record.toString().contains("ech=6142634465"));
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamIpv6Hint() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_IPV6HINT);
+ assertEquals(Arrays.asList(InetAddresses.parseNumericAddress("2001:db8::1")),
+ record.getAddresses());
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamDohPath() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_DOHPATH);
+ assertEquals("/some-path{?dns}", record.getDohPath());
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamGeneric_withValue() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(
+ TEST_SVC_PARAM_GENERIC_WITH_VALUE);
+ // Check the content returned from toString() for now because the getter function for
+ // generic SvcParam hasn't been implemented.
+ // TODO(b/240259333): Consider adding DnsSvcbRecord.getValueFromGenericSvcParam(int key)
+ // when needed.
+ assertTrue(record.toString().contains("key12345=1A2B0C"));
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamGeneric_withoutValue() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(
+ TEST_SVC_PARAM_GENERIC_WITHOUT_VALUE);
+ // Check the content returned from toString() for now because the getter function for
+ // generic SvcParam hasn't been implemented.
+ // TODO(b/240259333): Consider adding DnsSvcbRecord.getValueFromGenericSvcParam(int key)
+ // when needed.
+ assertTrue(record.toString().contains("key12346"));
+ }
+
+ @Test
+ public void testDnsSvcbRecord() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordFromByteArray(
+ new TestDnsRecordByteArrayBuilder()
+ .setRRType(TYPE_SVCB)
+ .setTargetName("doh.dns.com")
+ .addRdata(TEST_SVC_PARAM_ALPN_HTTPS)
+ .addRdata(TEST_SVC_PARAM_IPV4HINT_1)
+ .addRdata(TEST_SVC_PARAM_IPV6HINT)
+ .addRdata(TEST_SVC_PARAM_PORT)
+ .addRdata(TEST_SVC_PARAM_DOHPATH)
+ .build());
+ assertEquals("doh.dns.com", record.getTargetName());
+ assertEquals(Arrays.asList("h2", "http/1.1"), record.getAlpns());
+ assertEquals(5353, record.getPort());
+ assertEquals(Arrays.asList(
+ InetAddresses.parseNumericAddress("1.2.3.4"),
+ InetAddresses.parseNumericAddress("6.7.8.9"),
+ InetAddresses.parseNumericAddress("2001:db8::1")), record.getAddresses());
+ assertEquals("/some-path{?dns}", record.getDohPath());
+ }
+
+ @Test
+ public void testDnsSvcbRecord_createdFromNullObject() throws Exception {
+ assertThrows(NullPointerException.class, () -> makeDnsSvcbRecordFromByteArray(null));
+ }
+
+ @Test
+ public void testDnsSvcbRecord_invalidDnsRecord() throws Exception {
+ // The type is not SVCB.
+ final byte[] bytes1 = new TestDnsRecordByteArrayBuilder()
+ .setRRType(TYPE_A)
+ .addRdata(InetAddresses.parseNumericAddress("1.2.3.4").getAddress())
+ .build();
+ assertThrows(IllegalStateException.class, () -> makeDnsSvcbRecordFromByteArray(bytes1));
+
+ // TargetName is missing.
+ final byte[] bytes2 = new TestDnsRecordByteArrayBuilder()
+ .setRRType(TYPE_SVCB)
+ .addRdata(new byte[] { 0x01, 0x01 })
+ .build();
+ assertThrows(BufferUnderflowException.class, () -> makeDnsSvcbRecordFromByteArray(bytes2));
+
+ // Rdata is empty.
+ final byte[] bytes3 = new TestDnsRecordByteArrayBuilder()
+ .setRRType(TYPE_SVCB)
+ .build();
+ assertThrows(BufferUnderflowException.class, () -> makeDnsSvcbRecordFromByteArray(bytes3));
+ }
+
+ @Test
+ public void testDnsSvcbRecord_repeatedKeyIsInvalid() throws Exception {
+ final byte[] bytes = new TestDnsRecordByteArrayBuilder()
+ .setRRType(TYPE_SVCB)
+ .addRdata(TEST_SVC_PARAM_ALPN_HTTPS)
+ .addRdata(TEST_SVC_PARAM_ALPN_DOQ)
+ .build();
+ assertThrows(DnsPacket.ParseException.class, () -> makeDnsSvcbRecordFromByteArray(bytes));
+ }
+
+ @Test
+ public void testDnsSvcbRecord_invalidContent() throws Exception {
+ final List<byte[]> invalidContents = Arrays.asList(
+ // Invalid SvcParamValue for "mandatory":
+ // - SvcParamValue must not be empty.
+ // - SvcParamValue has less data than expected.
+ // - SvcParamValue has more data than expected.
+ // - SvcParamValue must be multiple of 2.
+ new byte[] { 0x00, 0x00, 0x00, 0x00},
+ new byte[] { 0x00, 0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x06 },
+ new byte[] { 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, },
+ new byte[] { 0x00, 0x00, 0x00, 0x03, 0x00, 0x04, 0x00 },
+
+ // Invalid SvcParamValue for "alpn":
+ // - SvcParamValue must not be empty.
+ // - SvcParamValue has less data than expected.
+ // - SvcParamValue has more data than expected.
+ // - Alpn length is less than the actual data size.
+ // - Alpn length is more than the actual data size.
+ // - Alpn must be a non-empty string.
+ new byte[] { 0x00, 0x01, 0x00, 0x00},
+ new byte[] { 0x00, 0x01, 0x00, 0x02, 0x02, 'h', '2' },
+ new byte[] { 0x00, 0x01, 0x00, 0x05, 0x02, 'h', '2' },
+ new byte[] { 0x00, 0x01, 0x00, 0x04, 0x02, 'd', 'o', 't' },
+ new byte[] { 0x00, 0x01, 0x00, 0x04, 0x08, 'd', 'o', 't' },
+ new byte[] { 0x00, 0x01, 0x00, 0x08, 0x02, 'h', '2', 0x00 },
+
+ // Invalid SvcParamValue for "no-default-alpn":
+ // - SvcParamValue must be empty.
+ // - SvcParamValue length must be 0.
+ new byte[] { 0x00, 0x02, 0x00, 0x04, 'd', 'a', 't', 'a' },
+ new byte[] { 0x00, 0x02, 0x00, 0x04 },
+
+ // Invalid SvcParamValue for "port":
+ // - SvcParamValue must not be empty.
+ // - SvcParamValue has less data than expected.
+ // - SvcParamValue has more data than expected.
+ // - SvcParamValue length must be multiple of 2.
+ new byte[] { 0x00, 0x03, 0x00, 0x00 },
+ new byte[] { 0x00, 0x03, 0x00, 0x02, 0x01 },
+ new byte[] { 0x00, 0x03, 0x00, 0x02, 0x01, 0x02, 0x03 },
+ new byte[] { 0x00, 0x03, 0x00, 0x03, 0x01, 0x02, 0x03 },
+
+ // Invalid SvcParamValue for "ipv4hint":
+ // - SvcParamValue must not be empty.
+ // - SvcParamValue has less data than expected.
+ // - SvcParamValue has more data than expected.
+ // - SvcParamValue must be multiple of 4.
+ new byte[] { 0x00, 0x04, 0x00, 0x00 },
+ new byte[] { 0x00, 0x04, 0x00, 0x04, 0x08 },
+ new byte[] { 0x00, 0x04, 0x00, 0x04, 0x08, 0x08, 0x08, 0x08, 0x08 },
+ new byte[] { 0x00, 0x04, 0x00, 0x05, 0x08, 0x08, 0x08, 0x08 },
+
+ // Invalid SvcParamValue for "ipv6hint":
+ // - SvcParamValue must not be empty.
+ // - SvcParamValue has less data than expected.
+ // - SvcParamValue has more data than expected.
+ // - SvcParamValue must be multiple of 16.
+ new byte[] { 0x00, 0x06, 0x00, 0x00 },
+ new byte[] { 0x00, 0x06, 0x00, 0x10, 0x01 },
+ new byte[] { 0x00, 0x06, 0x00, 0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17 },
+ new byte[] { 0x00, 0x06, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16 }
+ );
+
+ for (byte[] content : invalidContents) {
+ final byte[] bytes = new TestDnsRecordByteArrayBuilder()
+ .setRRType(TYPE_SVCB)
+ .addRdata(content)
+ .build();
+ assertThrows(DnsPacket.ParseException.class,
+ () -> makeDnsSvcbRecordFromByteArray(bytes));
+ }
+ }
+
+ @Test
+ public void testDnsSvcbPacket_createdFromNullObject() throws Exception {
+ assertThrows(DnsPacket.ParseException.class, () -> DnsSvcbPacket.fromResponse(null));
+ }
+
+ @Test
+ public void testDnsSvcbPacket() throws Exception {
+ final String dohTargetName = "https.dns.com";
+ final String doqTargetName = "doq.dns.com";
+ final InetAddress[] expectedIpAddressesForHttps = new InetAddress[] {
+ InetAddresses.parseNumericAddress("1.2.3.4"),
+ InetAddresses.parseNumericAddress("6.7.8.9"),
+ InetAddresses.parseNumericAddress("2001:db8::1"),
+ };
+ final InetAddress[] expectedIpAddressesForDoq = new InetAddress[] {
+ InetAddresses.parseNumericAddress("4.3.2.1"),
+ };
+
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ os.write(makeDnsResponseHeaderAsByteArray(1 /* qdcount */, 2 /* ancount */, 0 /* nscount */,
+ 0 /* arcount */));
+ os.write(TEST_DNS_SVCB_QUESTION_SECTION);
+ // Add answer for alpn h2 and http/1.1.
+ os.write(new TestDnsRecordByteArrayBuilder()
+ .setNameCompression(true)
+ .setRRType(TYPE_SVCB)
+ .setTargetName(dohTargetName)
+ .addRdata(TEST_SVC_PARAM_ALPN_HTTPS)
+ .addRdata(TEST_SVC_PARAM_IPV4HINT_1)
+ .addRdata(TEST_SVC_PARAM_IPV6HINT)
+ .addRdata(TEST_SVC_PARAM_PORT)
+ .addRdata(TEST_SVC_PARAM_DOHPATH)
+ .build());
+ // Add answer for alpn doq.
+ os.write(new TestDnsRecordByteArrayBuilder()
+ .setNameCompression(true)
+ .setRRType(TYPE_SVCB)
+ .setTargetName(doqTargetName)
+ .setSvcPriority(2)
+ .addRdata(TEST_SVC_PARAM_ALPN_DOQ)
+ .addRdata(TEST_SVC_PARAM_IPV4HINT_2)
+ .build());
+ final DnsSvcbPacket pkt = DnsSvcbPacket.fromResponse(os.toByteArray());
+
+ assertTrue(pkt.isSupported("http/1.1"));
+ assertTrue(pkt.isSupported("h2"));
+ assertTrue(pkt.isSupported("doq"));
+ assertFalse(pkt.isSupported("http"));
+ assertFalse(pkt.isSupported("h3"));
+ assertFalse(pkt.isSupported(""));
+
+ assertEquals(dohTargetName, pkt.getTargetName("http/1.1"));
+ assertEquals(dohTargetName, pkt.getTargetName("h2"));
+ assertEquals(doqTargetName, pkt.getTargetName("doq"));
+ assertEquals(null, pkt.getTargetName("http"));
+ assertEquals(null, pkt.getTargetName("h3"));
+ assertEquals(null, pkt.getTargetName(""));
+
+ assertEquals(5353, pkt.getPort("http/1.1"));
+ assertEquals(5353, pkt.getPort("h2"));
+ assertEquals(-1, pkt.getPort("doq"));
+ assertEquals(-1, pkt.getPort("http"));
+ assertEquals(-1, pkt.getPort("h3"));
+ assertEquals(-1, pkt.getPort(""));
+
+ assertArrayEquals(expectedIpAddressesForHttps, pkt.getAddresses("http/1.1").toArray());
+ assertArrayEquals(expectedIpAddressesForHttps, pkt.getAddresses("h2").toArray());
+ assertArrayEquals(expectedIpAddressesForDoq, pkt.getAddresses("doq").toArray());
+ assertTrue(pkt.getAddresses("http").isEmpty());
+ assertTrue(pkt.getAddresses("h3").isEmpty());
+ assertTrue(pkt.getAddresses("").isEmpty());
+
+ assertEquals("/some-path{?dns}", pkt.getDohPath("http/1.1"));
+ assertEquals("/some-path{?dns}", pkt.getDohPath("h2"));
+ assertEquals("", pkt.getDohPath("doq"));
+ assertEquals(null, pkt.getDohPath("http"));
+ assertEquals(null, pkt.getDohPath("h3"));
+ assertEquals(null, pkt.getDohPath(""));
+ }
+
+ @Test
+ public void testDnsSvcbPacket_noIpHint() throws Exception {
+ final String targetName = "doq.dns.com";
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ os.write(makeDnsResponseHeaderAsByteArray(1 /* qdcount */, 1 /* ancount */, 0 /* nscount */,
+ 0 /* arcount */));
+ os.write(TEST_DNS_SVCB_QUESTION_SECTION);
+ // Add answer for alpn doq.
+ os.write(new TestDnsRecordByteArrayBuilder()
+ .setNameCompression(true)
+ .setRRType(TYPE_SVCB)
+ .setTargetName(targetName)
+ .addRdata(TEST_SVC_PARAM_ALPN_DOQ)
+ .build());
+ final DnsSvcbPacket pkt = DnsSvcbPacket.fromResponse(os.toByteArray());
+
+ assertTrue(pkt.isSupported("doq"));
+ assertEquals(targetName, pkt.getTargetName("doq"));
+ assertEquals(-1, pkt.getPort("doq"));
+ assertArrayEquals(new InetAddress[] {}, pkt.getAddresses("doq").toArray());
+ assertEquals("", pkt.getDohPath("doq"));
+ }
+
+ @Test
+ public void testDnsSvcbPacket_hasAnswerInAdditionalSection() throws Exception {
+ final InetAddress[] expectedIpAddresses = new InetAddress[] {
+ InetAddresses.parseNumericAddress("1.2.3.4"),
+ InetAddresses.parseNumericAddress("2001:db8::2"),
+ };
+
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ os.write(makeDnsResponseHeaderAsByteArray(1 /* qdcount */, 1 /* ancount */, 0 /* nscount */,
+ 2 /* arcount */));
+ os.write(TEST_DNS_SVCB_QUESTION_SECTION);
+ // Add SVCB record in the Answer section.
+ os.write(new TestDnsRecordByteArrayBuilder()
+ .setNameCompression(true)
+ .setRRType(TYPE_SVCB)
+ .setTargetName("doq.dns.com")
+ .addRdata(TEST_SVC_PARAM_ALPN_DOQ)
+ .addRdata(TEST_SVC_PARAM_IPV4HINT_2)
+ .addRdata(TEST_SVC_PARAM_IPV6HINT)
+ .build());
+ // Add A/AAAA records in the Additional section.
+ os.write(new TestDnsRecordByteArrayBuilder()
+ .setNameCompression(true)
+ .setRRType(TYPE_A)
+ .addRdata(InetAddresses.parseNumericAddress("1.2.3.4").getAddress())
+ .build());
+ os.write(new TestDnsRecordByteArrayBuilder()
+ .setNameCompression(true)
+ .setRRType(TYPE_AAAA)
+ .addRdata(InetAddresses.parseNumericAddress("2001:db8::2").getAddress())
+ .build());
+ final DnsSvcbPacket pkt = DnsSvcbPacket.fromResponse(os.toByteArray());
+
+ // If there are A/AAAA records in the Additional section, getAddresses() returns the IP
+ // addresses in those records instead of the IP addresses in ipv4hint/ipv6hint.
+ assertArrayEquals(expectedIpAddresses, pkt.getAddresses("doq").toArray());
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
index 2d281fd..1ba83ca 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -19,6 +19,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import java.lang.IllegalStateException
import java.lang.reflect.Modifier
import org.junit.runner.Description
import org.junit.runner.Runner
@@ -27,6 +28,7 @@
import org.junit.runner.manipulation.NoTestsRemainException
import org.junit.runner.manipulation.Sortable
import org.junit.runner.manipulation.Sorter
+import org.junit.runner.notification.Failure
import org.junit.runner.notification.RunNotifier
import org.junit.runners.Parameterized
@@ -52,6 +54,9 @@
* class MyTestClass { ... }
*/
class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner(), Filterable, Sortable {
+ private val leakMonitorDesc = Description.createTestDescription(klass, "ThreadLeakMonitor")
+ private val shouldThreadLeakFailTest = klass.isAnnotationPresent(MonitorThreadLeak::class.java)
+
// Inference correctly infers Runner & Filterable & Sortable for |baseRunner|, but the
// Java bytecode doesn't have a way to express this. Give this type a name by wrapping it.
private class RunnerWrapper<T>(private val wrapped: T) :
@@ -61,6 +66,10 @@
override fun run(notifier: RunNotifier?) = wrapped.run(notifier)
}
+ // Annotation for test classes to indicate the test runner should monitor thread leak.
+ // TODO(b/307693729): Remove this annotation and monitor thread leak by default.
+ annotation class MonitorThreadLeak
+
private val baseRunner: RunnerWrapper<*>? = klass.let {
val ignoreAfter = it.getAnnotation(IgnoreAfter::class.java)
val ignoreUpTo = it.getAnnotation(IgnoreUpTo::class.java)
@@ -81,20 +90,52 @@
it.isAnnotationPresent(Parameterized.Parameters::class.java) }
override fun run(notifier: RunNotifier) {
- if (baseRunner != null) {
+ if (baseRunner == null) {
+ // Report a single, skipped placeholder test for this class, as the class is expected to
+ // report results when run. In practice runners that apply the Filterable implementation
+ // would see a NoTestsRemainException and not call the run method.
+ notifier.fireTestIgnored(
+ Description.createTestDescription(klass, "skippedClassForDevSdkMismatch"))
+ return
+ }
+ if (!shouldThreadLeakFailTest) {
baseRunner.run(notifier)
return
}
- // Report a single, skipped placeholder test for this class, as the class is expected to
- // report results when run. In practice runners that apply the Filterable implementation
- // would see a NoTestsRemainException and not call the run method.
- notifier.fireTestIgnored(
- Description.createTestDescription(klass, "skippedClassForDevSdkMismatch"))
+ // Dump threads as a baseline to monitor thread leaks.
+ val threadCountsBeforeTest = getAllThreadNameCounts()
+
+ baseRunner.run(notifier)
+
+ notifier.fireTestStarted(leakMonitorDesc)
+ val threadCountsAfterTest = getAllThreadNameCounts()
+ if (threadCountsBeforeTest != threadCountsAfterTest) {
+ notifier.fireTestFailure(Failure(leakMonitorDesc,
+ IllegalStateException("Expected threads: $threadCountsBeforeTest " +
+ "but got: $threadCountsAfterTest")))
+ }
+ notifier.fireTestFinished(leakMonitorDesc)
+ }
+
+ private fun getAllThreadNameCounts(): Map<String, Int> {
+ // Get the counts of threads in the group per name.
+ // Filter system thread groups.
+ return Thread.getAllStackTraces().keys
+ .filter { it.threadGroup?.name != "system" }
+ .groupingBy { it.name }.eachCount()
}
override fun getDescription(): Description {
- return baseRunner?.description ?: Description.createSuiteDescription(klass)
+ if (baseRunner == null) {
+ return Description.createSuiteDescription(klass)
+ }
+
+ return baseRunner.description.also {
+ if (shouldThreadLeakFailTest) {
+ it.addChild(leakMonitorDesc)
+ }
+ }
}
/**
@@ -102,7 +143,9 @@
*/
override fun testCount(): Int {
// When ignoring the tests, a skipped placeholder test is reported, so test count is 1.
- return baseRunner?.testCount() ?: 1
+ if (baseRunner == null) return 1
+
+ return baseRunner.testCount() + if (shouldThreadLeakFailTest) 1 else 0
}
@Throws(NoTestsRemainException::class)
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index bb32052..198b009 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -62,6 +62,8 @@
import android.util.Log;
import android.util.Pair;
+import androidx.annotation.Nullable;
+
import com.android.compatibility.common.util.AmUtils;
import com.android.compatibility.common.util.BatteryUtils;
import com.android.compatibility.common.util.DeviceConfigStateHelper;
@@ -283,8 +285,30 @@
}
protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
+ assertBackgroundNetworkAccess(expectAllowed, null);
+ }
+
+ /**
+ * Asserts whether the active network is available or not for the background app. If the network
+ * is unavailable, also checks whether it is blocked by the expected error.
+ *
+ * @param expectAllowed expect background network access to be allowed or not.
+ * @param expectedUnavailableError the expected error when {@code expectAllowed} is false. It's
+ * meaningful only when the {@code expectAllowed} is 'false'.
+ * Throws an IllegalArgumentException when {@code expectAllowed}
+ * is true and this parameter is not null. When the
+ * {@code expectAllowed} is 'false' and this parameter is null,
+ * this function does not compare error type of the networking
+ * access failure.
+ */
+ protected void assertBackgroundNetworkAccess(boolean expectAllowed,
+ @Nullable final String expectedUnavailableError) throws Exception {
assertBackgroundState();
- assertNetworkAccess(expectAllowed /* expectAvailable */, false /* needScreenOn */);
+ if (expectAllowed && expectedUnavailableError != null) {
+ throw new IllegalArgumentException("expectedUnavailableError is not null");
+ }
+ assertNetworkAccess(expectAllowed /* expectAvailable */, false /* needScreenOn */,
+ expectedUnavailableError);
}
protected void assertForegroundNetworkAccess() throws Exception {
@@ -407,12 +431,17 @@
*/
private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn)
throws Exception {
+ assertNetworkAccess(expectAvailable, needScreenOn, null);
+ }
+
+ private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn,
+ @Nullable final String expectedUnavailableError) throws Exception {
final int maxTries = 5;
String error = null;
int timeoutMs = 500;
for (int i = 1; i <= maxTries; i++) {
- error = checkNetworkAccess(expectAvailable);
+ error = checkNetworkAccess(expectAvailable, expectedUnavailableError);
if (error == null) return;
@@ -479,12 +508,15 @@
*
* @return error message with the mismatch (or empty if assertion passed).
*/
- private String checkNetworkAccess(boolean expectAvailable) throws Exception {
+ private String checkNetworkAccess(boolean expectAvailable,
+ @Nullable final String expectedUnavailableError) throws Exception {
final String resultData = mServiceClient.checkNetworkStatus();
- return checkForAvailabilityInResultData(resultData, expectAvailable);
+ return checkForAvailabilityInResultData(resultData, expectAvailable,
+ expectedUnavailableError);
}
- private String checkForAvailabilityInResultData(String resultData, boolean expectAvailable) {
+ private String checkForAvailabilityInResultData(String resultData, boolean expectAvailable,
+ @Nullable final String expectedUnavailableError) {
if (resultData == null) {
assertNotNull("Network status from app2 is null", resultData);
}
@@ -516,6 +548,10 @@
if (expectedState != state || expectedDetailedState != detailedState) {
errors.append(String.format("Connection state mismatch: expected %s/%s, got %s/%s\n",
expectedState, expectedDetailedState, state, detailedState));
+ } else if (!expectAvailable && (expectedUnavailableError != null)
+ && !connectionCheckDetails.contains(expectedUnavailableError)) {
+ errors.append("Connection unavailable reason mismatch: expected "
+ + expectedUnavailableError + "\n");
}
if (errors.length() > 0) {
@@ -914,7 +950,7 @@
final String resultData = result.get(0).second;
if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
final String error = checkForAvailabilityInResultData(
- resultData, expectAvailable);
+ resultData, expectAvailable, null /* expectedUnavailableError */);
if (error != null) {
fail("Network is not available for activity in app2 (" + mUid + "): "
+ error);
@@ -949,7 +985,7 @@
final String resultData = result.get(0).second;
if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
final String error = checkForAvailabilityInResultData(
- resultData, expectAvailable);
+ resultData, expectAvailable, null /* expectedUnavailableError */);
if (error != null) {
Log.d(TAG, "Network state is unexpected, checking again. " + error);
// Right now we could end up in an unexpected state if expedited job
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 ab3cf14..82f4a65 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
@@ -32,8 +32,11 @@
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
+import android.net.cts.util.CtsNetUtils;
import android.util.Log;
+import com.android.modules.utils.build.SdkLevel;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -46,6 +49,9 @@
public class NetworkCallbackTest extends AbstractRestrictBackgroundNetworkTestCase {
private Network mNetwork;
private final TestNetworkCallback mTestNetworkCallback = new TestNetworkCallback();
+ private CtsNetUtils mCtsNetUtils;
+ private static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google";
+
@Rule
public final MeterednessConfigurationRule mMeterednessConfiguration
= new MeterednessConfigurationRule();
@@ -218,6 +224,26 @@
mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
false /* hasCapability */, NET_CAPABILITY_NOT_METERED);
mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+
+ // Before Android T, DNS queries over private DNS should be but are not restricted by Power
+ // Saver or Data Saver. The issue is fixed in mainline update and apps can no longer request
+ // DNS queries when its network is restricted by Power Saver. The fix takes effect backwards
+ // starting from Android T. But for Data Saver, the fix is not backward compatible since
+ // there are some platform changes involved. It is only available on devices that a specific
+ // trunk flag is enabled.
+ //
+ // This test can not only verify that the network traffic from apps is blocked at the right
+ // time, but also verify whether it is correctly blocked at the DNS stage, or at a later
+ // socket connection stage.
+ if (SdkLevel.isAtLeastT()) {
+ // Enable private DNS
+ mCtsNetUtils = new CtsNetUtils(mContext);
+ mCtsNetUtils.storePrivateDnsSetting();
+ mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
+ mCtsNetUtils.awaitPrivateDnsSetting(
+ "NetworkCallbackTest wait private DNS setting timeout", mNetwork,
+ GOOGLE_PRIVATE_DNS_SERVER, true);
+ }
}
@After
@@ -227,6 +253,10 @@
setRestrictBackground(false);
setBatterySaverMode(false);
unregisterNetworkCallback();
+
+ if (SdkLevel.isAtLeastT() && (mCtsNetUtils != null)) {
+ mCtsNetUtils.restorePrivateDnsSetting();
+ }
}
@RequiredProperties({DATA_SAVER_MODE})
@@ -235,6 +265,8 @@
try {
// Enable restrict background
setRestrictBackground(true);
+ // TODO: Verify expectedUnavailableError when aconfig support mainline.
+ // (see go/aconfig-in-mainline-problems)
assertBackgroundNetworkAccess(false);
assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
@@ -247,6 +279,7 @@
// Remove from whitelist
removeRestrictBackgroundWhitelist(mUid);
+ // TODO: Verify expectedUnavailableError when aconfig support mainline.
assertBackgroundNetworkAccess(false);
assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
@@ -278,7 +311,11 @@
try {
// Enable Power Saver
setBatterySaverMode(true);
- assertBackgroundNetworkAccess(false);
+ if (SdkLevel.isAtLeastT()) {
+ assertBackgroundNetworkAccess(false, "java.net.UnknownHostException");
+ } else {
+ assertBackgroundNetworkAccess(false);
+ }
mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
@@ -298,7 +335,11 @@
try {
// Enable Power Saver
setBatterySaverMode(true);
- assertBackgroundNetworkAccess(false);
+ if (SdkLevel.isAtLeastT()) {
+ assertBackgroundNetworkAccess(false, "java.net.UnknownHostException");
+ } else {
+ assertBackgroundNetworkAccess(false);
+ }
mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
diff --git a/tests/native/utilities/firewall.cpp b/tests/native/utilities/firewall.cpp
index 22f83e8..669b76a 100644
--- a/tests/native/utilities/firewall.cpp
+++ b/tests/native/utilities/firewall.cpp
@@ -59,9 +59,11 @@
Result<void> Firewall::addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif) {
// iif should be non-zero if and only if match == MATCH_IIF
if (match == IIF_MATCH && iif == 0) {
- return Errorf("Interface match {} must have nonzero interface index", match);
+ return Errorf("Interface match {} must have nonzero interface index",
+ static_cast<int>(match));
} else if (match != IIF_MATCH && iif != 0) {
- return Errorf("Non-interface match {} must have zero interface index", match);
+ return Errorf("Non-interface match {} must have zero interface index",
+ static_cast<int>(match));
}
std::lock_guard guard(mMutex);
diff --git a/tests/unit/java/android/net/BpfNetMapsReaderTest.kt b/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
index 258e422..9de7f4d 100644
--- a/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
+++ b/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
@@ -16,6 +16,9 @@
package android.net
+import android.net.BpfNetMapsConstants.DATA_SAVER_DISABLED
+import android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED
+import android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY
import android.net.BpfNetMapsConstants.DOZABLE_MATCH
import android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH
import android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH
@@ -26,6 +29,8 @@
import com.android.net.module.util.IBpfMap
import com.android.net.module.util.Struct.S32
import com.android.net.module.util.Struct.U32
+import com.android.net.module.util.Struct.U8
+import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.TestBpfMap
@@ -33,6 +38,7 @@
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -45,17 +51,24 @@
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(VERSION_CODES.S_V2)
class BpfNetMapsReaderTest {
+ @Rule
+ @JvmField
+ val ignoreRule = DevSdkIgnoreRule()
+
private val testConfigurationMap: IBpfMap<S32, U32> = TestBpfMap()
private val testUidOwnerMap: IBpfMap<S32, UidOwnerValue> = TestBpfMap()
+ private val testDataSaverEnabledMap: IBpfMap<S32, U8> = TestBpfMap()
private val bpfNetMapsReader = BpfNetMapsReader(
- TestDependencies(testConfigurationMap, testUidOwnerMap))
+ TestDependencies(testConfigurationMap, testUidOwnerMap, testDataSaverEnabledMap))
class TestDependencies(
private val configMap: IBpfMap<S32, U32>,
- private val uidOwnerMap: IBpfMap<S32, UidOwnerValue>
+ private val uidOwnerMap: IBpfMap<S32, UidOwnerValue>,
+ private val dataSaverEnabledMap: IBpfMap<S32, U8>
) : BpfNetMapsReader.Dependencies() {
override fun getConfigurationMap() = configMap
override fun getUidOwnerMap() = uidOwnerMap
+ override fun getDataSaverEnabledMap() = dataSaverEnabledMap
}
private fun doTestIsChainEnabled(chain: Int) {
@@ -199,4 +212,13 @@
assertFalse(isUidNetworkingBlocked(TEST_UID2))
assertFalse(isUidNetworkingBlocked(TEST_UID3))
}
+
+ @IgnoreUpTo(VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testGetDataSaverEnabled() {
+ testDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, U8(DATA_SAVER_DISABLED))
+ assertFalse(bpfNetMapsReader.dataSaverEnabled)
+ testDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, U8(DATA_SAVER_ENABLED))
+ assertTrue(bpfNetMapsReader.dataSaverEnabled)
+ }
}
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index b8c5447..0082af2 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -90,6 +90,7 @@
import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -102,6 +103,8 @@
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(VERSION_CODES.R)
public class ConnectivityManagerTest {
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
private static final int TIMEOUT_MS = 30_000;
private static final int SHORT_TIMEOUT_MS = 150;
@@ -524,6 +527,7 @@
+ " attempts", ref.get());
}
+ @DevSdkIgnoreRule.IgnoreAfter(VERSION_CODES.UPSIDE_DOWN_CAKE)
@Test
public void testDataSaverStatusTracker() {
mockService(NetworkPolicyManager.class, Context.NETWORK_POLICY_SERVICE, mNpm);
diff --git a/tests/unit/java/android/net/MulticastRoutingConfigTest.kt b/tests/unit/java/android/net/MulticastRoutingConfigTest.kt
new file mode 100644
index 0000000..f01057b
--- /dev/null
+++ b/tests/unit/java/android/net/MulticastRoutingConfigTest.kt
@@ -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 android.net
+
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.Inet6Address
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+import android.net.MulticastRoutingConfig.Builder
+import android.net.MulticastRoutingConfig.FORWARD_NONE
+import android.net.MulticastRoutingConfig.FORWARD_SELECTED
+import android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class MulticastRoutingConfigTest {
+
+ val address1 = Inet6Address.getByName("2000::8888") as Inet6Address
+ val address2 = Inet6Address.getByName("2000::9999") as Inet6Address
+
+ private fun configNone() = Builder(FORWARD_NONE).build()
+ private fun configMinScope(scope: Int) = Builder(FORWARD_WITH_MIN_SCOPE, scope).build()
+ private fun configSelected() = Builder(FORWARD_SELECTED).build()
+ private fun configSelectedWithAddress1AndAddress2() =
+ Builder(FORWARD_SELECTED).addListeningAddress(address1)
+ .addListeningAddress(address2).build()
+ private fun configSelectedWithAddress2AndAddress1() =
+ Builder(FORWARD_SELECTED).addListeningAddress(address2)
+ .addListeningAddress(address1).build()
+
+ @Test
+ fun equalityTests() {
+
+ assertTrue(configNone().equals(configNone()))
+
+ assertTrue(configSelected().equals(configSelected()))
+
+ assertTrue(configMinScope(4).equals(configMinScope(4)))
+
+ assertTrue(configSelectedWithAddress1AndAddress2()
+ .equals(configSelectedWithAddress2AndAddress1()))
+ }
+
+ @Test
+ fun inequalityTests() {
+
+ assertFalse(configNone().equals(configSelected()))
+
+ assertFalse(configNone().equals(configMinScope(4)))
+
+ assertFalse(configSelected().equals(configMinScope(4)))
+
+ assertFalse(configMinScope(4).equals(configMinScope(5)))
+
+ assertFalse(configSelected().equals(configSelectedWithAddress1AndAddress2()))
+ }
+
+ @Test
+ fun toString_equalObjects_returnsEqualStrings() {
+ val config1 = configSelectedWithAddress1AndAddress2()
+ val config2 = configSelectedWithAddress2AndAddress1()
+
+ val str1 = config1.toString()
+ val str2 = config2.toString()
+
+ assertTrue(str1.equals(str2))
+ }
+
+ @Test
+ fun toString_unequalObjects_returnsUnequalStrings() {
+ val config1 = configSelected()
+ val config2 = configSelectedWithAddress1AndAddress2()
+
+ val str1 = config1.toString()
+ val str2 = config2.toString()
+
+ assertFalse(str1.equals(str2))
+ }
+}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index bafd450..b8cf08e 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -31,6 +31,7 @@
import static android.Manifest.permission.NETWORK_STACK;
import static android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.STATUS_BAR_SERVICE;
import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN;
import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
@@ -154,9 +155,12 @@
import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS;
import static android.os.Process.INVALID_UID;
import static android.system.OsConstants.IPPROTO_TCP;
+import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH;
+import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
import static com.android.server.ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION;
import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+import static com.android.server.ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS;
import static com.android.server.ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION;
import static com.android.server.ConnectivityService.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID;
import static com.android.server.ConnectivityService.PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED;
@@ -638,8 +642,8 @@
// BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the
// underlying binder calls.
- final BatteryStatsManager mBatteryStatsManager =
- new BatteryStatsManager(mock(IBatteryStats.class));
+ final IBatteryStats mIBatteryStats = mock(IBatteryStats.class);
+ final BatteryStatsManager mBatteryStatsManager = new BatteryStatsManager(mIBatteryStats);
private ArgumentCaptor<ResolverParamsParcel> mResolverParamsParcelCaptor =
ArgumentCaptor.forClass(ResolverParamsParcel.class);
@@ -2150,6 +2154,16 @@
}
}
+ @Override
+ public boolean isFeatureNotChickenedOut(Context context, String name) {
+ switch (name) {
+ case ALLOW_SYSUI_CONNECTIVITY_REPORTS:
+ return true;
+ default:
+ return super.isFeatureNotChickenedOut(context, name);
+ }
+ }
+
public void setChangeIdEnabled(final boolean enabled, final long changeId, final int uid) {
final Pair<Long, Integer> data = new Pair<>(changeId, uid);
// mEnabledChangeIds is read on the handler thread and maybe the test thread, so
@@ -10795,6 +10809,11 @@
expectNativeNetworkCreated(netId, permission, iface, null /* inOrder */);
}
+ private int getIdleTimerLabel(int netId, int transportType) {
+ return ConnectivityService.LegacyNetworkActivityTracker.getIdleTimerLabel(
+ mDeps.isAtLeastV(), netId, transportType);
+ }
+
@Test
public void testStackedLinkProperties() throws Exception {
final LinkAddress myIpv4 = new LinkAddress("1.2.3.4/24");
@@ -11036,7 +11055,7 @@
networkCallback.expect(LOST, mCellAgent);
networkCallback.assertNoCallback();
verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ eq(Integer.toString(getIdleTimerLabel(cellNetId, TRANSPORT_CELLULAR))));
verify(mMockNetd).networkDestroy(cellNetId);
if (mDeps.isAtLeastU()) {
verify(mMockNetd).setNetworkAllowlist(any());
@@ -11095,7 +11114,7 @@
}
verify(mMockNetd).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ eq(Integer.toString(getIdleTimerLabel(cellNetId, TRANSPORT_CELLULAR))));
verify(mMockNetd).networkDestroy(cellNetId);
if (mDeps.isAtLeastU()) {
verify(mMockNetd).setNetworkAllowlist(any());
@@ -11350,8 +11369,21 @@
final ConditionVariable onNetworkActiveCv = new ConditionVariable();
final ConnectivityManager.OnNetworkActiveListener listener = onNetworkActiveCv::open;
+ TestNetworkCallback defaultCallback = new TestNetworkCallback();
+
testAndCleanup(() -> {
+ mCm.registerDefaultNetworkCallback(defaultCallback);
agent.connect(true);
+ defaultCallback.expectAvailableThenValidatedCallbacks(agent);
+ if (transportType == TRANSPORT_CELLULAR) {
+ verify(mIBatteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+ } else if (transportType == TRANSPORT_WIFI) {
+ verify(mIBatteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+ }
+ clearInvocations(mIBatteryStats);
+ final int idleTimerLabel = getIdleTimerLabel(agent.getNetwork().netId, transportType);
// Network is considered active when the network becomes the default network.
assertTrue(mCm.isDefaultNetworkActive());
@@ -11360,19 +11392,57 @@
// Interface goes to inactive state
netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
- transportType, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+ idleTimerLabel, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
mServiceContext.expectDataActivityBroadcast(legacyType, false /* isActive */,
TIMESTAMP);
assertFalse(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
assertFalse(mCm.isDefaultNetworkActive());
+ if (mDeps.isAtLeastV()) {
+ if (transportType == TRANSPORT_CELLULAR) {
+ verify(mIBatteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_LOW),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+ } else if (transportType == TRANSPORT_WIFI) {
+ verify(mIBatteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_LOW),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+ }
+ } else {
+ // If TrackMultiNetworks is disabled, LegacyNetworkActivityTracker does not call
+ // BatteryStats API by the netd activity change callback since BatteryStatsService
+ // listen to netd callback via NetworkManagementService and update battery stats by
+ // itself.
+ verify(mIBatteryStats, never())
+ .noteMobileRadioPowerState(anyInt(), anyLong(), anyInt());
+ verify(mIBatteryStats, never())
+ .noteWifiRadioPowerState(anyInt(), anyLong(), anyInt());
+ }
// Interface goes to active state
netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
- transportType, TIMESTAMP, TEST_PACKAGE_UID);
+ idleTimerLabel, TIMESTAMP, TEST_PACKAGE_UID);
mServiceContext.expectDataActivityBroadcast(legacyType, true /* isActive */, TIMESTAMP);
assertTrue(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
assertTrue(mCm.isDefaultNetworkActive());
+ if (mDeps.isAtLeastV()) {
+ if (transportType == TRANSPORT_CELLULAR) {
+ verify(mIBatteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(TEST_PACKAGE_UID));
+ } else if (transportType == TRANSPORT_WIFI) {
+ verify(mIBatteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(TEST_PACKAGE_UID));
+ }
+ } else {
+ // If TrackMultiNetworks is disabled, LegacyNetworkActivityTracker does not call
+ // BatteryStats API by the netd activity change callback since BatteryStatsService
+ // listen to netd callback via NetworkManagementService and update battery stats by
+ // itself.
+ verify(mIBatteryStats, never())
+ .noteMobileRadioPowerState(anyInt(), anyLong(), anyInt());
+ verify(mIBatteryStats, never())
+ .noteWifiRadioPowerState(anyInt(), anyLong(), anyInt());
+ }
}, () -> { // Cleanup
+ mCm.unregisterNetworkCallback(defaultCallback);
+ }, () -> { // Cleanup
mCm.removeDefaultNetworkActiveListener(listener);
}, () -> { // Cleanup
agent.disconnect();
@@ -11420,12 +11490,13 @@
}
@Test
- public void testOnNetworkActive_NewEthernetConnects_CallbackNotCalled() throws Exception {
- // LegacyNetworkActivityTracker calls onNetworkActive callback only for networks that
- // tracker adds the idle timer to. And the tracker does not set the idle timer for the
- // ethernet network.
+ public void testOnNetworkActive_NewEthernetConnects_Callback() throws Exception {
+ // On pre-V devices, LegacyNetworkActivityTracker calls onNetworkActive callback only for
+ // networks that tracker adds the idle timer to. And the tracker does not set the idle timer
+ // for the ethernet network.
// So onNetworkActive is not called when the ethernet becomes the default network
- doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, false /* expectCallback */);
+ final boolean expectCallback = mDeps.isAtLeastV();
+ doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, expectCallback);
}
@Test
@@ -11455,15 +11526,19 @@
mCm.registerNetworkCallback(networkRequest, networkCallback);
mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+ final String cellIdleTimerLabel = Integer.toString(getIdleTimerLabel(
+ mCellAgent.getNetwork().netId, TRANSPORT_CELLULAR));
final LinkProperties cellLp = new LinkProperties();
cellLp.setInterfaceName(MOBILE_IFNAME);
mCellAgent.sendLinkProperties(cellLp);
mCellAgent.connect(true);
networkCallback.expectAvailableThenValidatedCallbacks(mCellAgent);
verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ eq(cellIdleTimerLabel));
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+ String wifiIdleTimerLabel = Integer.toString(getIdleTimerLabel(
+ mWiFiAgent.getNetwork().netId, TRANSPORT_WIFI));
final LinkProperties wifiLp = new LinkProperties();
wifiLp.setInterfaceName(WIFI_IFNAME);
mWiFiAgent.sendLinkProperties(wifiLp);
@@ -11474,9 +11549,18 @@
networkCallback.expectLosing(mCellAgent);
networkCallback.expectCaps(mWiFiAgent, c -> c.hasCapability(NET_CAPABILITY_VALIDATED));
verify(mMockNetd, times(1)).idletimerAddInterface(eq(WIFI_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_WIFI)));
- verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ eq(wifiIdleTimerLabel));
+ if (mDeps.isAtLeastV()) {
+ // V+ devices add idleTimer when the network is first connected and remove when the
+ // network is disconnected.
+ verify(mMockNetd, never()).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(mCellAgent.getNetwork().netId)));
+ } else {
+ // pre V devices add idleTimer when the network becomes the default network and remove
+ // when the network becomes no longer the default network.
+ verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(TRANSPORT_CELLULAR)));
+ }
// Disconnect wifi and switch back to cell
reset(mMockNetd);
@@ -11484,13 +11568,20 @@
networkCallback.expect(LOST, mWiFiAgent);
assertNoCallbacks(networkCallback);
verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(WIFI_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_WIFI)));
- verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ eq(wifiIdleTimerLabel));
+ if (mDeps.isAtLeastV()) {
+ verify(mMockNetd, never()).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(mCellAgent.getNetwork().netId)));
+ } else {
+ verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(TRANSPORT_CELLULAR)));
+ }
// reconnect wifi
reset(mMockNetd);
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+ wifiIdleTimerLabel = Integer.toString(getIdleTimerLabel(
+ mWiFiAgent.getNetwork().netId, TRANSPORT_WIFI));
wifiLp.setInterfaceName(WIFI_IFNAME);
mWiFiAgent.sendLinkProperties(wifiLp);
mWiFiAgent.connect(true);
@@ -11498,20 +11589,30 @@
networkCallback.expectLosing(mCellAgent);
networkCallback.expectCaps(mWiFiAgent, c -> c.hasCapability(NET_CAPABILITY_VALIDATED));
verify(mMockNetd, times(1)).idletimerAddInterface(eq(WIFI_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_WIFI)));
- verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ eq(wifiIdleTimerLabel));
+ if (mDeps.isAtLeastV()) {
+ verify(mMockNetd, never()).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(mCellAgent.getNetwork().netId)));
+ } else {
+ verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(TRANSPORT_CELLULAR)));
+ }
// Disconnect cell
reset(mMockNetd);
mCellAgent.disconnect();
networkCallback.expect(LOST, mCellAgent);
- // LOST callback is triggered earlier than removing idle timer. Broadcast should also be
- // sent as network being switched. Ensure rule removal for cell will not be triggered
- // unexpectedly before network being removed.
waitForIdle();
- verify(mMockNetd, times(0)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ if (mDeps.isAtLeastV()) {
+ verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(mCellAgent.getNetwork().netId)));
+ } else {
+ // LOST callback is triggered earlier than removing idle timer. Broadcast should also be
+ // sent as network being switched. Ensure rule removal for cell will not be triggered
+ // unexpectedly before network being removed.
+ verify(mMockNetd, times(0)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(TRANSPORT_CELLULAR)));
+ }
verify(mMockNetd, times(1)).networkDestroy(eq(mCellAgent.getNetwork().netId));
verify(mMockDnsResolver, times(1)).destroyNetworkCache(eq(mCellAgent.getNetwork().netId));
@@ -11520,12 +11621,27 @@
mWiFiAgent.disconnect();
b.expectBroadcast();
verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(WIFI_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_WIFI)));
+ eq(wifiIdleTimerLabel));
// Clean up
mCm.unregisterNetworkCallback(networkCallback);
}
+ @Test
+ public void testDataActivityTracking_VpnNetwork() throws Exception {
+ mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+ mWiFiAgent.connect(true /* validated */);
+ mMockVpn.setUnderlyingNetworks(new Network[] { mWiFiAgent.getNetwork() });
+
+ final LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(VPN_IFNAME);
+ mMockVpn.establishForMyUid(lp);
+
+ // NetworkActivityTracker should not track the VPN network since VPN can change the
+ // underlying network without disconnect.
+ verify(mMockNetd, never()).idletimerAddInterface(eq(VPN_IFNAME), anyInt(), any());
+ }
+
private void verifyTcpBufferSizeChange(String tcpBufferSizes) throws Exception {
String[] values = tcpBufferSizes.split(",");
String rmemValues = String.join(" ", values[0], values[1], values[2]);
@@ -12810,6 +12926,18 @@
}
@Test
+ public void testCheckConnectivityDiagnosticsPermissionsSysUi() throws Exception {
+ final NetworkAgentInfo naiWithoutUid = fakeMobileNai(new NetworkCapabilities());
+
+ mServiceContext.setPermission(STATUS_BAR_SERVICE, PERMISSION_GRANTED);
+ assertTrue(
+ "SysUi permission (STATUS_BAR_SERVICE) not applied",
+ mService.checkConnectivityDiagnosticsPermissions(
+ Process.myPid(), Process.myUid(), naiWithoutUid,
+ mContext.getOpPackageName()));
+ }
+
+ @Test
public void testCheckConnectivityDiagnosticsPermissionsWrongUidPackageName() throws Exception {
final int wrongUid = Process.myUid() + 1;
@@ -18704,6 +18832,7 @@
final LinkProperties lp = new LinkProperties();
lp.setInterfaceName(transportToTestIfaceName(transportType));
final TestNetworkAgentWrapper agent = new TestNetworkAgentWrapper(transportType, lp);
+ final int idleTimerLabel = getIdleTimerLabel(agent.getNetwork().netId, transportType);
testAndCleanup(() -> {
final UidFrozenStateChangedCallback uidFrozenStateChangedCallback =
getUidFrozenStateChangedCallback().get();
@@ -18716,7 +18845,7 @@
if (freezeWithNetworkInactive) {
// Make network inactive
netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
- transportType, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+ idleTimerLabel, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
}
// Freeze TEST_FROZEN_UID and TEST_UNFROZEN_UID
@@ -18740,7 +18869,7 @@
// Make network active
netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
- transportType, TIMESTAMP, TEST_PACKAGE_UID);
+ idleTimerLabel, TIMESTAMP, TEST_PACKAGE_UID);
waitForIdle();
if (expectDelay) {
@@ -18759,8 +18888,8 @@
@Test
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
public void testDelayFrozenUidSocketDestroy_ActiveCellular() throws Exception {
- doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR,
- false /* freezeWithNetworkInactive */, false /* expectDelay */);
+ doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR, false /* freezeWithNetworkInactive */,
+ false /* expectDelay */);
}
@Test
@@ -18768,22 +18897,22 @@
public void testDelayFrozenUidSocketDestroy_InactiveCellular() throws Exception {
// When the default network is cellular and cellular network is inactive, closing socket
// is delayed.
- doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR,
- true /* freezeWithNetworkInactive */, true /* expectDelay */);
+ doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR, true /* freezeWithNetworkInactive */,
+ true /* expectDelay */);
}
@Test
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
public void testDelayFrozenUidSocketDestroy_ActiveWifi() throws Exception {
- doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI,
- false /* freezeWithNetworkInactive */, false /* expectDelay */);
+ doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI, false /* freezeWithNetworkInactive */,
+ false /* expectDelay */);
}
@Test
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
public void testDelayFrozenUidSocketDestroy_InactiveWifi() throws Exception {
- doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI,
- true /* freezeWithNetworkInactive */, false /* expectDelay */);
+ doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI, true /* freezeWithNetworkInactive */,
+ false /* expectDelay */);
}
/**
@@ -18804,6 +18933,8 @@
final LinkProperties cellLp = new LinkProperties();
cellLp.setInterfaceName(MOBILE_IFNAME);
mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+ final int idleTimerLabel =
+ getIdleTimerLabel(mCellAgent.getNetwork().netId, TRANSPORT_CELLULAR);
final TestNetworkCallback defaultCallback = new TestNetworkCallback();
mCm.registerDefaultNetworkCallback(defaultCallback);
@@ -18813,7 +18944,7 @@
// Make cell network inactive
netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
- TRANSPORT_CELLULAR, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+ idleTimerLabel, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
// Freeze TEST_FROZEN_UID
final int[] uids = {TEST_FROZEN_UID};
diff --git a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
index 8e19c01..10a0982 100644
--- a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
@@ -72,7 +72,9 @@
import android.os.SystemClock;
import android.telephony.SubscriptionManager;
import android.test.suitebuilder.annotation.SmallTest;
+import android.util.ArraySet;
import android.util.Log;
+import android.util.Range;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -102,7 +104,9 @@
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Set;
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@@ -232,6 +236,9 @@
private static final byte[] TEST_RESPONSE_BYTES =
HexEncoding.decode(TEST_RESPONSE_HEX.toCharArray(), false);
+ private static final Set<Range<Integer>> TEST_UID_RANGES =
+ new ArraySet<>(Arrays.asList(new Range<>(10000, 99999)));
+
private static class TestKeepaliveInfo {
private static List<Socket> sOpenSockets = new ArrayList<>();
@@ -409,28 +416,28 @@
public void testIsAnyTcpSocketConnected_runOnNonHandlerThread() throws Exception {
setupResponseWithSocketExisting();
assertThrows(IllegalStateException.class,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID, TEST_UID_RANGES));
}
@Test
public void testIsAnyTcpSocketConnected_withTargetNetId() throws Exception {
setupResponseWithSocketExisting();
assertTrue(visibleOnHandlerThread(mTestHandler,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID, TEST_UID_RANGES)));
}
@Test
public void testIsAnyTcpSocketConnected_withIncorrectNetId() throws Exception {
setupResponseWithSocketExisting();
assertFalse(visibleOnHandlerThread(mTestHandler,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID)));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID, TEST_UID_RANGES)));
}
@Test
public void testIsAnyTcpSocketConnected_noSocketExists() throws Exception {
setupResponseWithoutSocketExisting();
assertFalse(visibleOnHandlerThread(mTestHandler,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID, TEST_UID_RANGES)));
}
private void triggerEventKeepalive(int slot, int reason) {
@@ -474,14 +481,16 @@
setupResponseWithoutSocketExisting();
visibleOnHandlerThread(
mTestHandler,
- () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(autoKi, TEST_NETID));
+ () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(
+ autoKi, TEST_NETID, TEST_UID_RANGES));
}
private void doResumeKeepalive(AutomaticOnOffKeepalive autoKi) throws Exception {
setupResponseWithSocketExisting();
visibleOnHandlerThread(
mTestHandler,
- () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(autoKi, TEST_NETID));
+ () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(
+ autoKi, TEST_NETID, TEST_UID_RANGES));
}
private void doStopKeepalive(AutomaticOnOffKeepalive autoKi) throws Exception {
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 48cfe77..ef88984 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -56,11 +56,9 @@
import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV4_UDP;
import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_ESP;
import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_UDP;
-import static com.android.testutils.Cleanup.testAndCleanup;
import static com.android.testutils.HandlerUtils.waitForIdleSerialExecutor;
import static com.android.testutils.MiscAsserts.assertThrows;
-import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -76,7 +74,9 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.longThat;
import static org.mockito.Mockito.after;
+import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doNothing;
@@ -149,7 +149,6 @@
import android.net.wifi.WifiInfo;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
-import android.os.ConditionVariable;
import android.os.INetworkManagementService;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
@@ -191,6 +190,7 @@
import org.mockito.AdditionalAnswers;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -207,6 +207,7 @@
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.List;
@@ -317,6 +318,8 @@
@Mock DeviceIdleInternal mDeviceIdleInternal;
private final VpnProfile mVpnProfile;
+ @Captor private ArgumentCaptor<Collection<Range<Integer>>> mUidRangesCaptor;
+
private IpSecManager mIpSecManager;
private TestDeps mTestDeps;
@@ -1096,37 +1099,53 @@
}
}
- private Vpn prepareVpnForVerifyAppExclusionList() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ private String startVpnForVerifyAppExclusionList(Vpn vpn) throws Exception {
when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
.thenReturn(mVpnProfile.encode());
when(mVpnProfileStore.get(PRIMARY_USER_APP_EXCLUDE_KEY))
.thenReturn(HexDump.hexStringToByteArray(PKGS_BYTES));
-
- vpn.startVpnProfile(TEST_VPN_PKG);
+ final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
+ final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
+ PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
+ verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
+ clearInvocations(mConnectivityManager);
verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
vpn.mNetworkAgent = mMockNetworkAgent;
+
+ return sessionKey;
+ }
+
+ private Vpn prepareVpnForVerifyAppExclusionList() throws Exception {
+ final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ startVpnForVerifyAppExclusionList(vpn);
+
return vpn;
}
@Test
public void testSetAndGetAppExclusionList() throws Exception {
- final Vpn vpn = prepareVpnForVerifyAppExclusionList();
+ final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ final String sessionKey = startVpnForVerifyAppExclusionList(vpn);
verify(mVpnProfileStore, never()).put(eq(PRIMARY_USER_APP_EXCLUDE_KEY), any());
vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
verify(mVpnProfileStore)
.put(eq(PRIMARY_USER_APP_EXCLUDE_KEY),
eq(HexDump.hexStringToByteArray(PKGS_BYTES)));
- assertEquals(vpn.createUserAndRestrictedProfilesRanges(
- PRIMARY_USER.id, null, Arrays.asList(PKGS)),
- vpn.mNetworkCapabilities.getUids());
+ final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
+ PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
+ verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
+ assertEquals(uidRanges, vpn.mNetworkCapabilities.getUids());
assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
}
@Test
public void testRefreshPlatformVpnAppExclusionList_updatesExcludedUids() throws Exception {
- final Vpn vpn = prepareVpnForVerifyAppExclusionList();
+ final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ final String sessionKey = startVpnForVerifyAppExclusionList(vpn);
vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
+ final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
+ PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
+ verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
verify(mMockNetworkAgent).doSendNetworkCapabilities(any());
assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
@@ -1135,33 +1154,36 @@
// Remove one of the package
List<Integer> newExcludedUids = toList(PKG_UIDS);
newExcludedUids.remove((Integer) PKG_UIDS[0]);
+ Set<Range<Integer>> newUidRanges = makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids);
sPackages.remove(PKGS[0]);
vpn.refreshPlatformVpnAppExclusionList();
// List in keystore is not changed, but UID for the removed packages is no longer exempted.
assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
- assertEquals(makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids),
- vpn.mNetworkCapabilities.getUids());
+ assertEquals(newUidRanges, vpn.mNetworkCapabilities.getUids());
ArgumentCaptor<NetworkCapabilities> ncCaptor =
ArgumentCaptor.forClass(NetworkCapabilities.class);
verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
- assertEquals(makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids),
- ncCaptor.getValue().getUids());
+ assertEquals(newUidRanges, ncCaptor.getValue().getUids());
+ verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(newUidRanges));
reset(mMockNetworkAgent);
// Add the package back
newExcludedUids.add(PKG_UIDS[0]);
+ newUidRanges = makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids);
sPackages.put(PKGS[0], PKG_UIDS[0]);
vpn.refreshPlatformVpnAppExclusionList();
// List in keystore is not changed and the uid list should be updated in the net cap.
assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
- assertEquals(makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids),
- vpn.mNetworkCapabilities.getUids());
+ assertEquals(newUidRanges, vpn.mNetworkCapabilities.getUids());
verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
- assertEquals(makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids),
- ncCaptor.getValue().getUids());
+ assertEquals(newUidRanges, ncCaptor.getValue().getUids());
+
+ // The uidRange is the same as the original setAppExclusionList so this is the second call
+ verify(mConnectivityManager, times(2))
+ .setVpnDefaultForUids(eq(sessionKey), eq(newUidRanges));
}
private List<Range<Integer>> makeVpnUidRange(int userId, List<Integer> excludedAppIdList) {
@@ -1787,6 +1809,9 @@
.getRedactedLinkPropertiesForPackage(any(), anyInt(), anyString());
final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
+ final Set<Range<Integer>> uidRanges = rangeSet(PRIMARY_USER_RANGE);
+ // This is triggered by Ikev2VpnRunner constructor.
+ verify(mConnectivityManager, times(1)).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
final NetworkCallback cb = triggerOnAvailableAndGetCallback();
verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
@@ -1795,6 +1820,8 @@
// state
verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
.createIkeSession(any(), any(), any(), any(), captor.capture(), any());
+ // This is triggered by Vpn#startOrMigrateIkeSession().
+ verify(mConnectivityManager, times(2)).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
reset(mIkev2SessionCreator);
// For network lost case, the process should be triggered by calling onLost(), which is the
// same process with the real case.
@@ -1814,16 +1841,43 @@
new String[] {TEST_VPN_PKG}, new VpnProfileState(VpnProfileState.STATE_CONNECTING,
sessionKey, false /* alwaysOn */, false /* lockdown */));
if (errorType == VpnManager.ERROR_CLASS_NOT_RECOVERABLE) {
+ verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey),
+ eq(Collections.EMPTY_LIST));
verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
.unregisterNetworkCallback(eq(cb));
} else if (errorType == VpnManager.ERROR_CLASS_RECOVERABLE
// Vpn won't retry when there is no usable underlying network.
&& errorCode != VpnManager.ERROR_CODE_NETWORK_LOST) {
int retryIndex = 0;
- final IkeSessionCallback ikeCb2 = verifyRetryAndGetNewIkeCb(retryIndex++);
+ // First failure occurred above.
+ final IkeSessionCallback retryCb = verifyRetryAndGetNewIkeCb(retryIndex++);
+ // Trigger 2 more failures to let the retry delay increase to 5s.
+ mExecutor.execute(() -> retryCb.onClosedWithException(exception));
+ final IkeSessionCallback retryCb2 = verifyRetryAndGetNewIkeCb(retryIndex++);
+ mExecutor.execute(() -> retryCb2.onClosedWithException(exception));
+ final IkeSessionCallback retryCb3 = verifyRetryAndGetNewIkeCb(retryIndex++);
- mExecutor.execute(() -> ikeCb2.onClosedWithException(exception));
+ // setVpnDefaultForUids may be called again but the uidRanges should not change.
+ verify(mConnectivityManager, atLeast(2)).setVpnDefaultForUids(eq(sessionKey),
+ mUidRangesCaptor.capture());
+ final List<Collection<Range<Integer>>> capturedUidRanges =
+ mUidRangesCaptor.getAllValues();
+ for (int i = 2; i < capturedUidRanges.size(); i++) {
+ // Assert equals no order.
+ assertTrue(
+ "uid ranges should not be modified. Expected: " + uidRanges
+ + ", actual: " + capturedUidRanges.get(i),
+ capturedUidRanges.get(i).containsAll(uidRanges)
+ && capturedUidRanges.get(i).size() == uidRanges.size());
+ }
+
+ // A fourth failure will cause the retry delay to be greater than 5s.
+ mExecutor.execute(() -> retryCb3.onClosedWithException(exception));
verifyRetryAndGetNewIkeCb(retryIndex++);
+
+ // The VPN network preference will be cleared when the retry delay is greater than 5s.
+ verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey),
+ eq(Collections.EMPTY_LIST));
}
}
@@ -1983,22 +2037,6 @@
// a subsequent CL.
}
- @Test
- public void testStartLegacyVpnIpv6() throws Exception {
- setMockedUsers(PRIMARY_USER);
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final LinkProperties lp = new LinkProperties();
- lp.setInterfaceName(EGRESS_IFACE);
- lp.addLinkAddress(new LinkAddress("2001:db8::1/64"));
- final RouteInfo defaultRoute = new RouteInfo(
- new IpPrefix(Inet6Address.ANY, 0), null, EGRESS_IFACE);
- lp.addRoute(defaultRoute);
-
- // IllegalStateException thrown since legacy VPN only supports IPv4.
- assertThrows(IllegalStateException.class,
- () -> vpn.startLegacyVpn(mVpnProfile, EGRESS_NETWORK, lp));
- }
-
private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
setMockedUsers(PRIMARY_USER);
@@ -2122,7 +2160,9 @@
when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
.thenReturn(vpnProfile.encode());
- vpn.startVpnProfile(TEST_VPN_PKG);
+ final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
+ final Set<Range<Integer>> uidRanges = Collections.singleton(PRIMARY_USER_RANGE);
+ verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
final NetworkCallback nwCb = triggerOnAvailableAndGetCallback(underlyingNetworkCaps);
// There are 4 interactions with the executor.
// - Network available
@@ -2215,6 +2255,7 @@
final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
+ verify(mConnectivityManager).setVpnDefaultForUids(anyString(), eq(Collections.EMPTY_LIST));
}
@Test
@@ -3112,23 +3153,15 @@
}
@Test
- public void testStartRacoonNumericAddress() throws Exception {
- startRacoon("1.2.3.4", "1.2.3.4");
- }
+ public void testStartLegacyVpnType() throws Exception {
+ setMockedUsers(PRIMARY_USER);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ final VpnProfile profile = new VpnProfile("testProfile" /* key */);
- @Test
- public void testStartRacoonHostname() throws Exception {
- startRacoon("hostname", "5.6.7.8"); // address returned by deps.resolve
- }
-
- @Test
- public void testStartPptp() throws Exception {
- startPptp(true /* useMppe */);
- }
-
- @Test
- public void testStartPptp_NoMppe() throws Exception {
- startPptp(false /* useMppe */);
+ profile.type = VpnProfile.TYPE_PPTP;
+ assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile));
+ profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK;
+ assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile));
}
private void assertTransportInfoMatches(NetworkCapabilities nc, int type) {
@@ -3138,125 +3171,6 @@
assertEquals(type, ti.getType());
}
- private void startPptp(boolean useMppe) throws Exception {
- final VpnProfile profile = new VpnProfile("testProfile" /* key */);
- profile.type = VpnProfile.TYPE_PPTP;
- profile.name = "testProfileName";
- profile.username = "userName";
- profile.password = "thePassword";
- profile.server = "192.0.2.123";
- profile.mppe = useMppe;
-
- doReturn(new Network[] { new Network(101) }).when(mConnectivityManager).getAllNetworks();
- doReturn(new Network(102)).when(mConnectivityManager).registerNetworkAgent(
- any(), // INetworkAgent
- any(), // NetworkInfo
- any(), // LinkProperties
- any(), // NetworkCapabilities
- any(), // LocalNetworkConfig
- any(), // NetworkScore
- any(), // NetworkAgentConfig
- anyInt()); // provider ID
-
- final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
- final TestDeps deps = (TestDeps) vpn.mDeps;
-
- testAndCleanup(() -> {
- final String[] mtpdArgs = deps.mtpdArgs.get(10, TimeUnit.SECONDS);
- final String[] argsPrefix = new String[]{
- EGRESS_IFACE, "pptp", profile.server, "1723", "name", profile.username,
- "password", profile.password, "linkname", "vpn", "refuse-eap", "nodefaultroute",
- "usepeerdns", "idle", "1800", "mtu", "1270", "mru", "1270"
- };
- assertArrayEquals(argsPrefix, Arrays.copyOf(mtpdArgs, argsPrefix.length));
- if (useMppe) {
- assertEquals(argsPrefix.length + 2, mtpdArgs.length);
- assertEquals("+mppe", mtpdArgs[argsPrefix.length]);
- assertEquals("-pap", mtpdArgs[argsPrefix.length + 1]);
- } else {
- assertEquals(argsPrefix.length + 1, mtpdArgs.length);
- assertEquals("nomppe", mtpdArgs[argsPrefix.length]);
- }
-
- verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(
- any(), // INetworkAgent
- any(), // NetworkInfo
- any(), // LinkProperties
- any(), // NetworkCapabilities
- any(), // LocalNetworkConfig
- any(), // NetworkScore
- any(), // NetworkAgentConfig
- anyInt()); // provider ID
- }, () -> { // Cleanup
- vpn.mVpnRunner.exitVpnRunner();
- deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier
- vpn.mVpnRunner.join(10_000); // wait for up to 10s for the runner to die and cleanup
- });
- }
-
- public void startRacoon(final String serverAddr, final String expectedAddr)
- throws Exception {
- final ConditionVariable legacyRunnerReady = new ConditionVariable();
- final VpnProfile profile = new VpnProfile("testProfile" /* key */);
- profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK;
- profile.name = "testProfileName";
- profile.username = "userName";
- profile.password = "thePassword";
- profile.server = serverAddr;
- profile.ipsecIdentifier = "id";
- profile.ipsecSecret = "secret";
- profile.l2tpSecret = "l2tpsecret";
-
- when(mConnectivityManager.getAllNetworks())
- .thenReturn(new Network[] { new Network(101) });
-
- when(mConnectivityManager.registerNetworkAgent(any(), any(), any(), any(),
- any(), any(), any(), anyInt())).thenAnswer(invocation -> {
- // The runner has registered an agent and is now ready.
- legacyRunnerReady.open();
- return new Network(102);
- });
- final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
- final TestDeps deps = (TestDeps) vpn.mDeps;
- try {
- // udppsk and 1701 are the values for TYPE_L2TP_IPSEC_PSK
- assertArrayEquals(
- new String[] { EGRESS_IFACE, expectedAddr, "udppsk",
- profile.ipsecIdentifier, profile.ipsecSecret, "1701" },
- deps.racoonArgs.get(10, TimeUnit.SECONDS));
- // literal values are hardcoded in Vpn.java for mtpd args
- assertArrayEquals(
- new String[] { EGRESS_IFACE, "l2tp", expectedAddr, "1701", profile.l2tpSecret,
- "name", profile.username, "password", profile.password,
- "linkname", "vpn", "refuse-eap", "nodefaultroute", "usepeerdns",
- "idle", "1800", "mtu", "1270", "mru", "1270" },
- deps.mtpdArgs.get(10, TimeUnit.SECONDS));
-
- // Now wait for the runner to be ready before testing for the route.
- ArgumentCaptor<LinkProperties> lpCaptor = ArgumentCaptor.forClass(LinkProperties.class);
- ArgumentCaptor<NetworkCapabilities> ncCaptor =
- ArgumentCaptor.forClass(NetworkCapabilities.class);
- verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(any(), any(),
- lpCaptor.capture(), ncCaptor.capture(), any(), any(), any(), anyInt());
-
- // In this test the expected address is always v4 so /32.
- // Note that the interface needs to be specified because RouteInfo objects stored in
- // LinkProperties objects always acquire the LinkProperties' interface.
- final RouteInfo expectedRoute = new RouteInfo(new IpPrefix(expectedAddr + "/32"),
- null, EGRESS_IFACE, RouteInfo.RTN_THROW);
- final List<RouteInfo> actualRoutes = lpCaptor.getValue().getRoutes();
- assertTrue("Expected throw route (" + expectedRoute + ") not found in " + actualRoutes,
- actualRoutes.contains(expectedRoute));
-
- assertTransportInfoMatches(ncCaptor.getValue(), VpnManager.TYPE_VPN_LEGACY);
- } finally {
- // Now interrupt the thread, unblock the runner and clean up.
- vpn.mVpnRunner.exitVpnRunner();
- deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier
- vpn.mVpnRunner.join(10_000); // wait for up to 10s for the runner to die and cleanup
- }
- }
-
// Make it public and un-final so as to spy it
public class TestDeps extends Vpn.Dependencies {
public final CompletableFuture<String[]> racoonArgs = new CompletableFuture();
@@ -3394,12 +3308,6 @@
}
@Override
- public long getNextRetryDelayMs(int retryCount) {
- // Simply return retryCount as the delay seconds for retrying.
- return retryCount * 1000;
- }
-
- @Override
public long getValidationFailRecoveryMs(int retryCount) {
// Simply return retryCount as the delay seconds for retrying.
return retryCount * 100L;
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
index 8917ed3..ad30ce0 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
@@ -82,8 +82,8 @@
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
mSocketKey = new SocketKey(1000 /* interfaceIndex */);
- mSocketClient = new MdnsMultinetworkSocketClient(
- mHandlerThread.getLooper(), mProvider, mSharedLog);
+ mSocketClient = new MdnsMultinetworkSocketClient(mHandlerThread.getLooper(), mProvider,
+ mSharedLog, MdnsFeatureFlags.newBuilder().build());
mHandler.post(() -> mSocketClient.setCallback(mCallback));
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
index 19d8a00..37588b5 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
@@ -75,7 +75,7 @@
+ "the packet length");
} catch (IOException e) {
// Expected
- } catch (Exception e) {
+ } catch (RuntimeException e) {
fail(String.format(
Locale.ROOT,
"Should not have thrown any other exception except " + "for IOException: %s",
@@ -83,4 +83,4 @@
}
assertEquals(data.length, packetReader.getRemaining());
}
-}
\ No newline at end of file
+}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
index 28ea4b6..0877b68 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
@@ -27,6 +27,9 @@
@RunWith(DevSdkIgnoreRunner::class)
class MdnsPacketTest {
+ private fun makeFlags(isLabelCountLimitEnabled: Boolean = false): MdnsFeatureFlags =
+ MdnsFeatureFlags.newBuilder()
+ .setIsLabelCountLimitEnabled(isLabelCountLimitEnabled).build()
@Test
fun testParseQuery() {
// Probe packet with 1 question for Android.local, and 4 additionalRecords with 4 addresses
@@ -38,7 +41,7 @@
"010db8000000000000000000000789"
val bytes = HexDump.hexStringToByteArray(packetHex)
- val reader = MdnsPacketReader(bytes, bytes.size)
+ val reader = MdnsPacketReader(bytes, bytes.size, makeFlags())
val packet = MdnsPacket.parse(reader)
assertEquals(123, packet.transactionId)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
index 3fc656a..a22e8c6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
@@ -17,8 +17,10 @@
package com.android.server.connectivity.mdns;
import static android.net.InetAddresses.parseNumericAddress;
+
import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -337,7 +339,8 @@
packet.setSocketAddress(
new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT));
- final MdnsPacket parsedPacket = MdnsResponseDecoder.parseResponse(data6, data6.length);
+ final MdnsPacket parsedPacket = MdnsResponseDecoder.parseResponse(
+ data6, data6.length, MdnsFeatureFlags.newBuilder().build());
assertNotNull(parsedPacket);
final Network network = mock(Network.class);
@@ -636,7 +639,8 @@
private ArraySet<MdnsResponse> decode(MdnsResponseDecoder decoder, byte[] data,
Collection<MdnsResponse> existingResponses) throws MdnsPacket.ParseException {
- final MdnsPacket parsedPacket = MdnsResponseDecoder.parseResponse(data, data.length);
+ final MdnsPacket parsedPacket = MdnsResponseDecoder.parseResponse(
+ data, data.length, MdnsFeatureFlags.newBuilder().build());
assertNotNull(parsedPacket);
return new ArraySet<>(decoder.augmentResponses(parsedPacket,
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
index 2b3b834..3cea5cb 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
@@ -19,7 +19,10 @@
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
+import com.android.net.module.util.ArrayTrackRecord
import com.android.server.connectivity.mdns.MdnsServiceCache.CacheKey
+import com.android.server.connectivity.mdns.MdnsServiceCacheTest.ExpiredRecord.ExpiredEvent.ServiceRecordExpired
+import com.android.server.connectivity.mdns.util.MdnsUtils
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import java.util.concurrent.CompletableFuture
@@ -32,13 +35,19 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
private const val SERVICE_NAME_1 = "service-instance-1"
private const val SERVICE_NAME_2 = "service-instance-2"
+private const val SERVICE_NAME_3 = "service-instance-3"
private const val SERVICE_TYPE_1 = "_test1._tcp.local"
private const val SERVICE_TYPE_2 = "_test2._tcp.local"
private const val INTERFACE_INDEX = 999
private const val DEFAULT_TIMEOUT_MS = 2000L
+private const val NO_CALLBACK_TIMEOUT_MS = 200L
+private const val TEST_ELAPSED_REALTIME_MS = 123L
+private const val DEFAULT_TTL_TIME_MS = 120000L
@RunWith(DevSdkIgnoreRunner::class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -47,10 +56,46 @@
private val cacheKey1 = CacheKey(SERVICE_TYPE_1, socketKey)
private val cacheKey2 = CacheKey(SERVICE_TYPE_2, socketKey)
private val thread = HandlerThread(MdnsServiceCacheTest::class.simpleName)
+ private val clock = mock(MdnsUtils.Clock::class.java)
private val handler by lazy {
Handler(thread.looper)
}
+ private class ExpiredRecord : MdnsServiceCache.ServiceExpiredCallback {
+ val history = ArrayTrackRecord<ExpiredEvent>().newReadHead()
+
+ sealed class ExpiredEvent {
+ abstract val previousResponse: MdnsResponse
+ abstract val newResponse: MdnsResponse?
+ data class ServiceRecordExpired(
+ override val previousResponse: MdnsResponse,
+ override val newResponse: MdnsResponse?
+ ) : ExpiredEvent()
+ }
+
+ override fun onServiceRecordExpired(
+ previousResponse: MdnsResponse,
+ newResponse: MdnsResponse?
+ ) {
+ history.add(ServiceRecordExpired(previousResponse, newResponse))
+ }
+
+ fun expectedServiceRecordExpired(
+ serviceName: String,
+ timeoutMs: Long = DEFAULT_TIMEOUT_MS
+ ) {
+ val event = history.poll(timeoutMs)
+ assertNotNull(event)
+ assertTrue(event is ServiceRecordExpired)
+ assertEquals(serviceName, event.previousResponse.serviceInstanceName)
+ }
+
+ fun assertNoCallback() {
+ val cb = history.poll(NO_CALLBACK_TIMEOUT_MS)
+ assertNull("Expected no callback but got $cb", cb)
+ }
+ }
+
@Before
fun setUp() {
thread.start()
@@ -89,19 +134,27 @@
private fun getService(
serviceCache: MdnsServiceCache,
serviceName: String,
- cacheKey: CacheKey,
+ cacheKey: CacheKey
): MdnsResponse? = runningOnHandlerAndReturn {
serviceCache.getCachedService(serviceName, cacheKey)
}
private fun getServices(
serviceCache: MdnsServiceCache,
- cacheKey: CacheKey,
+ cacheKey: CacheKey
): List<MdnsResponse> = runningOnHandlerAndReturn { serviceCache.getCachedServices(cacheKey) }
+ private fun registerServiceExpiredCallback(
+ serviceCache: MdnsServiceCache,
+ cacheKey: CacheKey,
+ callback: MdnsServiceCache.ServiceExpiredCallback
+ ) = runningOnHandlerAndReturn {
+ serviceCache.registerServiceExpiredCallback(cacheKey, callback)
+ }
+
@Test
fun testAddAndRemoveService() {
- val serviceCache = MdnsServiceCache(thread.looper, makeFlags())
+ val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
var response = getService(serviceCache, SERVICE_NAME_1, cacheKey1)
assertNotNull(response)
@@ -113,7 +166,7 @@
@Test
fun testGetCachedServices_multipleServiceTypes() {
- val serviceCache = MdnsServiceCache(thread.looper, makeFlags())
+ val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1))
addOrUpdateService(serviceCache, cacheKey2, createResponse(SERVICE_NAME_2, SERVICE_TYPE_2))
@@ -145,7 +198,127 @@
})
}
- private fun createResponse(serviceInstanceName: String, serviceType: String) = MdnsResponse(
- 0 /* now */, "$serviceInstanceName.$serviceType".split(".").toTypedArray(),
- socketKey.interfaceIndex, socketKey.network)
+ @Test
+ fun testServiceExpiredAndSendCallbacks() {
+ val serviceCache = MdnsServiceCache(
+ thread.looper, makeFlags(isExpiredServicesRemovalEnabled = true), clock)
+ // Register service expired callbacks
+ val callback1 = ExpiredRecord()
+ val callback2 = ExpiredRecord()
+ registerServiceExpiredCallback(serviceCache, cacheKey1, callback1)
+ registerServiceExpiredCallback(serviceCache, cacheKey2, callback2)
+
+ doReturn(TEST_ELAPSED_REALTIME_MS).`when`(clock).elapsedRealtime()
+
+ // Add multiple services with different ttl time.
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1,
+ DEFAULT_TTL_TIME_MS))
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1,
+ DEFAULT_TTL_TIME_MS + 20L))
+ addOrUpdateService(serviceCache, cacheKey2, createResponse(SERVICE_NAME_3, SERVICE_TYPE_2,
+ DEFAULT_TTL_TIME_MS + 10L))
+
+ // Check the service expiration immediately. Should be no callback.
+ assertEquals(2, getServices(serviceCache, cacheKey1).size)
+ assertEquals(1, getServices(serviceCache, cacheKey2).size)
+ callback1.assertNoCallback()
+ callback2.assertNoCallback()
+
+ // Simulate the case where the response is after TTL then check expired services.
+ // Expect SERVICE_NAME_1 expired.
+ doReturn(TEST_ELAPSED_REALTIME_MS + DEFAULT_TTL_TIME_MS).`when`(clock).elapsedRealtime()
+ assertEquals(1, getServices(serviceCache, cacheKey1).size)
+ assertEquals(1, getServices(serviceCache, cacheKey2).size)
+ callback1.expectedServiceRecordExpired(SERVICE_NAME_1)
+ callback2.assertNoCallback()
+
+ // Simulate the case where the response is after TTL then check expired services.
+ // Expect SERVICE_NAME_3 expired.
+ doReturn(TEST_ELAPSED_REALTIME_MS + DEFAULT_TTL_TIME_MS + 11L)
+ .`when`(clock).elapsedRealtime()
+ assertEquals(1, getServices(serviceCache, cacheKey1).size)
+ assertEquals(0, getServices(serviceCache, cacheKey2).size)
+ callback1.assertNoCallback()
+ callback2.expectedServiceRecordExpired(SERVICE_NAME_3)
+ }
+
+ @Test
+ fun testRemoveExpiredServiceWhenGetting() {
+ val serviceCache = MdnsServiceCache(
+ thread.looper, makeFlags(isExpiredServicesRemovalEnabled = true), clock)
+
+ doReturn(TEST_ELAPSED_REALTIME_MS).`when`(clock).elapsedRealtime()
+ addOrUpdateService(serviceCache, cacheKey1,
+ createResponse(SERVICE_NAME_1, SERVICE_TYPE_1, 1L /* ttlTime */))
+ doReturn(TEST_ELAPSED_REALTIME_MS + 2L).`when`(clock).elapsedRealtime()
+ assertNull(getService(serviceCache, SERVICE_NAME_1, cacheKey1))
+
+ addOrUpdateService(serviceCache, cacheKey2,
+ createResponse(SERVICE_NAME_2, SERVICE_TYPE_2, 3L /* ttlTime */))
+ doReturn(TEST_ELAPSED_REALTIME_MS + 4L).`when`(clock).elapsedRealtime()
+ assertEquals(0, getServices(serviceCache, cacheKey2).size)
+ }
+
+ @Test
+ fun testInsertResponseAndSortList() {
+ val responses = ArrayList<MdnsResponse>()
+ val response1 = createResponse(SERVICE_NAME_1, SERVICE_TYPE_1, 100L /* ttlTime */)
+ MdnsServiceCache.insertResponseAndSortList(responses, response1, TEST_ELAPSED_REALTIME_MS)
+ assertEquals(1, responses.size)
+ assertEquals(response1, responses[0])
+
+ val response2 = createResponse(SERVICE_NAME_2, SERVICE_TYPE_1, 50L /* ttlTime */)
+ MdnsServiceCache.insertResponseAndSortList(responses, response2, TEST_ELAPSED_REALTIME_MS)
+ assertEquals(2, responses.size)
+ assertEquals(response2, responses[0])
+ assertEquals(response1, responses[1])
+
+ val response3 = createResponse(SERVICE_NAME_3, SERVICE_TYPE_1, 75L /* ttlTime */)
+ MdnsServiceCache.insertResponseAndSortList(responses, response3, TEST_ELAPSED_REALTIME_MS)
+ assertEquals(3, responses.size)
+ assertEquals(response2, responses[0])
+ assertEquals(response3, responses[1])
+ assertEquals(response1, responses[2])
+
+ val response4 = createResponse("service-instance-4", SERVICE_TYPE_1, 125L /* ttlTime */)
+ MdnsServiceCache.insertResponseAndSortList(responses, response4, TEST_ELAPSED_REALTIME_MS)
+ assertEquals(4, responses.size)
+ assertEquals(response2, responses[0])
+ assertEquals(response3, responses[1])
+ assertEquals(response1, responses[2])
+ assertEquals(response4, responses[3])
+ }
+
+ private fun createResponse(
+ serviceInstanceName: String,
+ serviceType: String,
+ ttlTime: Long = 120000L
+ ): MdnsResponse {
+ val serviceName = "$serviceInstanceName.$serviceType".split(".").toTypedArray()
+ val response = MdnsResponse(
+ 0 /* now */, "$serviceInstanceName.$serviceType".split(".").toTypedArray(),
+ socketKey.interfaceIndex, socketKey.network)
+
+ // Set PTR record
+ val pointerRecord = MdnsPointerRecord(
+ serviceType.split(".").toTypedArray(),
+ TEST_ELAPSED_REALTIME_MS /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ ttlTime /* ttlMillis */,
+ serviceName)
+ response.addPointerRecord(pointerRecord)
+
+ // Set SRV record.
+ val serviceRecord = MdnsServiceRecord(
+ serviceName,
+ TEST_ELAPSED_REALTIME_MS /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ ttlTime /* ttlMillis */,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ 12345 /* port */,
+ arrayOf("hostname"))
+ response.serviceRecord = serviceRecord
+ return response
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index ce154dd..26a3796 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -194,7 +194,9 @@
thread.start();
handler = new Handler(thread.getLooper());
serviceCache = new MdnsServiceCache(
- thread.getLooper(), MdnsFeatureFlags.newBuilder().build());
+ thread.getLooper(),
+ MdnsFeatureFlags.newBuilder().setIsExpiredServicesRemovalEnabled(false).build(),
+ mockDecoderClock);
doAnswer(inv -> {
latestDelayMs = 0;
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
index 74f1c37..8b7ab71 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
@@ -78,6 +78,7 @@
@Mock private SharedLog sharedLog;
private MdnsSocketClient mdnsClient;
+ private MdnsFeatureFlags flags = MdnsFeatureFlags.newBuilder().build();
@Before
public void setup() throws RuntimeException, IOException {
@@ -86,7 +87,7 @@
when(mockWifiManager.createMulticastLock(ArgumentMatchers.anyString()))
.thenReturn(mockMulticastLock);
- mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock, sharedLog) {
+ mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock, sharedLog, flags) {
@Override
MdnsSocket createMdnsSocket(int port, SharedLog sharedLog) throws IOException {
if (port == MdnsConstants.MDNS_PORT) {
@@ -515,7 +516,7 @@
//MdnsConfigsFlagsImpl.allowNetworkInterfaceIndexPropagation.override(true);
when(mockMulticastSocket.getInterfaceIndex()).thenReturn(21);
- mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock, sharedLog) {
+ mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock, sharedLog, flags) {
@Override
MdnsSocket createMdnsSocket(int port, SharedLog sharedLog) {
if (port == MdnsConstants.MDNS_PORT) {
@@ -538,7 +539,7 @@
//MdnsConfigsFlagsImpl.allowNetworkInterfaceIndexPropagation.override(false);
when(mockMulticastSocket.getInterfaceIndex()).thenReturn(21);
- mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock, sharedLog) {
+ mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock, sharedLog, flags) {
@Override
MdnsSocket createMdnsSocket(int port, SharedLog sharedLog) {
if (port == MdnsConstants.MDNS_PORT) {
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
index 2126a09..a753922 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
@@ -47,8 +47,9 @@
val keepConnectedAgent = Agent(nc = nc, score = FromS(NetworkScore.Builder()
.setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
.build()),
- lnc = LocalNetworkConfig.Builder().build())
- val dontKeepConnectedAgent = Agent(nc = nc, lnc = LocalNetworkConfig.Builder().build())
+ lnc = FromS(LocalNetworkConfig.Builder().build()))
+ val dontKeepConnectedAgent = Agent(nc = nc,
+ lnc = FromS(LocalNetworkConfig.Builder().build()))
doTestKeepConnected(keepConnectedAgent, dontKeepConnectedAgent)
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
index cfc3a3d..6add6b9 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
@@ -49,7 +49,7 @@
private fun keepConnectedScore() =
FromS(NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build())
-private fun defaultLnc() = LocalNetworkConfig.Builder().build()
+private fun defaultLnc() = FromS(LocalNetworkConfig.Builder().build())
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@@ -124,8 +124,7 @@
lnc = null)
}
assertFailsWith<IllegalArgumentException> {
- Agent(nc = NetworkCapabilities.Builder().build(),
- lnc = LocalNetworkConfig.Builder().build())
+ Agent(nc = NetworkCapabilities.Builder().build(), lnc = defaultLnc())
}
}
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index 235f7de..ad21bf5 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -29,6 +29,7 @@
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_THREAD
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkRequest
import android.net.NetworkScore
@@ -93,7 +94,7 @@
}
assertFailsWith<IllegalArgumentException> {
Agent(nc = NetworkCapabilities.Builder().build(),
- lnc = LocalNetworkConfig.Builder().build())
+ lnc = FromS(LocalNetworkConfig.Builder().build()))
}
}
@@ -109,7 +110,7 @@
val agent = Agent(nc = NetworkCapabilities.Builder()
.addCapability(NET_CAPABILITY_LOCAL_NETWORK)
.build(),
- lnc = LocalNetworkConfig.Builder().build())
+ lnc = FromS(LocalNetworkConfig.Builder().build()))
agent.connect()
cb.expectAvailableCallbacks(agent.network, validated = false)
agent.sendNetworkCapabilities(NetworkCapabilities.Builder().build())
@@ -140,7 +141,7 @@
val localAgent = Agent(
nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
lp = lp("local0"),
- lnc = LocalNetworkConfig.Builder().build(),
+ lnc = FromS(LocalNetworkConfig.Builder().build()),
)
localAgent.connect()
@@ -193,11 +194,11 @@
// Set up a local agent that should forward its traffic to the best wifi upstream.
val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
lp = lp("local0"),
- lnc = LocalNetworkConfig.Builder()
+ lnc = FromS(LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
.addTransportType(TRANSPORT_WIFI)
.build())
- .build(),
+ .build()),
score = FromS(NetworkScore.Builder()
.setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
.build())
@@ -246,11 +247,11 @@
// Set up a local agent that should forward its traffic to the best wifi upstream.
val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
lp = lp("local0"),
- lnc = LocalNetworkConfig.Builder()
+ lnc = FromS(LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
.addTransportType(TRANSPORT_WIFI)
.build())
- .build(),
+ .build()),
score = FromS(NetworkScore.Builder()
.setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
.build())
@@ -292,11 +293,11 @@
cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
val localNc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK)
- val lnc = LocalNetworkConfig.Builder()
+ val lnc = FromS(LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
.addTransportType(TRANSPORT_WIFI)
.build())
- .build()
+ .build())
val localScore = FromS(NetworkScore.Builder().build())
// Set up a local agent that should forward its traffic to the best wifi upstream.
@@ -345,11 +346,11 @@
wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
lp = lp("local0"),
- lnc = LocalNetworkConfig.Builder()
+ lnc = FromS(LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
.addTransportType(TRANSPORT_WIFI)
.build())
- .build(),
+ .build()),
score = FromS(NetworkScore.Builder()
.setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
.build())
@@ -373,11 +374,11 @@
fun testForwardingRules() {
deps.setBuildSdk(VERSION_V)
// Set up a local agent that should forward its traffic to the best DUN upstream.
- val lnc = LocalNetworkConfig.Builder()
+ val lnc = FromS(LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
.addCapability(NET_CAPABILITY_DUN)
.build())
- .build()
+ .build())
val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
lp = lp("local0"),
lnc = lnc,
@@ -425,7 +426,7 @@
// Make sure sending the same config again doesn't do anything
repeat(5) {
- localAgent.sendLocalNetworkConfig(lnc)
+ localAgent.sendLocalNetworkConfig(lnc.value)
}
inOrder.verifyNoMoreInteractions()
@@ -472,4 +473,55 @@
cb.expect<Lost>(localAgent.network)
cb.assertNoCallback()
}
+
+ @Test
+ fun testLocalNetworkUnwanted_withUpstream() {
+ doTestLocalNetworkUnwanted(true)
+ }
+
+ @Test
+ fun testLocalNetworkUnwanted_withoutUpstream() {
+ doTestLocalNetworkUnwanted(false)
+ }
+
+ fun doTestLocalNetworkUnwanted(haveUpstream: Boolean) {
+ deps.setBuildSdk(VERSION_V)
+
+ val nr = NetworkRequest.Builder().addCapability(NET_CAPABILITY_LOCAL_NETWORK).build()
+ val requestCb = TestableNetworkCallback()
+ cm.requestNetwork(nr, requestCb)
+ val listenCb = TestableNetworkCallback()
+ cm.registerNetworkCallback(nr, listenCb)
+
+ val upstream = if (haveUpstream) {
+ Agent(score = keepScore(), lp = lp("wifi0"),
+ nc = nc(TRANSPORT_WIFI)).also { it.connect() }
+ } else {
+ null
+ }
+
+ // Set up a local agent.
+ val lnc = FromS(LocalNetworkConfig.Builder().apply {
+ if (haveUpstream) {
+ setUpstreamSelector(NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .build())
+ }
+ }.build())
+ val localAgent = Agent(nc = nc(TRANSPORT_THREAD, NET_CAPABILITY_LOCAL_NETWORK),
+ lp = lp("local0"),
+ lnc = lnc,
+ score = FromS(NetworkScore.Builder().build())
+ )
+ localAgent.connect()
+
+ requestCb.expectAvailableCallbacks(localAgent.network,
+ validated = false, upstream = upstream?.network)
+ listenCb.expectAvailableCallbacks(localAgent.network,
+ validated = false, upstream = upstream?.network)
+
+ cm.unregisterNetworkCallback(requestCb)
+
+ listenCb.expect<Lost>()
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
new file mode 100644
index 0000000..526ec9d
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
@@ -0,0 +1,267 @@
+/*
+ * 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.ConnectivityManager
+import android.net.ConnectivityManager.ACTION_DATA_ACTIVITY_CHANGE
+import android.net.ConnectivityManager.EXTRA_DEVICE_TYPE
+import android.net.ConnectivityManager.EXTRA_IS_ACTIVE
+import android.net.ConnectivityManager.EXTRA_REALTIME_NS
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_IMS
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.ConditionVariable
+import android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH
+import android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_LOW
+import androidx.test.filters.SmallTest
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener
+import com.android.server.CSTest.CSContext
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import kotlin.test.assertNotNull
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+
+private const val DATA_CELL_IFNAME = "rmnet_data"
+private const val IMS_CELL_IFNAME = "rmnet_ims"
+private const val WIFI_IFNAME = "wlan0"
+private const val TIMESTAMP = 1234L
+private const val NETWORK_ACTIVITY_NO_UID = -1
+private const val PACKAGE_UID = 123
+private const val TIMEOUT_MS = 250L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class CSNetworkActivityTest : CSTest() {
+
+ private fun getRegisteredNetdUnsolicitedEventListener(): BaseNetdUnsolicitedEventListener {
+ val captor = ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener::class.java)
+ verify(netd).registerUnsolicitedEventListener(captor.capture())
+ return captor.value
+ }
+
+ @Test
+ fun testInterfaceClassActivityChanged_NonDefaultNetwork() {
+ val netdUnsolicitedEventListener = getRegisteredNetdUnsolicitedEventListener()
+ val batteryStatsInorder = inOrder(batteryStats)
+
+ val cellNr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build()
+ val cellCb = TestableNetworkCallback()
+ // Request cell network to keep cell network up
+ cm.requestNetwork(cellNr, cellCb)
+
+ val defaultCb = TestableNetworkCallback()
+ cm.registerDefaultNetworkCallback(defaultCb)
+
+ val cellNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val cellLp = LinkProperties().apply {
+ interfaceName = DATA_CELL_IFNAME
+ }
+ // Connect Cellular network
+ val cellAgent = Agent(nc = cellNc, lp = cellLp)
+ cellAgent.connect()
+ defaultCb.expectAvailableCallbacks(cellAgent.network, validated = false)
+
+ val wifiNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val wifiLp = LinkProperties().apply {
+ interfaceName = WIFI_IFNAME
+ }
+ // Connect Wi-Fi network, Wi-Fi network should be the default network.
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ defaultCb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+ batteryStatsInorder.verify(batteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID))
+
+ val onNetworkActiveCv = ConditionVariable()
+ val listener = ConnectivityManager.OnNetworkActiveListener { onNetworkActiveCv::open }
+ cm.addDefaultNetworkActiveListener(listener)
+
+ // Cellular network (non default network) goes to inactive state.
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+ cellAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+ // Non-default network activity change does not change default network activity
+ // But cellular radio power state is updated
+ assertFalse(onNetworkActiveCv.block(TIMEOUT_MS))
+ context.expectNoDataActivityBroadcast(0 /* timeoutMs */)
+ assertTrue(cm.isDefaultNetworkActive)
+ batteryStatsInorder.verify(batteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_LOW),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID))
+
+ // Cellular network (non default network) goes to active state.
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+ cellAgent.network.netId, TIMESTAMP, PACKAGE_UID)
+ // Non-default network activity change does not change default network activity
+ // But cellular radio power state is updated
+ assertFalse(onNetworkActiveCv.block(TIMEOUT_MS))
+ context.expectNoDataActivityBroadcast(0 /* timeoutMs */)
+ assertTrue(cm.isDefaultNetworkActive)
+ batteryStatsInorder.verify(batteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(PACKAGE_UID))
+
+ cm.unregisterNetworkCallback(cellCb)
+ cm.unregisterNetworkCallback(defaultCb)
+ cm.removeDefaultNetworkActiveListener(listener)
+ }
+
+ @Test
+ fun testDataActivityTracking_MultiCellNetwork() {
+ val netdUnsolicitedEventListener = getRegisteredNetdUnsolicitedEventListener()
+ val batteryStatsInorder = inOrder(batteryStats)
+
+ val dataNetworkNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .build()
+ val dataNetworkNr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build()
+ val dataNetworkLp = LinkProperties().apply {
+ interfaceName = DATA_CELL_IFNAME
+ }
+ val dataNetworkCb = TestableNetworkCallback()
+ cm.requestNetwork(dataNetworkNr, dataNetworkCb)
+ val dataNetworkAgent = Agent(nc = dataNetworkNc, lp = dataNetworkLp)
+ val dataNetworkNetId = dataNetworkAgent.network.netId.toString()
+
+ val imsNetworkNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_IMS)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .build()
+ val imsNetworkNr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_IMS)
+ .build()
+ val imsNetworkLp = LinkProperties().apply {
+ interfaceName = IMS_CELL_IFNAME
+ }
+ val imsNetworkCb = TestableNetworkCallback()
+ cm.requestNetwork(imsNetworkNr, imsNetworkCb)
+ val imsNetworkAgent = Agent(nc = imsNetworkNc, lp = imsNetworkLp)
+ val imsNetworkNetId = imsNetworkAgent.network.netId.toString()
+
+ dataNetworkAgent.connect()
+ dataNetworkCb.expectAvailableCallbacks(dataNetworkAgent.network, validated = false)
+
+ imsNetworkAgent.connect()
+ imsNetworkCb.expectAvailableCallbacks(imsNetworkAgent.network, validated = false)
+
+ // Both cell networks have idleTimers
+ verify(netd).idletimerAddInterface(eq(DATA_CELL_IFNAME), anyInt(), eq(dataNetworkNetId))
+ verify(netd).idletimerAddInterface(eq(IMS_CELL_IFNAME), anyInt(), eq(imsNetworkNetId))
+ verify(netd, never()).idletimerRemoveInterface(eq(DATA_CELL_IFNAME), anyInt(),
+ eq(dataNetworkNetId))
+ verify(netd, never()).idletimerRemoveInterface(eq(IMS_CELL_IFNAME), anyInt(),
+ eq(imsNetworkNetId))
+
+ // Both cell networks go to inactive state
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+ imsNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+ dataNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+
+ // Data cell network goes to active state. This should update the cellular radio power state
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+ dataNetworkAgent.network.netId, TIMESTAMP, PACKAGE_UID)
+ batteryStatsInorder.verify(batteryStats, timeout(TIMEOUT_MS)).noteMobileRadioPowerState(
+ eq(DC_POWER_STATE_HIGH), anyLong() /* timestampNs */, eq(PACKAGE_UID))
+ // Ims cell network goes to active state. But this should not update the cellular radio
+ // power state since cellular radio power state is already high
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+ imsNetworkAgent.network.netId, TIMESTAMP, PACKAGE_UID)
+ waitForIdle()
+ batteryStatsInorder.verify(batteryStats, never()).noteMobileRadioPowerState(anyInt(),
+ anyLong() /* timestampNs */, anyInt())
+
+ // Data cell network goes to inactive state. But this should not update the cellular radio
+ // power state ims cell network is still active state
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+ dataNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+ waitForIdle()
+ batteryStatsInorder.verify(batteryStats, never()).noteMobileRadioPowerState(anyInt(),
+ anyLong() /* timestampNs */, anyInt())
+
+ // Ims cell network goes to inactive state.
+ // This should update the cellular radio power state
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+ imsNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+ batteryStatsInorder.verify(batteryStats, timeout(TIMEOUT_MS)).noteMobileRadioPowerState(
+ eq(DC_POWER_STATE_LOW), anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID))
+
+ dataNetworkAgent.disconnect()
+ dataNetworkCb.expect<Lost>(dataNetworkAgent.network)
+ verify(netd).idletimerRemoveInterface(eq(DATA_CELL_IFNAME), anyInt(), eq(dataNetworkNetId))
+
+ imsNetworkAgent.disconnect()
+ imsNetworkCb.expect<Lost>(imsNetworkAgent.network)
+ verify(netd).idletimerRemoveInterface(eq(IMS_CELL_IFNAME), anyInt(), eq(imsNetworkNetId))
+
+ cm.unregisterNetworkCallback(dataNetworkCb)
+ cm.unregisterNetworkCallback(imsNetworkCb)
+ }
+}
+
+internal fun CSContext.expectDataActivityBroadcast(
+ deviceType: Int,
+ isActive: Boolean,
+ tsNanos: Long
+) {
+ assertNotNull(orderedBroadcastAsUserHistory.poll(BROADCAST_TIMEOUT_MS) {
+ intent -> intent.action.equals(ACTION_DATA_ACTIVITY_CHANGE) &&
+ intent.getIntExtra(EXTRA_DEVICE_TYPE, -1) == deviceType &&
+ intent.getBooleanExtra(EXTRA_IS_ACTIVE, !isActive) == isActive &&
+ intent.getLongExtra(EXTRA_REALTIME_NS, -1) == tsNanos
+ })
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index 013a749..d41c742 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -69,7 +69,7 @@
nac: NetworkAgentConfig,
val nc: NetworkCapabilities,
val lp: LinkProperties,
- val lnc: LocalNetworkConfig?,
+ val lnc: FromS<LocalNetworkConfig>?,
val score: FromS<NetworkScore>,
val provider: NetworkProvider?
) : TestableNetworkCallback.HasNetwork {
@@ -101,7 +101,7 @@
// Create the actual agent. NetworkAgent is abstract, so make an anonymous subclass.
if (deps.isAtLeastS()) {
agent = object : NetworkAgent(context, csHandlerThread.looper, TAG,
- nc, lp, lnc, score.value, nac, provider) {}
+ nc, lp, lnc?.value, score.value, nac, provider) {}
} else {
agent = object : NetworkAgent(context, csHandlerThread.looper, TAG,
nc, lp, 50 /* score */, nac, provider) {}
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 0ccbfc3..4f5cfc0 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -43,6 +43,7 @@
import android.net.PacProxyManager
import android.net.networkstack.NetworkStackClientBase
import android.os.BatteryStatsManager
+import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.os.UserHandle
@@ -54,6 +55,7 @@
import com.android.internal.app.IBatteryStats
import com.android.internal.util.test.BroadcastInterceptingContext
import com.android.modules.utils.build.SdkLevel
+import com.android.net.module.util.ArrayTrackRecord
import com.android.networkstack.apishim.common.UnsupportedApiLevelException
import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker
import com.android.server.connectivity.CarrierPrivilegeAuthenticator
@@ -64,14 +66,16 @@
import com.android.server.connectivity.ProxyTracker
import com.android.testutils.visibleOnHandlerThread
import com.android.testutils.waitForIdle
+import java.util.concurrent.Executors
+import kotlin.test.assertNull
+import kotlin.test.fail
import org.mockito.AdditionalAnswers.delegatesTo
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
-import java.util.concurrent.Executors
-import kotlin.test.fail
internal const val HANDLER_TIMEOUT_MS = 2_000
+internal const val BROADCAST_TIMEOUT_MS = 3_000L
internal const val TEST_PACKAGE_NAME = "com.android.test.package"
internal const val WIFI_WOL_IFNAME = "test_wlan_wol"
internal val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
@@ -127,6 +131,7 @@
it[ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER] = true
it[ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION] = true
it[ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION] = true
+ it[ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS] = true
}
fun enableFeature(f: String) = enabledFeatures.set(f, true)
fun disableFeature(f: String) = enabledFeatures.set(f, false)
@@ -154,7 +159,8 @@
val proxyTracker = ProxyTracker(context, mock<Handler>(), 16 /* EVENT_PROXY_HAS_CHANGED */)
val alarmManager = makeMockAlarmManager()
val systemConfigManager = makeMockSystemConfigManager()
- val batteryManager = BatteryStatsManager(mock<IBatteryStats>())
+ val batteryStats = mock<IBatteryStats>()
+ val batteryManager = BatteryStatsManager(batteryStats)
val telephonyManager = mock<TelephonyManager>().also {
doReturn(true).`when`(it).isDataCapable()
}
@@ -195,6 +201,8 @@
// checking permissions.
override fun isFeatureEnabled(context: Context?, name: String?) =
enabledFeatures[name] ?: fail("Unmocked feature $name, see CSTest.enabledFeatures")
+ override fun isFeatureNotChickenedOut(context: Context?, name: String?) =
+ enabledFeatures[name] ?: fail("Unmocked feature $name, see CSTest.enabledFeatures")
// Mocked change IDs
private val enabledChangeIds = ArraySet<Long>()
@@ -282,6 +290,26 @@
Context.STATS_MANAGER -> null // Stats manager is final and can't be mocked
else -> super.getSystemService(serviceName)
}
+
+ internal val orderedBroadcastAsUserHistory = ArrayTrackRecord<Intent>().newReadHead()
+
+ fun expectNoDataActivityBroadcast(timeoutMs: Int) {
+ assertNull(orderedBroadcastAsUserHistory.poll(
+ timeoutMs.toLong()) { intent -> true })
+ }
+
+ override fun sendOrderedBroadcastAsUser(
+ intent: Intent,
+ user: UserHandle,
+ receiverPermission: String?,
+ resultReceiver: BroadcastReceiver?,
+ scheduler: Handler?,
+ initialCode: Int,
+ initialData: String?,
+ initialExtras: Bundle?
+ ) {
+ orderedBroadcastAsUserHistory.add(intent)
+ }
}
// Utility methods for subclasses to use
@@ -293,7 +321,7 @@
nc: NetworkCapabilities = defaultNc(),
nac: NetworkAgentConfig = emptyAgentConfig(nc.getLegacyType()),
lp: LinkProperties = defaultLp(),
- lnc: LocalNetworkConfig? = null,
+ lnc: FromS<LocalNetworkConfig>? = null,
score: FromS<NetworkScore> = defaultScore(),
provider: NetworkProvider? = null
) = CSAgentWrapper(context, deps, csHandlerThread, networkStack,
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 92a5b64..7a4dfed 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -193,6 +193,7 @@
* TODO: This test used to be really brittle because it used Easymock - it uses Mockito now, but
* still uses the Easymock structure, which could be simplified.
*/
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
// NetworkStatsService is not updatable before T, so tests do not need to be backwards compatible
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
index 3eaebfa..30aeca5 100644
--- a/thread/TEST_MAPPING
+++ b/thread/TEST_MAPPING
@@ -3,5 +3,10 @@
{
"name": "CtsThreadNetworkTestCases"
}
+ ],
+ "postsubmit": [
+ {
+ "name": "ThreadNetworkUnitTests"
+ }
]
}
diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
index c9b047a..b74a15a 100644
--- a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
+++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
@@ -16,8 +16,6 @@
package android.net.thread;
-import static android.net.thread.ActiveOperationalDataset.SecurityPolicy.DEFAULT_ROTATION_TIME_HOURS;
-
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkState;
import static com.android.net.module.util.HexDump.dumpHexString;
@@ -41,26 +39,25 @@
import java.io.ByteArrayOutputStream;
import java.net.Inet6Address;
import java.net.UnknownHostException;
-import java.security.SecureRandom;
-import java.time.Instant;
import java.util.Arrays;
-import java.util.Random;
/**
* Data interface for managing a Thread Active Operational Dataset.
*
- * <p>An example usage of creating an Active Operational Dataset with random parameters:
+ * <p>An example usage of creating an Active Operational Dataset with randomized parameters:
*
* <pre>{@code
- * ActiveOperationalDataset activeDataset = ActiveOperationalDataset.createRandomDataset();
+ * ActiveOperationalDataset activeDataset = controller.createRandomizedDataset("MyNet");
* }</pre>
*
- * <p>or random Dataset with customized Network Name:
+ * <p>or randomized Dataset with customized channel:
*
* <pre>{@code
* ActiveOperationalDataset activeDataset =
- * new ActiveOperationalDataset.Builder(ActiveOperationalDataset.createRandomDataset())
- * .setNetworkName("MyThreadNet").build();
+ * new ActiveOperationalDataset.Builder(controller.createRandomizedDataset("MyNet"))
+ * .setChannel(CHANNEL_PAGE_24_GHZ, 17)
+ * .setActiveTimestamp(OperationalDatasetTimestamp.fromInstant(Instant.now()))
+ * .build();
* }</pre>
*
* <p>If the Active Operational Dataset is already known as <a
@@ -116,7 +113,9 @@
/** @hide */
@VisibleForTesting public static final int TYPE_CHANNEL_MASK = 53;
- private static final byte MESH_LOCAL_PREFIX_FIRST_BYTE = (byte) 0xfd;
+ /** @hide */
+ public static final byte MESH_LOCAL_PREFIX_FIRST_BYTE = (byte) 0xfd;
+
private static final int LENGTH_CHANNEL = 3;
private static final int LENGTH_PAN_ID = 2;
@@ -344,86 +343,6 @@
outputStream.write(entries, 0, entries.length);
}
- /**
- * Creates a new {@link ActiveOperationalDataset} object with randomized or default parameters.
- *
- * <p>The randomized (or default) value for each parameter:
- *
- * <ul>
- * <li>{@code Active Timestamp} defaults to {@code new OperationalDatasetTimestamp(1, 0,
- * false)}
- * <li>{@code Network Name} defaults to "THREAD-PAN-<PAN ID decimal>", for example
- * "THREAD-PAN-12345"
- * <li>{@code Extended PAN ID} filled with randomly generated bytes
- * <li>{@code PAN ID} randomly generated integer in range of [0, 0xfffe]
- * <li>{@code Channel Page} defaults to {@link #CHANNEL_PAGE_24_GHZ}
- * <li>{@code Channel} randomly selected channel in range of [{@link #CHANNEL_MIN_24_GHZ},
- * {@link #CHANNEL_MAX_24_GHZ}]
- * <li>{@code Channel Mask} all bits from {@link #CHANNEL_MIN_24_GHZ} to {@link
- * #CHANNEL_MAX_24_GHZ} are set to {@code true}
- * <li>{@code PSKc} filled with bytes generated by secure random generator
- * <li>{@code Network Key} filled with bytes generated by secure random generator
- * <li>{@code Mesh-local Prefix} filled with randomly generated bytes except that the first
- * byte is always set to {@code 0xfd}
- * <li>{@code Security Policy} defaults to {@code new SecurityPolicy(
- * DEFAULT_ROTATION_TIME_HOURS, new byte[]{(byte)0xff, (byte)0xf8})}. This is the default
- * values required by the Thread 1.2 specification
- * </ul>
- *
- * <p>This method is the recommended way to create a randomized operational dataset for a new
- * Thread network. It may be desired to change one or more of the generated value(s). For
- * example, to use a more meaningful Network Name. To do that, create a new {@link Builder}
- * object from this dataset with {@link Builder#Builder(ActiveOperationalDataset)} and override
- * the value with the setters of {@link Builder}.
- *
- * <p>Note that it's highly discouraged to change the randomly generated Extended PAN ID,
- * Network Key or PSKc, as it will compromise the security of a Thread network.
- */
- @NonNull
- public static ActiveOperationalDataset createRandomDataset() {
- return createRandomDataset(new Random(Instant.now().toEpochMilli()), new SecureRandom());
- }
-
- /** @hide */
- @VisibleForTesting
- public static ActiveOperationalDataset createRandomDataset(
- Random random, SecureRandom secureRandom) {
- int panId = random.nextInt(/* bound= */ 0xffff);
- byte[] meshLocalPrefix = newRandomBytes(random, LENGTH_MESH_LOCAL_PREFIX_BITS / 8);
- meshLocalPrefix[0] = MESH_LOCAL_PREFIX_FIRST_BYTE;
-
- SparseArray<byte[]> channelMask = new SparseArray<>(1);
- channelMask.put(CHANNEL_PAGE_24_GHZ, new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
-
- return new Builder()
- .setActiveTimestamp(
- new OperationalDatasetTimestamp(
- /* seconds= */ 1,
- /* ticks= */ 0,
- /* isAuthoritativeSource= */ false))
- .setExtendedPanId(newRandomBytes(random, LENGTH_EXTENDED_PAN_ID))
- .setPanId(panId)
- .setNetworkName("THREAD-PAN-" + panId)
- .setChannel(
- CHANNEL_PAGE_24_GHZ,
- random.nextInt(CHANNEL_MAX_24_GHZ - CHANNEL_MIN_24_GHZ + 1)
- + CHANNEL_MIN_24_GHZ)
- .setChannelMask(channelMask)
- .setPskc(newRandomBytes(secureRandom, LENGTH_PSKC))
- .setNetworkKey(newRandomBytes(secureRandom, LENGTH_NETWORK_KEY))
- .setMeshLocalPrefix(meshLocalPrefix)
- .setSecurityPolicy(
- new SecurityPolicy(
- DEFAULT_ROTATION_TIME_HOURS, new byte[] {(byte) 0xff, (byte) 0xf8}))
- .build();
- }
-
- private static byte[] newRandomBytes(Random random, int length) {
- byte[] result = new byte[length];
- random.nextBytes(result);
- return result;
- }
-
private static boolean areByteSparseArraysEqual(
@NonNull SparseArray<byte[]> first, @NonNull SparseArray<byte[]> second) {
if (first == second) {
@@ -683,6 +602,20 @@
return sb.toString();
}
+ static String checkNetworkName(@NonNull String networkName) {
+ requireNonNull(networkName, "networkName cannot be null");
+
+ int nameLength = networkName.getBytes(UTF_8).length;
+ checkArgument(
+ nameLength >= LENGTH_MIN_NETWORK_NAME_BYTES
+ && nameLength <= LENGTH_MAX_NETWORK_NAME_BYTES,
+ "Invalid network name (length = %d, expectedLengthRange = [%d, %d])",
+ nameLength,
+ LENGTH_MIN_NETWORK_NAME_BYTES,
+ LENGTH_MAX_NETWORK_NAME_BYTES);
+ return networkName;
+ }
+
/** The builder for creating {@link ActiveOperationalDataset} objects. */
public static final class Builder {
private OperationalDatasetTimestamp mActiveTimestamp;
@@ -748,7 +681,7 @@
* @param networkName the name of the Thread network
* @throws IllegalArgumentException if length of the UTF-8 representation of {@code
* networkName} isn't in range of [{@link #LENGTH_MIN_NETWORK_NAME_BYTES}, {@link
- * #LENGTH_MAX_NETWORK_NAME_BYTES}].
+ * #LENGTH_MAX_NETWORK_NAME_BYTES}]
*/
@NonNull
public Builder setNetworkName(
@@ -757,26 +690,16 @@
min = LENGTH_MIN_NETWORK_NAME_BYTES,
max = LENGTH_MAX_NETWORK_NAME_BYTES)
String networkName) {
- requireNonNull(networkName, "networkName cannot be null");
-
- int nameLength = networkName.getBytes(UTF_8).length;
- checkArgument(
- nameLength >= LENGTH_MIN_NETWORK_NAME_BYTES
- && nameLength <= LENGTH_MAX_NETWORK_NAME_BYTES,
- "Invalid network name (length = %d, expectedLengthRange = [%d, %d])",
- nameLength,
- LENGTH_MIN_NETWORK_NAME_BYTES,
- LENGTH_MAX_NETWORK_NAME_BYTES);
- this.mNetworkName = networkName;
+ this.mNetworkName = checkNetworkName(networkName);
return this;
}
/**
* Sets the Extended PAN ID.
*
- * <p>Use with caution. A randomly generated Extended PAN ID should be used for real Thread
+ * <p>Use with caution. A randomized Extended PAN ID should be used for real Thread
* networks. It's discouraged to call this method to override the default value created by
- * {@link ActiveOperationalDataset#createRandomDataset} in production.
+ * {@link ThreadNetworkController#createRandomizedDataset} in production.
*
* @throws IllegalArgumentException if length of {@code extendedPanId} is not {@link
* #LENGTH_EXTENDED_PAN_ID}.
@@ -867,7 +790,7 @@
*
* <p>Use with caution. A randomly generated PSKc should be used for real Thread networks.
* It's discouraged to call this method to override the default value created by {@link
- * ActiveOperationalDataset#createRandomDataset} in production.
+ * ThreadNetworkController#createRandomizedDataset} in production.
*
* @param pskc the key stretched version of the Commissioning Credential for the network
* @throws IllegalArgumentException if length of {@code pskc} is not {@link #LENGTH_PSKC}
@@ -889,7 +812,7 @@
*
* <p>Use with caution, randomly generated Network Key should be used for real Thread
* networks. It's discouraged to call this method to override the default value created by
- * {@link ActiveOperationalDataset#createRandomDataset} in production.
+ * {@link ThreadNetworkController#createRandomizedDataset} in production.
*
* @param networkKey a 128-bit security key-derivation key for the Thread Network
* @throws IllegalArgumentException if length of {@code networkKey} is not {@link
@@ -930,8 +853,16 @@
return this;
}
+ /**
+ * Sets the Mesh-Local Prefix.
+ *
+ * @param meshLocalPrefix the prefix used for realm-local traffic within the mesh
+ * @throws IllegalArgumentException if {@code meshLocalPrefix} doesn't start with {@code
+ * 0xfd} or has length other than {@code LENGTH_MESH_LOCAL_PREFIX_BITS / 8}
+ * @hide
+ */
@NonNull
- private Builder setMeshLocalPrefix(byte[] meshLocalPrefix) {
+ public Builder setMeshLocalPrefix(byte[] meshLocalPrefix) {
final int prefixLength = meshLocalPrefix.length * 8;
checkArgument(
prefixLength == LENGTH_MESH_LOCAL_PREFIX_BITS,
diff --git a/thread/framework/java/android/net/thread/IActiveOperationalDatasetReceiver.aidl b/thread/framework/java/android/net/thread/IActiveOperationalDatasetReceiver.aidl
new file mode 100644
index 0000000..aba54eb
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IActiveOperationalDatasetReceiver.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright 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 android.net.thread.ActiveOperationalDataset;
+
+/** Receives the result of an operation which returns an Active Operational Dataset. @hide */
+oneway interface IActiveOperationalDatasetReceiver {
+ void onSuccess(in ActiveOperationalDataset dataset);
+ void onError(int errorCode, String errorMessage);
+}
diff --git a/thread/framework/java/android/net/thread/IOperationReceiver.aidl b/thread/framework/java/android/net/thread/IOperationReceiver.aidl
new file mode 100644
index 0000000..42e157b
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IOperationReceiver.aidl
@@ -0,0 +1,23 @@
+/*
+ * Copyright 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;
+
+/** Receives the result of a Thread network operation. @hide */
+oneway interface IOperationReceiver {
+ void onSuccess();
+ void onError(int errorCode, String errorMessage);
+}
diff --git a/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl b/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
new file mode 100644
index 0000000..b576b33
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright 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 android.net.thread.ActiveOperationalDataset;
+import android.net.thread.PendingOperationalDataset;
+
+/**
+ * @hide
+ */
+oneway interface IOperationalDatasetCallback {
+ void onActiveOperationalDatasetChanged(in @nullable ActiveOperationalDataset activeOpDataset);
+ void onPendingOperationalDatasetChanged(in @nullable PendingOperationalDataset pendingOpDataset);
+}
diff --git a/thread/framework/java/android/net/thread/IScheduleMigrationReceiver.aidl b/thread/framework/java/android/net/thread/IScheduleMigrationReceiver.aidl
new file mode 100644
index 0000000..c45d463
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IScheduleMigrationReceiver.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright 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;
+
+/** Receives the result of {@link ThreadNetworkManager#scheduleMigration}. @hide */
+oneway interface IScheduleMigrationReceiver {
+ void onScheduled(long delayTimerMillis);
+ void onMigrated();
+ void onError(int errorCode, String errorMessage);
+}
diff --git a/thread/framework/java/android/net/thread/IStateCallback.aidl b/thread/framework/java/android/net/thread/IStateCallback.aidl
new file mode 100644
index 0000000..d7cbda9
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IStateCallback.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright 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;
+
+/**
+ * @hide
+ */
+oneway interface IStateCallback {
+ void onDeviceRoleChanged(int deviceRole);
+ void onPartitionIdChanged(long partitionId);
+}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index 0219beb..51e4d88 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -16,10 +16,29 @@
package android.net.thread;
+import android.net.Network;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IActiveOperationalDatasetReceiver;
+import android.net.thread.IOperationalDatasetCallback;
+import android.net.thread.IOperationReceiver;
+import android.net.thread.IScheduleMigrationReceiver;
+import android.net.thread.IStateCallback;
+import android.net.thread.PendingOperationalDataset;
+
/**
* Interface for communicating with ThreadNetworkControllerService.
* @hide
*/
interface IThreadNetworkController {
+ void registerStateCallback(in IStateCallback callback);
+ void unregisterStateCallback(in IStateCallback callback);
+ void registerOperationalDatasetCallback(in IOperationalDatasetCallback callback);
+ void unregisterOperationalDatasetCallback(in IOperationalDatasetCallback callback);
+
+ void join(in ActiveOperationalDataset activeOpDataset, in IOperationReceiver receiver);
+ void scheduleMigration(in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
+ void leave(in IOperationReceiver receiver);
+
int getThreadVersion();
+ void createRandomizedDataset(String networkName, IActiveOperationalDatasetReceiver receiver);
}
diff --git a/thread/framework/java/android/net/thread/PendingOperationalDataset.java b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
index 4762d7f..c1351af 100644
--- a/thread/framework/java/android/net/thread/PendingOperationalDataset.java
+++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
@@ -39,6 +39,7 @@
* a given delay. This is typically used to deploy new network parameters (e.g. Network Key or
* Channel) to all devices in the network.
*
+ * @see ThreadNetworkController#scheduleMigration
* @hide
*/
@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@@ -69,7 +70,16 @@
@NonNull private final OperationalDatasetTimestamp mPendingTimestamp;
@NonNull private final Duration mDelayTimer;
- /** Creates a new {@link PendingOperationalDataset} object. */
+ /**
+ * Creates a new {@link PendingOperationalDataset} object.
+ *
+ * @param activeOpDataset the included Active Operational Dataset
+ * @param pendingTimestamp the Pending Timestamp which represents the version of this Pending
+ * Dataset
+ * @param delayTimer the delay after when {@code activeOpDataset} will be committed on this
+ * device; use {@link Duration#ZERO} to tell the system to choose a reasonable value
+ * automatically
+ */
public PendingOperationalDataset(
@NonNull ActiveOperationalDataset activeOpDataset,
@NonNull OperationalDatasetTimestamp pendingTimestamp,
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index fc77b1a..5c5fda9 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -18,23 +18,64 @@
import static java.util.Objects.requireNonNull;
+import android.Manifest.permission;
+import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
+import android.os.OutcomeReceiver;
import android.os.RemoteException;
+import com.android.internal.annotations.GuardedBy;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Executor;
/**
- * Provides the primary API for controlling all aspects of a Thread network.
+ * Provides the primary APIs for controlling all aspects of a Thread network.
+ *
+ * <p>For example, join this device to a Thread network with given Thread Operational Dataset, or
+ * migrate an existing network.
*
* @hide
*/
@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@SystemApi
public final class ThreadNetworkController {
+ private static final String TAG = "ThreadNetworkController";
+
+ /** The Thread stack is stopped. */
+ public static final int DEVICE_ROLE_STOPPED = 0;
+
+ /** The device is not currently participating in a Thread network/partition. */
+ public static final int DEVICE_ROLE_DETACHED = 1;
+
+ /** The device is a Thread Child. */
+ public static final int DEVICE_ROLE_CHILD = 2;
+
+ /** The device is a Thread Router. */
+ public static final int DEVICE_ROLE_ROUTER = 3;
+
+ /** The device is a Thread Leader. */
+ public static final int DEVICE_ROLE_LEADER = 4;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ DEVICE_ROLE_STOPPED,
+ DEVICE_ROLE_DETACHED,
+ DEVICE_ROLE_CHILD,
+ DEVICE_ROLE_ROUTER,
+ DEVICE_ROLE_LEADER
+ })
+ public @interface DeviceRole {}
/** Thread standard version 1.3. */
public static final int THREAD_VERSION_1_3 = 4;
@@ -46,9 +87,19 @@
private final IThreadNetworkController mControllerService;
+ private final Object mStateCallbackMapLock = new Object();
+
+ @GuardedBy("mStateCallbackMapLock")
+ private final Map<StateCallback, StateCallbackProxy> mStateCallbackMap = new HashMap<>();
+
+ private final Object mOpDatasetCallbackMapLock = new Object();
+
+ @GuardedBy("mOpDatasetCallbackMapLock")
+ private final Map<OperationalDatasetCallback, OperationalDatasetCallbackProxy>
+ mOpDatasetCallbackMap = new HashMap<>();
+
ThreadNetworkController(@NonNull IThreadNetworkController controllerService) {
requireNonNull(controllerService, "controllerService cannot be null");
-
mControllerService = controllerService;
}
@@ -61,4 +112,420 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Creates a new Active Operational Dataset with randomized parameters.
+ *
+ * <p>This method is the recommended way to create a randomized dataset which can be used with
+ * {@link #join} to securely join this device to the specified network . It's highly discouraged
+ * to change the randomly generated Extended PAN ID, Network Key or PSKc, as it will compromise
+ * the security of a Thread network.
+ *
+ * @throws IllegalArgumentException if length of the UTF-8 representation of {@code networkName}
+ * isn't in range of [{@link #LENGTH_MIN_NETWORK_NAME_BYTES}, {@link
+ * #LENGTH_MAX_NETWORK_NAME_BYTES}]
+ */
+ public void createRandomizedDataset(
+ @NonNull String networkName,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> receiver) {
+ ActiveOperationalDataset.checkNetworkName(networkName);
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(receiver, "receiver cannot be null");
+ try {
+ mControllerService.createRandomizedDataset(
+ networkName, new ActiveDatasetReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Returns {@code true} if {@code deviceRole} indicates an attached state. */
+ public static boolean isAttached(@DeviceRole int deviceRole) {
+ return deviceRole == DEVICE_ROLE_CHILD
+ || deviceRole == DEVICE_ROLE_ROUTER
+ || deviceRole == DEVICE_ROLE_LEADER;
+ }
+
+ /**
+ * Callback to receive notifications when the Thread network states are changed.
+ *
+ * <p>Applications which are interested in monitoring Thread network states should implement
+ * this interface and register the callback with {@link #registerStateCallback}.
+ */
+ public interface StateCallback {
+ /**
+ * The Thread device role has changed.
+ *
+ * @param deviceRole the new Thread device role
+ */
+ void onDeviceRoleChanged(@DeviceRole int deviceRole);
+
+ /**
+ * The Thread network partition ID has changed.
+ *
+ * @param partitionId the new Thread partition ID
+ */
+ default void onPartitionIdChanged(long partitionId) {}
+ }
+
+ private static final class StateCallbackProxy extends IStateCallback.Stub {
+ private final Executor mExecutor;
+ private final StateCallback mCallback;
+
+ StateCallbackProxy(@CallbackExecutor Executor executor, StateCallback callback) {
+ mExecutor = executor;
+ mCallback = callback;
+ }
+
+ @Override
+ public void onDeviceRoleChanged(@DeviceRole int deviceRole) {
+ mExecutor.execute(() -> mCallback.onDeviceRoleChanged(deviceRole));
+ }
+
+ @Override
+ public void onPartitionIdChanged(long partitionId) {
+ mExecutor.execute(() -> mCallback.onPartitionIdChanged(partitionId));
+ }
+ }
+
+ /**
+ * Registers a callback to be called when Thread network states are changed.
+ *
+ * <p>Upon return of this method, methods of {@code callback} will be invoked immediately with
+ * existing states.
+ *
+ * @param executor the executor to execute the {@code callback}
+ * @param callback the callback to receive Thread network state changes
+ * @throws IllegalArgumentException if {@code callback} has already been registered
+ */
+ @RequiresPermission(permission.ACCESS_NETWORK_STATE)
+ public void registerStateCallback(
+ @NonNull @CallbackExecutor Executor executor, @NonNull StateCallback callback) {
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(callback, "callback cannot be null");
+ synchronized (mStateCallbackMapLock) {
+ if (mStateCallbackMap.containsKey(callback)) {
+ throw new IllegalArgumentException("callback has already been registered");
+ }
+ StateCallbackProxy callbackProxy = new StateCallbackProxy(executor, callback);
+ mStateCallbackMap.put(callback, callbackProxy);
+
+ try {
+ mControllerService.registerStateCallback(callbackProxy);
+ } catch (RemoteException e) {
+ mStateCallbackMap.remove(callback);
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Unregisters the Thread state changed callback.
+ *
+ * @param callback the callback which has been registered with {@link #registerStateCallback}
+ * @throws IllegalArgumentException if {@code callback} hasn't been registered
+ */
+ @RequiresPermission(permission.ACCESS_NETWORK_STATE)
+ public void unregisterStateCallback(@NonNull StateCallback callback) {
+ requireNonNull(callback, "callback cannot be null");
+ synchronized (mStateCallbackMapLock) {
+ StateCallbackProxy callbackProxy = mStateCallbackMap.get(callback);
+ if (callbackProxy == null) {
+ throw new IllegalArgumentException("callback hasn't been registered");
+ }
+ try {
+ mControllerService.unregisterStateCallback(callbackProxy);
+ mStateCallbackMap.remove(callback);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Callback to receive notifications when the Thread Operational Datasets are changed.
+ *
+ * <p>Applications which are interested in monitoring Thread network datasets should implement
+ * this interface and register the callback with {@link #registerOperationalDatasetCallback}.
+ */
+ public interface OperationalDatasetCallback {
+ /**
+ * Called when the Active Operational Dataset is changed.
+ *
+ * @param activeDataset the new Active Operational Dataset or {@code null} if the dataset is
+ * absent
+ */
+ void onActiveOperationalDatasetChanged(@Nullable ActiveOperationalDataset activeDataset);
+
+ /**
+ * Called when the Pending Operational Dataset is changed.
+ *
+ * @param pendingDataset the new Pending Operational Dataset or {@code null} if the dataset
+ * has been committed and removed
+ */
+ default void onPendingOperationalDatasetChanged(
+ @Nullable PendingOperationalDataset pendingDataset) {}
+ }
+
+ private static final class OperationalDatasetCallbackProxy
+ extends IOperationalDatasetCallback.Stub {
+ private final Executor mExecutor;
+ private final OperationalDatasetCallback mCallback;
+
+ OperationalDatasetCallbackProxy(
+ @CallbackExecutor Executor executor, OperationalDatasetCallback callback) {
+ mExecutor = executor;
+ mCallback = callback;
+ }
+
+ @Override
+ public void onActiveOperationalDatasetChanged(
+ @Nullable ActiveOperationalDataset activeDataset) {
+ mExecutor.execute(() -> mCallback.onActiveOperationalDatasetChanged(activeDataset));
+ }
+
+ @Override
+ public void onPendingOperationalDatasetChanged(
+ @Nullable PendingOperationalDataset pendingDataset) {
+ mExecutor.execute(() -> mCallback.onPendingOperationalDatasetChanged(pendingDataset));
+ }
+ }
+
+ /**
+ * Registers a callback to be called when Thread Operational Datasets are changed.
+ *
+ * <p>Upon return of this method, methods of {@code callback} will be invoked immediately with
+ * existing Operational Datasets.
+ *
+ * @param executor the executor to execute {@code callback}
+ * @param callback the callback to receive Operational Dataset changes
+ * @throws IllegalArgumentException if {@code callback} has already been registered
+ */
+ @RequiresPermission(
+ allOf = {
+ permission.ACCESS_NETWORK_STATE,
+ "android.permission.THREAD_NETWORK_PRIVILEGED"
+ })
+ public void registerOperationalDatasetCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OperationalDatasetCallback callback) {
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(callback, "callback cannot be null");
+ synchronized (mOpDatasetCallbackMapLock) {
+ if (mOpDatasetCallbackMap.containsKey(callback)) {
+ throw new IllegalArgumentException("callback has already been registered");
+ }
+ OperationalDatasetCallbackProxy callbackProxy =
+ new OperationalDatasetCallbackProxy(executor, callback);
+ mOpDatasetCallbackMap.put(callback, callbackProxy);
+
+ try {
+ mControllerService.registerOperationalDatasetCallback(callbackProxy);
+ } catch (RemoteException e) {
+ mOpDatasetCallbackMap.remove(callback);
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Unregisters the Thread Operational Dataset callback.
+ *
+ * @param callback the callback which has been registered with {@link
+ * #registerOperationalDatasetCallback}
+ * @throws IllegalArgumentException if {@code callback} hasn't been registered
+ */
+ @RequiresPermission(
+ allOf = {
+ permission.ACCESS_NETWORK_STATE,
+ "android.permission.THREAD_NETWORK_PRIVILEGED"
+ })
+ public void unregisterOperationalDatasetCallback(@NonNull OperationalDatasetCallback callback) {
+ requireNonNull(callback, "callback cannot be null");
+ synchronized (mOpDatasetCallbackMapLock) {
+ OperationalDatasetCallbackProxy callbackProxy = mOpDatasetCallbackMap.get(callback);
+ if (callbackProxy == null) {
+ throw new IllegalArgumentException("callback hasn't been registered");
+ }
+ try {
+ mControllerService.unregisterOperationalDatasetCallback(callbackProxy);
+ mOpDatasetCallbackMap.remove(callback);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Joins to a Thread network with given Active Operational Dataset.
+ *
+ * <p>This method does nothing if this device has already joined to the same network specified
+ * by {@code activeDataset}. If this device has already joined to a different network, this
+ * device will first leave from that network and then join the new network. This method changes
+ * only this device and all other connected devices will stay in the old network. To change the
+ * network for all connected devices together, use {@link #scheduleMigration}.
+ *
+ * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called and the Dataset
+ * will be persisted on this device; this device will try to attach to the Thread network and
+ * the state changes can be observed by {@link #registerStateCallback}. On failure, {@link
+ * OutcomeReceiver#onError} of {@code receiver} will be invoked with a specific error:
+ *
+ * <ul>
+ * <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_CHANNEL} {@code activeDataset}
+ * specifies a channel which is not supported in the current country or region; the {@code
+ * activeDataset} is rejected and not persisted so this device won't auto re-join the next
+ * time
+ * <li>{@link ThreadNetworkException#ERROR_ABORTED} this operation is aborted by another
+ * {@code join} or {@code leave} operation
+ * </ul>
+ *
+ * @param activeDataset the Active Operational Dataset represents the Thread network to join
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive result of this operation
+ */
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public void join(
+ @NonNull ActiveOperationalDataset activeDataset,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ requireNonNull(activeDataset, "activeDataset cannot be null");
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(receiver, "receiver cannot be null");
+ try {
+ mControllerService.join(activeDataset, new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Schedules a network migration which moves all devices in the current connected network to a
+ * new network or updates parameters of the current connected network.
+ *
+ * <p>The migration doesn't happen immediately but is registered to the Leader device so that
+ * all devices in the current Thread network can be scheduled to apply the new dataset together.
+ *
+ * <p>On success, the Pending Dataset is successfully registered and persisted on the Leader and
+ * {@link OutcomeReceiver#onResult} of {@code receiver} will be called; Operational Dataset
+ * changes will be asynchronously delivered via {@link OperationalDatasetCallback} if a callback
+ * has been registered with {@link #registerOperationalDatasetCallback}. When failed, {@link
+ * OutcomeReceiver#onError} will be called with a specific error:
+ *
+ * <ul>
+ * <li>{@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} the migration is rejected
+ * because this device is not attached
+ * <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_CHANNEL} {@code pendingDataset}
+ * specifies a channel which is not supported in the current country or region; the {@code
+ * pendingDataset} is rejected and not persisted
+ * <li>{@link ThreadNetworkException#ERROR_REJECTED_BY_PEER} the Pending Dataset is rejected
+ * by the Leader device
+ * <li>{@link ThreadNetworkException#ERROR_BUSY} another {@code scheduleMigration} request is
+ * being processed
+ * <li>{@link ThreadNetworkException#ERROR_TIMEOUT} response from the Leader device hasn't
+ * been received before deadline
+ * </ul>
+ *
+ * <p>The Delay Timer of {@code pendingDataset} can vary from several minutes to a few days.
+ * It's important to select a proper value to safely migrate all devices in the network without
+ * leaving sleepy end devices orphaned. Apps are not suggested to specify the Delay Timer value
+ * if it's unclear how long it can take to propagate the {@code pendingDataset} to the whole
+ * network. Instead, use {@link Duration#ZERO} to use the default value suggested by the system.
+ *
+ * @param pendingDataset the Pending Operational Dataset
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive result of this operation
+ */
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public void scheduleMigration(
+ @NonNull PendingOperationalDataset pendingDataset,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ requireNonNull(pendingDataset, "pendingDataset cannot be null");
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(receiver, "receiver cannot be null");
+ try {
+ mControllerService.scheduleMigration(
+ pendingDataset, new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Leaves from the Thread network.
+ *
+ * <p>This undoes a {@link join} operation. On success, this device is disconnected from the
+ * joined network and will not automatically join a network before {@link #join} is called
+ * again. Active and Pending Operational Dataset configured and persisted on this device will be
+ * removed too.
+ *
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive result of this operation
+ */
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public void leave(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(receiver, "receiver cannot be null");
+ try {
+ mControllerService.leave(new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private static <T> void propagateError(
+ Executor executor,
+ OutcomeReceiver<T, ThreadNetworkException> receiver,
+ int errorCode,
+ String errorMsg) {
+ executor.execute(() -> receiver.onError(new ThreadNetworkException(errorCode, errorMsg)));
+ }
+
+ private static final class ActiveDatasetReceiverProxy
+ extends IActiveOperationalDatasetReceiver.Stub {
+ final Executor mExecutor;
+ final OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> mResultReceiver;
+
+ ActiveDatasetReceiverProxy(
+ @CallbackExecutor Executor executor,
+ OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> resultReceiver) {
+ this.mExecutor = executor;
+ this.mResultReceiver = resultReceiver;
+ }
+
+ @Override
+ public void onSuccess(ActiveOperationalDataset dataset) {
+ mExecutor.execute(() -> mResultReceiver.onResult(dataset));
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ propagateError(mExecutor, mResultReceiver, errorCode, errorMessage);
+ }
+ }
+
+ private static final class OperationReceiverProxy extends IOperationReceiver.Stub {
+ final Executor mExecutor;
+ final OutcomeReceiver<Void, ThreadNetworkException> mResultReceiver;
+
+ OperationReceiverProxy(
+ @CallbackExecutor Executor executor,
+ OutcomeReceiver<Void, ThreadNetworkException> resultReceiver) {
+ this.mExecutor = executor;
+ this.mResultReceiver = resultReceiver;
+ }
+
+ @Override
+ public void onSuccess() {
+ mExecutor.execute(() -> mResultReceiver.onResult(null));
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ propagateError(mExecutor, mResultReceiver, errorCode, errorMessage);
+ }
+ }
}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java
new file mode 100644
index 0000000..c5e1e97
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -0,0 +1,137 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represents a Thread network specific failure.
+ *
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@SystemApi
+public class ThreadNetworkException extends Exception {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ ERROR_INTERNAL_ERROR,
+ ERROR_ABORTED,
+ ERROR_TIMEOUT,
+ ERROR_UNAVAILABLE,
+ ERROR_BUSY,
+ ERROR_FAILED_PRECONDITION,
+ ERROR_UNSUPPORTED_CHANNEL,
+ ERROR_REJECTED_BY_PEER,
+ ERROR_RESPONSE_BAD_FORMAT,
+ ERROR_RESOURCE_EXHAUSTED,
+ })
+ public @interface ErrorCode {}
+
+ /**
+ * The operation failed because some invariants expected by the underlying system have been
+ * broken. This error code is reserved for serious errors. The caller can do nothing to recover
+ * from this error. A bugreport should be created and sent to the Android community if this
+ * error is ever returned.
+ */
+ public static final int ERROR_INTERNAL_ERROR = 1;
+
+ /**
+ * The operation failed because concurrent operations are overriding this one. Retrying an
+ * aborted operation has the risk of aborting another ongoing operation again. So the caller
+ * should retry at a higher level where it knows there won't be race conditions.
+ */
+ public static final int ERROR_ABORTED = 2;
+
+ /**
+ * The operation failed because a deadline expired before the operation could complete. This may
+ * be caused by connectivity unavailability and the caller can retry the same operation when the
+ * connectivity issue is fixed.
+ */
+ public static final int ERROR_TIMEOUT = 3;
+
+ /**
+ * The operation failed because the service is currently unavailable and that this is most
+ * likely a transient condition. The caller can recover from this error by retrying with a
+ * back-off scheme. Note that it is not always safe to retry non-idempotent operations.
+ */
+ public static final int ERROR_UNAVAILABLE = 4;
+
+ /**
+ * The operation failed because this device is currently busy processing concurrent requests.
+ * The caller may recover from this error when the current operations has been finished.
+ */
+ public static final int ERROR_BUSY = 5;
+
+ /**
+ * The operation failed because required preconditions were not satisfied. For example, trying
+ * to schedule a network migration when this device is not attached will receive this error. The
+ * caller should not retry the same operation before the precondition is satisfied.
+ */
+ public static final int ERROR_FAILED_PRECONDITION = 6;
+
+ /**
+ * The operation was rejected because the specified channel is currently not supported by this
+ * device in this country. For example, trying to join or migrate to a network with channel
+ * which is not supported. The caller should should change the channel or return an error to the
+ * user if the channel cannot be changed.
+ */
+ public static final int ERROR_UNSUPPORTED_CHANNEL = 7;
+
+ /**
+ * The operation failed because a request is rejected by the peer device. This happens because
+ * the peer device is not capable of processing the request, or a request from another device
+ * has already been accepted by the peer device. The caller may not be able to recover from this
+ * error by retrying the same operation.
+ */
+ public static final int ERROR_REJECTED_BY_PEER = 8;
+
+ /**
+ * The operation failed because the received response is malformed. This is typically because
+ * the peer device is misbehaving. The caller may only recover from this error by retrying with
+ * a different peer device.
+ */
+ public static final int ERROR_RESPONSE_BAD_FORMAT = 9;
+
+ /**
+ * The operation failed because some resource has been exhausted. For example, no enough
+ * allocated memory buffers, or maximum number of supported operations has been exceeded. The
+ * caller may retry and recover from this error when the resource has been freed.
+ */
+ public static final int ERROR_RESOURCE_EXHAUSTED = 10;
+
+ private final int mErrorCode;
+
+ /** Creates a new {@link ThreadNetworkException} object with given error code and message. */
+ public ThreadNetworkException(@ErrorCode int errorCode, @NonNull String errorMessage) {
+ super(requireNonNull(errorMessage, "errorMessage cannot be null"));
+ this.mErrorCode = errorCode;
+ }
+
+ /** Returns the error code. */
+ public @ErrorCode int getErrorCode() {
+ return mErrorCode;
+ }
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
index c3bdbd7..28012a7 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkManager.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -66,6 +66,19 @@
*/
public static final String FEATURE_NAME = "android.hardware.thread_network";
+ /**
+ * Permission allows changing Thread network state and access to Thread network credentials such
+ * as Network Key and PSKc.
+ *
+ * <p>This is the same value as android.Manifest.permission.THREAD_NETWORK_PRIVILEGED. That
+ * symbol is not available on U while this feature needs to support Android U TV devices, so
+ * here is making a copy of android.Manifest.permission.THREAD_NETWORK_PRIVILEGED.
+ *
+ * @hide
+ */
+ public static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
+ "android.permission.THREAD_NETWORK_PRIVILEGED";
+
@NonNull private final Context mContext;
@NonNull private final List<ThreadNetworkController> mUnmodifiableControllerServices;
diff --git a/thread/service/Android.bp b/thread/service/Android.bp
index fcf9521..bd265e6 100644
--- a/thread/service/Android.bp
+++ b/thread/service/Android.bp
@@ -33,11 +33,35 @@
min_sdk_version: "30",
srcs: [":service-thread-sources"],
libs: [
+ "framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
"service-connectivity-pre-jarjar",
],
static_libs: [
"net-utils-device-common",
+ "net-utils-device-common-netlink",
+ "ot-daemon-aidl-java",
+ ],
+ apex_available: ["com.android.tethering"],
+}
+
+cc_library_shared {
+ name: "libservice-thread-jni",
+ min_sdk_version: "30",
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ "-Wthread-safety",
+ ],
+ srcs: [
+ "jni/**/*.cpp",
+ ],
+ shared_libs: [
+ "libbase",
+ "libcutils",
+ "liblog",
+ "libnativehelper",
],
apex_available: ["com.android.tethering"],
}
diff --git a/thread/service/java/com/android/server/thread/InfraInterfaceController.java b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
new file mode 100644
index 0000000..d7c49a0
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
@@ -0,0 +1,45 @@
+/*
+ * 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.thread;
+
+import android.os.ParcelFileDescriptor;
+
+import java.io.IOException;
+
+/** Controller for the infrastructure network interface. */
+public class InfraInterfaceController {
+ private static final String TAG = "InfraIfController";
+
+ static {
+ System.loadLibrary("service-thread-jni");
+ }
+
+ /**
+ * Creates a socket on the infrastructure network interface for sending/receiving ICMPv6
+ * Neighbor Discovery messages.
+ *
+ * @param infraInterfaceName the infrastructure network interface name.
+ * @return an ICMPv6 socket file descriptor on the Infrastructure network interface.
+ * @throws IOException when fails to create the socket.
+ */
+ public static ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName)
+ throws IOException {
+ return ParcelFileDescriptor.adoptFd(nativeCreateIcmp6Socket(infraInterfaceName));
+ }
+
+ private static native int nativeCreateIcmp6Socket(String interfaceName) throws IOException;
+}
diff --git a/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java b/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java
new file mode 100644
index 0000000..a8909bc
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java
@@ -0,0 +1,82 @@
+/*
+ * 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.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
+
+import android.net.thread.IOperationReceiver;
+import android.os.RemoteException;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/** A {@link IOperationReceiver} wrapper which makes it easier to invoke the callbacks. */
+final class OperationReceiverWrapper {
+ private final IOperationReceiver mReceiver;
+
+ private static final Object sPendingReceiversLock = new Object();
+
+ @GuardedBy("sPendingReceiversLock")
+ private static final Set<OperationReceiverWrapper> sPendingReceivers = new HashSet<>();
+
+ public OperationReceiverWrapper(IOperationReceiver receiver) {
+ this.mReceiver = receiver;
+
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.add(this);
+ }
+ }
+
+ public static void onOtDaemonDied() {
+ synchronized (sPendingReceiversLock) {
+ for (OperationReceiverWrapper receiver : sPendingReceivers) {
+ try {
+ receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+ sPendingReceivers.clear();
+ }
+ }
+
+ public void onSuccess() {
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.remove(this);
+ }
+
+ try {
+ mReceiver.onSuccess();
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+
+ public void onError(int errorCode, String errorMessage, Object... messageArgs) {
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.remove(this);
+ }
+
+ try {
+ mReceiver.onError(errorCode, String.format(errorMessage, messageArgs));
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index e8b95bc..33516aa 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -1,31 +1,844 @@
/*
* 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
+ * 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
+ * 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.
+ * 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.thread;
+import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
+import static android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID;
+import static android.net.thread.ActiveOperationalDataset.LENGTH_MESH_LOCAL_PREFIX_BITS;
+import static android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY;
+import static android.net.thread.ActiveOperationalDataset.LENGTH_PSKC;
+import static android.net.thread.ActiveOperationalDataset.MESH_LOCAL_PREFIX_FIRST_BYTE;
+import static android.net.thread.ActiveOperationalDataset.SecurityPolicy.DEFAULT_ROTATION_TIME_HOURS;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
+import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
+import static android.net.thread.ThreadNetworkException.ERROR_BUSY;
+import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
+import static android.net.thread.ThreadNetworkException.ERROR_RESOURCE_EXHAUSTED;
+import static android.net.thread.ThreadNetworkException.ERROR_RESPONSE_BAD_FORMAT;
+import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
+import static android.net.thread.ThreadNetworkException.ErrorCode;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_BUSY;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_DETACHED;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_NO_BUFS;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_PARSE;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_REASSEMBLY_TIMEOUT;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_REJECTED;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_RESPONSE_TIMEOUT;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_UNSUPPORTED_CHANNEL;
+import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME;
+
+import android.Manifest.permission;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkScore;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
+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.OperationalDatasetTimestamp;
+import android.net.thread.PendingOperationalDataset;
import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkController.DeviceRole;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.SparseArray;
-/** Implementation of the {@link ThreadNetworkController} API. */
-public final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ServiceManagerWrapper;
+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 java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Random;
+import java.util.function.Supplier;
+
+/**
+ * Implementation of the {@link ThreadNetworkController} API.
+ *
+ * <p>Threading model: This class is not Thread-safe and should only be accessed from the
+ * ThreadNetworkService class. Additional attention should be paid to handle the threading code
+ * correctly: 1. All member fields other than `mHandler` and `mContext` MUST be accessed from
+ * `mHandlerThread` 2. In the @Override methods, the actual work MUST be dispatched to the
+ * HandlerThread except for arguments or permissions checking
+ */
+final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
+ private static final String TAG = "ThreadNetworkService";
+
+ // Below member fields can be accessed from both the binder and handler threads
+
+ private final Context mContext;
+ private final Handler mHandler;
+
+ // Below member fields can only be accessed from the handler thread (`mHandlerThread`). In
+ // particular, the constructor does not run on the handler thread, so it must not touch any of
+ // the non-final fields, nor must it mutate any of the non-final fields inside these objects.
+
+ private final HandlerThread mHandlerThread;
+ private final NetworkProvider mNetworkProvider;
+ private final Supplier<IOtDaemon> mOtDaemonSupplier;
+ private final ConnectivityManager mConnectivityManager;
+ private final TunInterfaceController mTunIfController;
+ private final LinkProperties mLinkProperties = new LinkProperties();
+ private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
+
+ // TODO(b/308310823): read supported channel from Thread dameon
+ private final int mSupportedChannelMask = 0x07FFF800; // from channel 11 to 26
+
+ private IOtDaemon mOtDaemon;
+ private NetworkAgent mNetworkAgent;
+
+ @VisibleForTesting
+ ThreadNetworkControllerService(
+ Context context,
+ HandlerThread handlerThread,
+ NetworkProvider networkProvider,
+ Supplier<IOtDaemon> otDaemonSupplier,
+ ConnectivityManager connectivityManager,
+ TunInterfaceController tunIfController) {
+ mContext = context;
+ mHandlerThread = handlerThread;
+ mHandler = new Handler(handlerThread.getLooper());
+ mNetworkProvider = networkProvider;
+ mOtDaemonSupplier = otDaemonSupplier;
+ mConnectivityManager = connectivityManager;
+ mTunIfController = tunIfController;
+ }
+
+ public static ThreadNetworkControllerService newInstance(Context context) {
+ HandlerThread handlerThread = new HandlerThread("ThreadHandlerThread");
+ handlerThread.start();
+ NetworkProvider networkProvider =
+ new NetworkProvider(context, handlerThread.getLooper(), "ThreadNetworkProvider");
+
+ return new ThreadNetworkControllerService(
+ context,
+ handlerThread,
+ networkProvider,
+ () -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
+ context.getSystemService(ConnectivityManager.class),
+ new TunInterfaceController(TUN_IF_NAME));
+ }
+
+ private static NetworkCapabilities newNetworkCapabilities() {
+ return new NetworkCapabilities.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build();
+ }
+
+ private static InetAddress addressInfoToInetAddress(Ipv6AddressInfo addressInfo) {
+ try {
+ return InetAddress.getByAddress(addressInfo.address);
+ } catch (UnknownHostException e) {
+ // This is impossible unless the Thread daemon is critically broken
+ return null;
+ }
+ }
+
+ private static LinkAddress newLinkAddress(Ipv6AddressInfo addressInfo) {
+ long deprecationTimeMillis =
+ addressInfo.isPreferred
+ ? LinkAddress.LIFETIME_PERMANENT
+ : SystemClock.elapsedRealtime();
+
+ InetAddress address = addressInfoToInetAddress(addressInfo);
+
+ // flags and scope will be adjusted automatically depending on the address and
+ // its lifetimes.
+ return new LinkAddress(
+ address,
+ addressInfo.prefixLength,
+ 0 /* flags */,
+ 0 /* scope */,
+ deprecationTimeMillis,
+ LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
+ }
+
+ private void initializeOtDaemon() {
+ try {
+ getOtDaemon();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to initialize ot-daemon");
+ }
+ }
+
+ private IOtDaemon getOtDaemon() throws RemoteException {
+ if (mOtDaemon != null) {
+ return mOtDaemon;
+ }
+
+ IOtDaemon otDaemon = mOtDaemonSupplier.get();
+ if (otDaemon == null) {
+ throw new RemoteException("Internal error: failed to start OT daemon");
+ }
+ otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
+ otDaemon.initialize(mTunIfController.getTunFd());
+ otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
+ mOtDaemon = otDaemon;
+ return mOtDaemon;
+ }
+
+ // TODO(b/309792480): restarts the OT daemon service
+ private void onOtDaemonDied() {
+ Log.w(TAG, "OT daemon became dead, clean up...");
+ OperationReceiverWrapper.onOtDaemonDied();
+ mOtDaemonCallbackProxy.onOtDaemonDied();
+ mOtDaemon = null;
+ }
+
+ public void initialize() {
+ mHandler.post(
+ () -> {
+ Log.d(TAG, "Initializing Thread system service...");
+ try {
+ mTunIfController.createTunInterface();
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Failed to create Thread tunnel interface", e);
+ }
+ mLinkProperties.setInterfaceName(TUN_IF_NAME);
+ mLinkProperties.setMtu(TunInterfaceController.MTU);
+ mConnectivityManager.registerNetworkProvider(mNetworkProvider);
+
+ initializeOtDaemon();
+ });
+ }
+
+ private void registerThreadNetwork() {
+ if (mNetworkAgent != null) {
+ return;
+ }
+ NetworkCapabilities netCaps = newNetworkCapabilities();
+ NetworkScore score =
+ new NetworkScore.Builder()
+ .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK)
+ .build();
+ mNetworkAgent =
+ new NetworkAgent(
+ mContext,
+ mHandlerThread.getLooper(),
+ TAG,
+ netCaps,
+ mLinkProperties,
+ score,
+ new NetworkAgentConfig.Builder().build(),
+ mNetworkProvider) {};
+ mNetworkAgent.register();
+ mNetworkAgent.markConnected();
+ Log.i(TAG, "Registered Thread network");
+ }
+
+ private void unregisterThreadNetwork() {
+ if (mNetworkAgent == null) {
+ // unregisterThreadNetwork can be called every time this device becomes detached or
+ // disabled and the mNetworkAgent may not be created in this cases
+ return;
+ }
+
+ Log.d(TAG, "Unregistering Thread network agent");
+
+ mNetworkAgent.unregister();
+ mNetworkAgent = null;
+ }
+
+ private void updateTunInterfaceAddress(LinkAddress linkAddress, boolean isAdded) {
+ try {
+ if (isAdded) {
+ mTunIfController.addAddress(linkAddress);
+ } else {
+ mTunIfController.removeAddress(linkAddress);
+ }
+ } catch (IOException e) {
+ Log.e(
+ TAG,
+ String.format(
+ "Failed to %s Thread tun interface address %s",
+ (isAdded ? "add" : "remove"), linkAddress),
+ e);
+ }
+ }
+
+ private void updateNetworkLinkProperties(LinkAddress linkAddress, boolean isAdded) {
+ if (isAdded) {
+ mLinkProperties.addLinkAddress(linkAddress);
+ } else {
+ mLinkProperties.removeLinkAddress(linkAddress);
+ }
+
+ // The Thread daemon can send link property updates before the networkAgent is
+ // registered
+ if (mNetworkAgent != null) {
+ mNetworkAgent.sendLinkProperties(mLinkProperties);
+ }
+ }
@Override
public int getThreadVersion() {
return THREAD_VERSION_1_3;
}
+
+ @Override
+ public void createRandomizedDataset(
+ String networkName, IActiveOperationalDatasetReceiver receiver) {
+ mHandler.post(
+ () -> {
+ ActiveOperationalDataset dataset =
+ createRandomizedDatasetInternal(
+ networkName,
+ mSupportedChannelMask,
+ Instant.now(),
+ new Random(),
+ new SecureRandom());
+ try {
+ receiver.onSuccess(dataset);
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ });
+ }
+
+ private static ActiveOperationalDataset createRandomizedDatasetInternal(
+ String networkName,
+ int supportedChannelMask,
+ Instant now,
+ Random random,
+ SecureRandom secureRandom) {
+ int panId = random.nextInt(/* bound= */ 0xffff);
+ final byte[] meshLocalPrefix = newRandomBytes(random, LENGTH_MESH_LOCAL_PREFIX_BITS / 8);
+ meshLocalPrefix[0] = MESH_LOCAL_PREFIX_FIRST_BYTE;
+
+ final SparseArray<byte[]> channelMask = new SparseArray<>(1);
+ channelMask.put(CHANNEL_PAGE_24_GHZ, channelMaskToByteArray(supportedChannelMask));
+
+ final byte[] securityFlags = new byte[] {(byte) 0xff, (byte) 0xf8};
+
+ return new ActiveOperationalDataset.Builder()
+ .setActiveTimestamp(
+ new OperationalDatasetTimestamp(
+ now.getEpochSecond() & 0xffffffffffffL, 0, false))
+ .setExtendedPanId(newRandomBytes(random, LENGTH_EXTENDED_PAN_ID))
+ .setPanId(panId)
+ .setNetworkName(networkName)
+ .setChannel(CHANNEL_PAGE_24_GHZ, selectRandomChannel(supportedChannelMask, random))
+ .setChannelMask(channelMask)
+ .setPskc(newRandomBytes(secureRandom, LENGTH_PSKC))
+ .setNetworkKey(newRandomBytes(secureRandom, LENGTH_NETWORK_KEY))
+ .setMeshLocalPrefix(meshLocalPrefix)
+ .setSecurityPolicy(new SecurityPolicy(DEFAULT_ROTATION_TIME_HOURS, securityFlags))
+ .build();
+ }
+
+ private static byte[] newRandomBytes(Random random, int length) {
+ byte[] result = new byte[length];
+ random.nextBytes(result);
+ return result;
+ }
+
+ private static byte[] channelMaskToByteArray(int channelMask) {
+ // Per Thread spec, a Channel Mask is:
+ // A variable-length bit mask that identifies the channels within the channel page
+ // (1 = selected, 0 = unselected). The channels are represented in most significant bit
+ // order. For example, the most significant bit of the left-most byte indicates channel 0.
+ // If channel 0 and channel 10 are selected, the mask would be: 80 20 00 00. For IEEE
+ // 802.15.4-2006 2.4 GHz PHY, the ChannelMask is 27 bits and MaskLength is 4.
+ //
+ // The pass-in channelMask represents a channel K by (channelMask & (1 << K)), so here
+ // needs to do bit-wise reverse to convert it to the Thread spec format in bytes.
+ channelMask = Integer.reverse(channelMask);
+ return new byte[] {
+ (byte) (channelMask >>> 24),
+ (byte) (channelMask >>> 16),
+ (byte) (channelMask >>> 8),
+ (byte) channelMask
+ };
+ }
+
+ private static int selectRandomChannel(int supportedChannelMask, Random random) {
+ int num = random.nextInt(Integer.bitCount(supportedChannelMask));
+ for (int i = 0; i < 32; i++) {
+ if ((supportedChannelMask & 1) == 1 && (num--) == 0) {
+ return i;
+ }
+ supportedChannelMask >>>= 1;
+ }
+ return -1;
+ }
+
+ private void enforceAllCallingPermissionsGranted(String... permissions) {
+ for (String permission : permissions) {
+ mContext.enforceCallingPermission(
+ permission, "Permission " + permission + " is missing");
+ }
+ }
+
+ @Override
+ public void registerStateCallback(IStateCallback stateCallback) throws RemoteException {
+ enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+ mHandler.post(() -> mOtDaemonCallbackProxy.registerStateCallback(stateCallback));
+ }
+
+ @Override
+ public void unregisterStateCallback(IStateCallback stateCallback) throws RemoteException {
+ enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+ mHandler.post(() -> mOtDaemonCallbackProxy.unregisterStateCallback(stateCallback));
+ }
+
+ @Override
+ public void registerOperationalDatasetCallback(IOperationalDatasetCallback callback)
+ throws RemoteException {
+ enforceAllCallingPermissionsGranted(
+ permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ mHandler.post(() -> mOtDaemonCallbackProxy.registerDatasetCallback(callback));
+ }
+
+ @Override
+ public void unregisterOperationalDatasetCallback(IOperationalDatasetCallback callback)
+ throws RemoteException {
+ enforceAllCallingPermissionsGranted(
+ permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ mHandler.post(() -> mOtDaemonCallbackProxy.unregisterDatasetCallback(callback));
+ }
+
+ private void checkOnHandlerThread() {
+ if (Looper.myLooper() != mHandlerThread.getLooper()) {
+ Log.wtf(TAG, "Must be on the handler thread!");
+ }
+ }
+
+ private IOtStatusReceiver newOtStatusReceiver(OperationReceiverWrapper receiver) {
+ return new IOtStatusReceiver.Stub() {
+ @Override
+ public void onSuccess() {
+ receiver.onSuccess();
+ }
+
+ @Override
+ public void onError(int otError, String message) {
+ receiver.onError(otErrorToAndroidError(otError), message);
+ }
+ };
+ }
+
+ @ErrorCode
+ private static int otErrorToAndroidError(int otError) {
+ // See external/openthread/include/openthread/error.h for OT error definition
+ switch (otError) {
+ case OT_ERROR_ABORT:
+ return ERROR_ABORTED;
+ case OT_ERROR_BUSY:
+ return ERROR_BUSY;
+ case OT_ERROR_DETACHED:
+ case OT_ERROR_INVALID_STATE:
+ return ERROR_FAILED_PRECONDITION;
+ case OT_ERROR_NO_BUFS:
+ return ERROR_RESOURCE_EXHAUSTED;
+ case OT_ERROR_PARSE:
+ return ERROR_RESPONSE_BAD_FORMAT;
+ case OT_ERROR_REASSEMBLY_TIMEOUT:
+ case OT_ERROR_RESPONSE_TIMEOUT:
+ return ERROR_TIMEOUT;
+ case OT_ERROR_REJECTED:
+ return ERROR_REJECTED_BY_PEER;
+ case OT_ERROR_UNSUPPORTED_CHANNEL:
+ return ERROR_UNSUPPORTED_CHANNEL;
+ default:
+ return ERROR_INTERNAL_ERROR;
+ }
+ }
+
+ @Override
+ public void join(
+ @NonNull ActiveOperationalDataset activeDataset, @NonNull IOperationReceiver receiver) {
+ enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
+ mHandler.post(() -> joinInternal(activeDataset, receiverWrapper));
+ }
+
+ private void joinInternal(
+ @NonNull ActiveOperationalDataset activeDataset,
+ @NonNull OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ // The otDaemon.join() will leave first if this device is currently attached
+ getOtDaemon().join(activeDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
+ } catch (RemoteException e) {
+ Log.e(TAG, "otDaemon.join failed", e);
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ }
+ }
+
+ @Override
+ public void scheduleMigration(
+ @NonNull PendingOperationalDataset pendingDataset,
+ @NonNull IOperationReceiver receiver) {
+ enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
+ mHandler.post(() -> scheduleMigrationInternal(pendingDataset, receiverWrapper));
+ }
+
+ public void scheduleMigrationInternal(
+ @NonNull PendingOperationalDataset pendingDataset,
+ @NonNull OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon()
+ .scheduleMigration(
+ pendingDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
+ } catch (RemoteException e) {
+ Log.e(TAG, "otDaemon.scheduleMigration failed", e);
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ }
+ }
+
+ @Override
+ public void leave(@NonNull IOperationReceiver receiver) throws RemoteException {
+ enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ mHandler.post(() -> leaveInternal(new OperationReceiverWrapper(receiver)));
+ }
+
+ private void leaveInternal(@NonNull OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().leave(newOtStatusReceiver(receiver));
+ } catch (RemoteException e) {
+ // Oneway AIDL API should never throw?
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ }
+ }
+
+ private void handleThreadInterfaceStateChanged(boolean isUp) {
+ try {
+ mTunIfController.setInterfaceUp(isUp);
+ Log.d(TAG, "Thread network interface becomes " + (isUp ? "up" : "down"));
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to handle Thread interface state changes", e);
+ }
+ }
+
+ private void handleDeviceRoleChanged(@DeviceRole int deviceRole) {
+ if (ThreadNetworkController.isAttached(deviceRole)) {
+ Log.d(TAG, "Attached to the Thread network");
+
+ // This is an idempotent method which can be called for multiple times when the device
+ // is already attached (e.g. going from Child to Router)
+ registerThreadNetwork();
+ } else {
+ Log.d(TAG, "Detached from the Thread network");
+
+ // This is an idempotent method which can be called for multiple times when the device
+ // is already detached or stopped
+ unregisterThreadNetwork();
+ }
+ }
+
+ private void handleAddressChanged(Ipv6AddressInfo addressInfo, boolean isAdded) {
+ checkOnHandlerThread();
+ InetAddress address = addressInfoToInetAddress(addressInfo);
+ if (address.isMulticastAddress()) {
+ Log.i(TAG, "Ignoring multicast address " + address.getHostAddress());
+ return;
+ }
+
+ LinkAddress linkAddress = newLinkAddress(addressInfo);
+ Log.d(TAG, (isAdded ? "Adding" : "Removing") + " address " + linkAddress);
+
+ updateTunInterfaceAddress(linkAddress, isAdded);
+ updateNetworkLinkProperties(linkAddress, isAdded);
+ }
+
+ private static final class CallbackMetadata {
+ private static long gId = 0;
+
+ // The unique ID
+ final long id;
+
+ final IBinder.DeathRecipient deathRecipient;
+
+ CallbackMetadata(IBinder.DeathRecipient deathRecipient) {
+ this.id = allocId();
+ this.deathRecipient = deathRecipient;
+ }
+
+ private static long allocId() {
+ if (gId == Long.MAX_VALUE) {
+ gId = 0;
+ }
+ return gId++;
+ }
+ }
+
+ /**
+ * Handles and forwards Thread daemon callbacks. This class must be accessed from the {@code
+ * mHandlerThread}.
+ */
+ private final class OtDaemonCallbackProxy extends IOtDaemonCallback.Stub {
+ private final Map<IStateCallback, CallbackMetadata> mStateCallbacks = new HashMap<>();
+ private final Map<IOperationalDatasetCallback, CallbackMetadata> mOpDatasetCallbacks =
+ new HashMap<>();
+
+ private OtDaemonState mState;
+ private ActiveOperationalDataset mActiveDataset;
+ private PendingOperationalDataset mPendingDataset;
+
+ public void registerStateCallback(IStateCallback callback) {
+ checkOnHandlerThread();
+ if (mStateCallbacks.containsKey(callback)) {
+ throw new IllegalStateException("Registering the same IStateCallback twice");
+ }
+
+ IBinder.DeathRecipient deathRecipient =
+ () -> mHandler.post(() -> unregisterStateCallback(callback));
+ CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
+ mStateCallbacks.put(callback, callbackMetadata);
+ try {
+ callback.asBinder().linkToDeath(deathRecipient, 0);
+ } catch (RemoteException e) {
+ mStateCallbacks.remove(callback);
+ // This is thrown when the client is dead, do nothing
+ }
+
+ try {
+ getOtDaemon().registerStateCallback(this, callbackMetadata.id);
+ } catch (RemoteException e) {
+ // oneway operation should never fail
+ }
+ }
+
+ public void unregisterStateCallback(IStateCallback callback) {
+ checkOnHandlerThread();
+ if (!mStateCallbacks.containsKey(callback)) {
+ return;
+ }
+ callback.asBinder().unlinkToDeath(mStateCallbacks.remove(callback).deathRecipient, 0);
+ }
+
+ public void registerDatasetCallback(IOperationalDatasetCallback callback) {
+ checkOnHandlerThread();
+ if (mOpDatasetCallbacks.containsKey(callback)) {
+ throw new IllegalStateException(
+ "Registering the same IOperationalDatasetCallback twice");
+ }
+
+ IBinder.DeathRecipient deathRecipient =
+ () -> mHandler.post(() -> unregisterDatasetCallback(callback));
+ CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
+ mOpDatasetCallbacks.put(callback, callbackMetadata);
+ try {
+ callback.asBinder().linkToDeath(deathRecipient, 0);
+ } catch (RemoteException e) {
+ mOpDatasetCallbacks.remove(callback);
+ }
+
+ try {
+ getOtDaemon().registerStateCallback(this, callbackMetadata.id);
+ } catch (RemoteException e) {
+ // oneway operation should never fail
+ }
+ }
+
+ public void unregisterDatasetCallback(IOperationalDatasetCallback callback) {
+ checkOnHandlerThread();
+ if (!mOpDatasetCallbacks.containsKey(callback)) {
+ return;
+ }
+ callback.asBinder()
+ .unlinkToDeath(mOpDatasetCallbacks.remove(callback).deathRecipient, 0);
+ }
+
+ public void onOtDaemonDied() {
+ checkOnHandlerThread();
+ if (mState == null) {
+ return;
+ }
+
+ // If this device is already STOPPED or DETACHED, do nothing
+ if (!ThreadNetworkController.isAttached(mState.deviceRole)) {
+ return;
+ }
+
+ // The Thread device role is considered DETACHED when the OT daemon process is dead
+ handleDeviceRoleChanged(DEVICE_ROLE_DETACHED);
+ for (IStateCallback callback : mStateCallbacks.keySet()) {
+ try {
+ callback.onDeviceRoleChanged(DEVICE_ROLE_DETACHED);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ }
+
+ @Override
+ public void onStateChanged(OtDaemonState newState, long listenerId) {
+ mHandler.post(() -> onStateChangedInternal(newState, listenerId));
+ }
+
+ private void onStateChangedInternal(OtDaemonState newState, long listenerId) {
+ checkOnHandlerThread();
+ onInterfaceStateChanged(newState.isInterfaceUp);
+ onDeviceRoleChanged(newState.deviceRole, listenerId);
+ onPartitionIdChanged(newState.partitionId, listenerId);
+ mState = newState;
+
+ ActiveOperationalDataset newActiveDataset;
+ try {
+ if (newState.activeDatasetTlvs.length != 0) {
+ newActiveDataset =
+ ActiveOperationalDataset.fromThreadTlvs(newState.activeDatasetTlvs);
+ } else {
+ newActiveDataset = null;
+ }
+ onActiveOperationalDatasetChanged(newActiveDataset, listenerId);
+ mActiveDataset = newActiveDataset;
+ } catch (IllegalArgumentException e) {
+ // Is unlikely that OT will generate invalid Operational Dataset
+ Log.wtf(TAG, "Invalid Active Operational Dataset from OpenThread", e);
+ }
+
+ PendingOperationalDataset newPendingDataset;
+ try {
+ if (newState.pendingDatasetTlvs.length != 0) {
+ newPendingDataset =
+ PendingOperationalDataset.fromThreadTlvs(newState.pendingDatasetTlvs);
+ } else {
+ newPendingDataset = null;
+ }
+ onPendingOperationalDatasetChanged(newPendingDataset, listenerId);
+ mPendingDataset = newPendingDataset;
+ } catch (IllegalArgumentException e) {
+ // Is unlikely that OT will generate invalid Operational Dataset
+ Log.wtf(TAG, "Invalid Pending Operational Dataset from OpenThread", e);
+ }
+ }
+
+ private void onInterfaceStateChanged(boolean isUp) {
+ checkOnHandlerThread();
+ if (mState == null || mState.isInterfaceUp != isUp) {
+ handleThreadInterfaceStateChanged(isUp);
+ }
+ }
+
+ private void onDeviceRoleChanged(@DeviceRole int deviceRole, long listenerId) {
+ checkOnHandlerThread();
+ boolean hasChange = (mState == null || mState.deviceRole != deviceRole);
+ if (hasChange) {
+ handleDeviceRoleChanged(deviceRole);
+ }
+
+ for (var callbackEntry : mStateCallbacks.entrySet()) {
+ if (!hasChange && callbackEntry.getValue().id != listenerId) {
+ continue;
+ }
+ try {
+ callbackEntry.getKey().onDeviceRoleChanged(deviceRole);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ }
+
+ private void onPartitionIdChanged(long partitionId, long listenerId) {
+ checkOnHandlerThread();
+ boolean hasChange = (mState == null || mState.partitionId != partitionId);
+
+ for (var callbackEntry : mStateCallbacks.entrySet()) {
+ if (!hasChange && callbackEntry.getValue().id != listenerId) {
+ continue;
+ }
+ try {
+ callbackEntry.getKey().onPartitionIdChanged(partitionId);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ }
+
+ private void onActiveOperationalDatasetChanged(
+ ActiveOperationalDataset activeDataset, long listenerId) {
+ checkOnHandlerThread();
+ boolean hasChange = !Objects.equals(mActiveDataset, activeDataset);
+
+ for (var callbackEntry : mOpDatasetCallbacks.entrySet()) {
+ if (!hasChange && callbackEntry.getValue().id != listenerId) {
+ continue;
+ }
+ try {
+ callbackEntry.getKey().onActiveOperationalDatasetChanged(activeDataset);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ }
+
+ private void onPendingOperationalDatasetChanged(
+ PendingOperationalDataset pendingDataset, long listenerId) {
+ checkOnHandlerThread();
+ boolean hasChange = !Objects.equals(mPendingDataset, pendingDataset);
+ for (var callbackEntry : mOpDatasetCallbacks.entrySet()) {
+ if (!hasChange && callbackEntry.getValue().id != listenerId) {
+ continue;
+ }
+ try {
+ callbackEntry.getKey().onPendingOperationalDatasetChanged(pendingDataset);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ }
+
+ @Override
+ public void onAddressChanged(Ipv6AddressInfo addressInfo, boolean isAdded) {
+ mHandler.post(() -> handleAddressChanged(addressInfo, isAdded));
+ }
+ }
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkService.java b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
index c6d47df..cc694a1 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
@@ -16,6 +16,7 @@
package com.android.server.thread;
+import android.annotation.Nullable;
import android.content.Context;
import android.net.thread.IThreadNetworkController;
import android.net.thread.IThreadNetworkManager;
@@ -29,16 +30,12 @@
* Implementation of the Thread network service. This is the entry point of Android Thread feature.
*/
public class ThreadNetworkService extends IThreadNetworkManager.Stub {
- private final ThreadNetworkControllerService mControllerService;
+ private final Context mContext;
+ @Nullable private ThreadNetworkControllerService mControllerService;
/** Creates a new {@link ThreadNetworkService} object. */
public ThreadNetworkService(Context context) {
- this(context, new ThreadNetworkControllerService());
- }
-
- private ThreadNetworkService(
- Context context, ThreadNetworkControllerService controllerService) {
- mControllerService = controllerService;
+ mContext = context;
}
/**
@@ -48,12 +45,16 @@
*/
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_BOOT_COMPLETED) {
- // TODO: initialize ThreadNetworkManagerService
+ mControllerService = ThreadNetworkControllerService.newInstance(mContext);
+ mControllerService.initialize();
}
}
@Override
public List<IThreadNetworkController> getAllThreadNetworkControllers() {
+ if (mControllerService == null) {
+ return Collections.emptyList();
+ }
return Collections.singletonList(mControllerService);
}
}
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
new file mode 100644
index 0000000..7223b2a
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -0,0 +1,143 @@
+/*
+ * 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.thread;
+
+import android.annotation.Nullable;
+import android.net.LinkAddress;
+import android.net.util.SocketUtils;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.net.module.util.netlink.NetlinkUtils;
+import com.android.net.module.util.netlink.RtNetlinkAddressMessage;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/** Controller for virtual/tunnel network interfaces. */
+public class TunInterfaceController {
+ private static final String TAG = "TunIfController";
+ private static final long INFINITE_LIFETIME = 0xffffffffL;
+ static final int MTU = 1280;
+
+ static {
+ System.loadLibrary("service-thread-jni");
+ }
+
+ private final String mIfName;
+ private ParcelFileDescriptor mParcelTunFd;
+ private FileDescriptor mNetlinkSocket;
+ private static int sNetlinkSeqNo = 0;
+
+ /** Creates a new {@link TunInterfaceController} instance for given interface. */
+ public TunInterfaceController(String interfaceName) {
+ this.mIfName = interfaceName;
+ }
+
+ /**
+ * Creates the tunnel interface.
+ *
+ * @throws IOException if failed to create the interface
+ */
+ public void createTunInterface() throws IOException {
+ mParcelTunFd = ParcelFileDescriptor.adoptFd(nativeCreateTunInterface(mIfName, MTU));
+ try {
+ mNetlinkSocket = NetlinkUtils.netlinkSocketForProto(OsConstants.NETLINK_ROUTE);
+ } catch (ErrnoException e) {
+ throw new IOException("Failed to create netlink socket", e);
+ }
+ }
+
+ public void destroyTunInterface() {
+ try {
+ mParcelTunFd.close();
+ SocketUtils.closeSocket(mNetlinkSocket);
+ } catch (IOException e) {
+ // Should never fail
+ }
+ mParcelTunFd = null;
+ mNetlinkSocket = null;
+ }
+
+ /** Returns the FD of the tunnel interface. */
+ @Nullable
+ public ParcelFileDescriptor getTunFd() {
+ return mParcelTunFd;
+ }
+
+ private native int nativeCreateTunInterface(String interfaceName, int mtu) throws IOException;
+
+ /** Sets the interface up or down according to {@code isUp}. */
+ public void setInterfaceUp(boolean isUp) throws IOException {
+ nativeSetInterfaceUp(mIfName, isUp);
+ }
+
+ private native void nativeSetInterfaceUp(String interfaceName, boolean isUp) throws IOException;
+
+ /** Adds a new address to the interface. */
+ public void addAddress(LinkAddress address) throws IOException {
+ Log.d(TAG, "Adding address " + address + " with flags: " + address.getFlags());
+
+ long validLifetimeSeconds;
+ long preferredLifetimeSeconds;
+
+ if (address.getDeprecationTime() == LinkAddress.LIFETIME_PERMANENT
+ || address.getDeprecationTime() == LinkAddress.LIFETIME_UNKNOWN) {
+ validLifetimeSeconds = INFINITE_LIFETIME;
+ } else {
+ validLifetimeSeconds =
+ Math.max(
+ (address.getDeprecationTime() - SystemClock.elapsedRealtime()) / 1000L,
+ 0L);
+ }
+
+ if (address.getExpirationTime() == LinkAddress.LIFETIME_PERMANENT
+ || address.getExpirationTime() == LinkAddress.LIFETIME_UNKNOWN) {
+ preferredLifetimeSeconds = INFINITE_LIFETIME;
+ } else {
+ preferredLifetimeSeconds =
+ Math.max(
+ (address.getExpirationTime() - SystemClock.elapsedRealtime()) / 1000L,
+ 0L);
+ }
+
+ byte[] message =
+ RtNetlinkAddressMessage.newRtmNewAddressMessage(
+ sNetlinkSeqNo,
+ address.getAddress(),
+ (short) address.getPrefixLength(),
+ address.getFlags(),
+ (byte) address.getScope(),
+ Os.if_nametoindex(mIfName),
+ validLifetimeSeconds,
+ preferredLifetimeSeconds);
+ try {
+ Os.write(mNetlinkSocket, message, 0, message.length);
+ } catch (ErrnoException e) {
+ throw new IOException("Failed to send netlink message", e);
+ }
+ }
+
+ /** Removes an address from the interface. */
+ public void removeAddress(LinkAddress address) throws IOException {
+ // TODO(b/263222068): remove address with netlink
+ }
+}
diff --git a/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp b/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
new file mode 100644
index 0000000..5d24eab
--- /dev/null
+++ b/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
@@ -0,0 +1,141 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "jniThreadInfra"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <ifaddrs.h>
+#include <inttypes.h>
+#include <linux/if_arp.h>
+#include <linux/ioctl.h>
+#include <log/log.h>
+#include <net/if.h>
+#include <netdb.h>
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <private/android_filesystem_config.h>
+#include <signal.h>
+#include <spawn.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "jni.h"
+#include "nativehelper/JNIHelp.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+namespace android {
+static jint
+com_android_server_thread_InfraInterfaceController_createIcmp6Socket(JNIEnv *env, jobject clazz,
+ jstring interfaceName) {
+ ScopedUtfChars ifName(env, interfaceName);
+
+ struct icmp6_filter filter;
+ constexpr int kEnable = 1;
+ constexpr int kIpv6ChecksumOffset = 2;
+ constexpr int kHopLimit = 255;
+
+ // Initializes the ICMPv6 socket.
+ int sock = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
+ if (sock == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to create the socket (%s)",
+ strerror(errno));
+ return -1;
+ }
+
+ // Only accept Router Advertisements, Router Solicitations and Neighbor
+ // Advertisements.
+ ICMP6_FILTER_SETBLOCKALL(&filter);
+ ICMP6_FILTER_SETPASS(ND_ROUTER_SOLICIT, &filter);
+ ICMP6_FILTER_SETPASS(ND_ROUTER_ADVERT, &filter);
+ ICMP6_FILTER_SETPASS(ND_NEIGHBOR_ADVERT, &filter);
+
+ if (setsockopt(sock, IPPROTO_ICMPV6, ICMP6_FILTER, &filter, sizeof(filter)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt ICMP6_FILTER (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ // We want a source address and interface index.
+
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVPKTINFO, &kEnable, sizeof(kEnable)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_RECVPKTINFO (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ if (setsockopt(sock, IPPROTO_RAW, IPV6_CHECKSUM, &kIpv6ChecksumOffset,
+ sizeof(kIpv6ChecksumOffset)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_CHECKSUM (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ // We need to be able to reject RAs arriving from off-link.
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &kEnable, sizeof(kEnable)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_RECVHOPLIMIT (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &kHopLimit, sizeof(kHopLimit)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_UNICAST_HOPS (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &kHopLimit, sizeof(kHopLimit)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException",
+ "failed to create the setsockopt IPV6_MULTICAST_HOPS (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, ifName.c_str(), strlen(ifName.c_str()))) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt SO_BINDTODEVICE (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ return sock;
+}
+
+/*
+ * JNI registration.
+ */
+
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"nativeCreateIcmp6Socket", "(Ljava/lang/String;)I",
+ (void *)com_android_server_thread_InfraInterfaceController_createIcmp6Socket},
+};
+
+int register_com_android_server_thread_InfraInterfaceController(JNIEnv *env) {
+ return jniRegisterNativeMethods(env, "com/android/server/thread/InfraInterfaceController",
+ gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp b/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
new file mode 100644
index 0000000..c56bc0b
--- /dev/null
+++ b/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
@@ -0,0 +1,123 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "jniThreadTun"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/if_arp.h>
+#include <linux/if_tun.h>
+#include <linux/ioctl.h>
+#include <log/log.h>
+#include <net/if.h>
+#include <spawn.h>
+#include <sys/wait.h>
+#include <string>
+
+#include <private/android_filesystem_config.h>
+
+#include "jni.h"
+#include "nativehelper/JNIHelp.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+namespace android {
+static jint com_android_server_thread_TunInterfaceController_createTunInterface(
+ JNIEnv* env, jobject clazz, jstring interfaceName, jint mtu) {
+ ScopedUtfChars ifName(env, interfaceName);
+
+ int fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK | O_CLOEXEC);
+ if (fd == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "open tun device failed (%s)",
+ strerror(errno));
+ return -1;
+ }
+
+ struct ifreq ifr = {
+ .ifr_flags = IFF_TUN | IFF_NO_PI | static_cast<short>(IFF_TUN_EXCL),
+ };
+ strlcpy(ifr.ifr_name, ifName.c_str(), sizeof(ifr.ifr_name));
+
+ if (ioctl(fd, TUNSETIFF, &ifr, sizeof(ifr)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(TUNSETIFF) failed (%s)",
+ strerror(errno));
+ close(fd);
+ return -1;
+ }
+
+ int inet6 = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_IP);
+ if (inet6 == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "create inet6 socket failed (%s)",
+ strerror(errno));
+ close(fd);
+ return -1;
+ }
+ ifr.ifr_mtu = mtu;
+ if (ioctl(inet6, SIOCSIFMTU, &ifr) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(SIOCSIFMTU) failed (%s)",
+ strerror(errno));
+ close(fd);
+ close(inet6);
+ return -1;
+ }
+
+ close(inet6);
+ return fd;
+}
+
+static void com_android_server_thread_TunInterfaceController_setInterfaceUp(
+ JNIEnv* env, jobject clazz, jstring interfaceName, jboolean isUp) {
+ struct ifreq ifr;
+ ScopedUtfChars ifName(env, interfaceName);
+
+ ifr.ifr_flags = isUp ? IFF_UP : 0;
+ strlcpy(ifr.ifr_name, ifName.c_str(), sizeof(ifr.ifr_name));
+
+ int inet6 = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_IP);
+ if (inet6 == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "create inet6 socket failed (%s)",
+ strerror(errno));
+ }
+
+ if (ioctl(inet6, SIOCSIFFLAGS, &ifr) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(SIOCSIFFLAGS) failed (%s)",
+ strerror(errno));
+ }
+
+ close(inet6);
+}
+
+/*
+ * JNI registration.
+ */
+
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"nativeCreateTunInterface",
+ "(Ljava/lang/String;I)I",
+ (void*)com_android_server_thread_TunInterfaceController_createTunInterface},
+ {"nativeSetInterfaceUp",
+ "(Ljava/lang/String;Z)V",
+ (void*)com_android_server_thread_TunInterfaceController_setInterfaceUp},
+};
+
+int register_com_android_server_thread_TunInterfaceController(JNIEnv* env) {
+ return jniRegisterNativeMethods(env, "com/android/server/thread/TunInterfaceController",
+ gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/thread/service/jni/onload.cpp b/thread/service/jni/onload.cpp
new file mode 100644
index 0000000..66add74
--- /dev/null
+++ b/thread/service/jni/onload.cpp
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+#include "jni.h"
+#include "utils/Log.h"
+
+namespace android {
+int register_com_android_server_thread_TunInterfaceController(JNIEnv* env);
+int register_com_android_server_thread_InfraInterfaceController(JNIEnv* env);
+}
+
+using namespace android;
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
+ JNIEnv* env = NULL;
+
+ if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
+ ALOGE("GetEnv failed!");
+ return -1;
+ }
+ ALOG_ASSERT(env != NULL, "Could not retrieve the env!");
+
+ register_com_android_server_thread_TunInterfaceController(env);
+ register_com_android_server_thread_InfraInterfaceController(env);
+ return JNI_VERSION_1_4;
+}
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index b75b8e6..6862398 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -37,6 +37,7 @@
"androidx.test.ext.junit",
"compatibility-device-util-axt",
"ctstestrunner-axt",
+ "guava",
"guava-android-testlib",
"net-tests-utils",
"truth",
diff --git a/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java b/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
index 39df21b..0e76930 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
@@ -70,7 +70,7 @@
// PAN ID: 0xD9A0
// PSKc: A245479C836D551B9CA557F7B9D351B4
// Security Policy: 672 onrcb
- private static final byte[] VALID_DATASET =
+ private static final byte[] VALID_DATASET_TLVS =
base16().decode(
"0E080000000000010000000300001335060004001FFFE002"
+ "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
@@ -78,6 +78,9 @@
+ "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ "B9D351B40C0402A0FFF8");
+ private static final ActiveOperationalDataset DEFAULT_DATASET =
+ ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET_TLVS);
+
private static byte[] removeTlv(byte[] dataset, int type) {
ByteArrayOutputStream os = new ByteArrayOutputStream(dataset.length);
int i = 0;
@@ -105,7 +108,8 @@
@Test
public void parcelable_parcelingIsLossLess() {
- ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET);
+ ActiveOperationalDataset dataset =
+ ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET_TLVS);
assertParcelingIsLossless(dataset);
}
@@ -126,7 +130,8 @@
@Test
public void fromThreadTlvs_invalidNetworkKeyTlv_throwsIllegalArgument() {
- byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_NETWORK_KEY, "05080000000000000000");
+ byte[] invalidTlv =
+ replaceTlv(VALID_DATASET_TLVS, TYPE_NETWORK_KEY, "05080000000000000000");
assertThrows(
IllegalArgumentException.class,
@@ -135,7 +140,7 @@
@Test
public void fromThreadTlvs_noNetworkKeyTlv_throwsIllegalArgument() {
- byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_NETWORK_KEY);
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_NETWORK_KEY);
assertThrows(
IllegalArgumentException.class,
@@ -144,7 +149,8 @@
@Test
public void fromThreadTlvs_invalidActiveTimestampTlv_throwsIllegalArgument() {
- byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_ACTIVE_TIMESTAMP, "0E0700000000010000");
+ byte[] invalidTlv =
+ replaceTlv(VALID_DATASET_TLVS, TYPE_ACTIVE_TIMESTAMP, "0E0700000000010000");
assertThrows(
IllegalArgumentException.class,
@@ -153,7 +159,7 @@
@Test
public void fromThreadTlvs_noActiveTimestampTlv_throwsIllegalArgument() {
- byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_ACTIVE_TIMESTAMP);
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_ACTIVE_TIMESTAMP);
assertThrows(
IllegalArgumentException.class,
@@ -162,7 +168,7 @@
@Test
public void fromThreadTlvs_invalidNetworkNameTlv_emptyName_throwsIllegalArgument() {
- byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_NETWORK_NAME, "0300");
+ byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_NETWORK_NAME, "0300");
assertThrows(
IllegalArgumentException.class,
@@ -173,7 +179,9 @@
public void fromThreadTlvs_invalidNetworkNameTlv_tooLongName_throwsIllegalArgument() {
byte[] invalidTlv =
replaceTlv(
- VALID_DATASET, TYPE_NETWORK_NAME, "03114142434445464748494A4B4C4D4E4F5051");
+ VALID_DATASET_TLVS,
+ TYPE_NETWORK_NAME,
+ "03114142434445464748494A4B4C4D4E4F5051");
assertThrows(
IllegalArgumentException.class,
@@ -182,7 +190,7 @@
@Test
public void fromThreadTlvs_noNetworkNameTlv_throwsIllegalArgument() {
- byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_NETWORK_NAME);
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_NETWORK_NAME);
assertThrows(
IllegalArgumentException.class,
@@ -191,7 +199,7 @@
@Test
public void fromThreadTlvs_invalidChannelTlv_channelMissing_throwsIllegalArgument() {
- byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000100");
+ byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL, "000100");
assertThrows(
IllegalArgumentException.class,
@@ -200,7 +208,7 @@
@Test
public void fromThreadTlvs_undefinedChannelPage_success() {
- byte[] datasetTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "0003010020");
+ byte[] datasetTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL, "0003010020");
ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlv);
@@ -210,8 +218,8 @@
@Test
public void fromThreadTlvs_invalid2P4GhzChannel_throwsIllegalArgument() {
- byte[] invalidTlv1 = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000300000A");
- byte[] invalidTlv2 = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000300001B");
+ byte[] invalidTlv1 = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL, "000300000A");
+ byte[] invalidTlv2 = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL, "000300001B");
assertThrows(
IllegalArgumentException.class,
@@ -223,7 +231,7 @@
@Test
public void fromThreadTlvs_valid2P4GhzChannelTlv_success() {
- byte[] validTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "0003000010");
+ byte[] validTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL, "0003000010");
ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(validTlv);
@@ -232,7 +240,7 @@
@Test
public void fromThreadTlvs_noChannelTlv_throwsIllegalArgument() {
- byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_CHANNEL);
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_CHANNEL);
assertThrows(
IllegalArgumentException.class,
@@ -241,7 +249,7 @@
@Test
public void fromThreadTlvs_prematureEndOfChannelMaskEntry_throwsIllegalArgument() {
- byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "350100");
+ byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL_MASK, "350100");
assertThrows(
IllegalArgumentException.class,
@@ -250,7 +258,7 @@
@Test
public void fromThreadTlvs_inconsistentChannelMaskLength_throwsIllegalArgument() {
- byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "3506000500010000");
+ byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL_MASK, "3506000500010000");
assertThrows(
IllegalArgumentException.class,
@@ -261,7 +269,7 @@
public void fromThreadTlvs_unsupportedChannelMaskLength_success() {
ActiveOperationalDataset dataset =
ActiveOperationalDataset.fromThreadTlvs(
- replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "350700050001000000"));
+ replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL_MASK, "350700050001000000"));
SparseArray<byte[]> channelMask = dataset.getChannelMask();
assertThat(channelMask.size()).isEqualTo(1);
@@ -271,7 +279,7 @@
@Test
public void fromThreadTlvs_noChannelMaskTlv_throwsIllegalArgument() {
- byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_CHANNEL_MASK);
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_CHANNEL_MASK);
assertThrows(
IllegalArgumentException.class,
@@ -280,7 +288,7 @@
@Test
public void fromThreadTlvs_invalidPanIdTlv_throwsIllegalArgument() {
- byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_PAN_ID, "010101");
+ byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_PAN_ID, "010101");
assertThrows(
IllegalArgumentException.class,
@@ -289,7 +297,7 @@
@Test
public void fromThreadTlvs_noPanIdTlv_throwsIllegalArgument() {
- byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_PAN_ID);
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_PAN_ID);
assertThrows(
IllegalArgumentException.class,
@@ -298,7 +306,8 @@
@Test
public void fromThreadTlvs_invalidExtendedPanIdTlv_throwsIllegalArgument() {
- byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_EXTENDED_PAN_ID, "020700010203040506");
+ byte[] invalidTlv =
+ replaceTlv(VALID_DATASET_TLVS, TYPE_EXTENDED_PAN_ID, "020700010203040506");
assertThrows(
IllegalArgumentException.class,
@@ -307,7 +316,7 @@
@Test
public void fromThreadTlvs_noExtendedPanIdTlv_throwsIllegalArgument() {
- byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_EXTENDED_PAN_ID);
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_EXTENDED_PAN_ID);
assertThrows(
IllegalArgumentException.class,
@@ -317,7 +326,7 @@
@Test
public void fromThreadTlvs_invalidPskcTlv_throwsIllegalArgument() {
byte[] invalidTlv =
- replaceTlv(VALID_DATASET, TYPE_PSKC, "0411000102030405060708090A0B0C0D0E0F10");
+ replaceTlv(VALID_DATASET_TLVS, TYPE_PSKC, "0411000102030405060708090A0B0C0D0E0F10");
assertThrows(
IllegalArgumentException.class,
@@ -326,7 +335,7 @@
@Test
public void fromThreadTlvs_noPskcTlv_throwsIllegalArgument() {
- byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_PSKC);
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_PSKC);
assertThrows(
IllegalArgumentException.class,
@@ -336,7 +345,7 @@
@Test
public void fromThreadTlvs_invalidMeshLocalPrefixTlv_throwsIllegalArgument() {
byte[] invalidTlv =
- replaceTlv(VALID_DATASET, TYPE_MESH_LOCAL_PREFIX, "0709FD0001020304050607");
+ replaceTlv(VALID_DATASET_TLVS, TYPE_MESH_LOCAL_PREFIX, "0709FD0001020304050607");
assertThrows(
IllegalArgumentException.class,
@@ -345,7 +354,7 @@
@Test
public void fromThreadTlvs_noMeshLocalPrefixTlv_throwsIllegalArgument() {
- byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_MESH_LOCAL_PREFIX);
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_MESH_LOCAL_PREFIX);
assertThrows(
IllegalArgumentException.class,
@@ -354,7 +363,7 @@
@Test
public void fromThreadTlvs_tooShortSecurityPolicyTlv_throwsIllegalArgument() {
- byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_SECURITY_POLICY, "0C0101");
+ byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_SECURITY_POLICY, "0C0101");
assertThrows(
IllegalArgumentException.class,
@@ -363,7 +372,7 @@
@Test
public void fromThreadTlvs_noSecurityPolicyTlv_throwsIllegalArgument() {
- byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_SECURITY_POLICY);
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_SECURITY_POLICY);
assertThrows(
IllegalArgumentException.class,
@@ -429,7 +438,7 @@
@Test
public void fromThreadTlvs_containsUnknownTlvs_unknownTlvsRetained() {
- final byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET, "AA01FFBB020102");
+ final byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET_TLVS, "AA01FFBB020102");
ActiveOperationalDataset dataset =
ActiveOperationalDataset.fromThreadTlvs(datasetWithUnknownTlvs);
@@ -443,7 +452,7 @@
@Test
public void toThreadTlvs_conversionIsLossLess() {
- ActiveOperationalDataset dataset1 = ActiveOperationalDataset.createRandomDataset();
+ ActiveOperationalDataset dataset1 = DEFAULT_DATASET;
ActiveOperationalDataset dataset2 =
ActiveOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
@@ -465,9 +474,7 @@
};
ActiveOperationalDataset dataset =
- new Builder(ActiveOperationalDataset.createRandomDataset())
- .setNetworkKey(networkKey)
- .build();
+ new Builder(DEFAULT_DATASET).setNetworkKey(networkKey).build();
assertThat(dataset.getNetworkKey()).isEqualTo(networkKey);
}
@@ -475,7 +482,7 @@
@Test
public void builder_setInvalidNetworkKey_throwsIllegalArgument() {
byte[] invalidNetworkKey = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
- Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+ Builder builder = new Builder();
assertThrows(
IllegalArgumentException.class, () -> builder.setNetworkKey(invalidNetworkKey));
@@ -486,9 +493,7 @@
byte[] extendedPanId = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
ActiveOperationalDataset dataset =
- new Builder(ActiveOperationalDataset.createRandomDataset())
- .setExtendedPanId(extendedPanId)
- .build();
+ new Builder(DEFAULT_DATASET).setExtendedPanId(extendedPanId).build();
assertThat(dataset.getExtendedPanId()).isEqualTo(extendedPanId);
}
@@ -496,31 +501,28 @@
@Test
public void builder_setInvalidExtendedPanId_throwsIllegalArgument() {
byte[] extendedPanId = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
- Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+ Builder builder = new Builder();
assertThrows(IllegalArgumentException.class, () -> builder.setExtendedPanId(extendedPanId));
}
@Test
public void builder_setValidPanId_success() {
- ActiveOperationalDataset dataset =
- new Builder(ActiveOperationalDataset.createRandomDataset())
- .setPanId(0xfffe)
- .build();
+ ActiveOperationalDataset dataset = new Builder(DEFAULT_DATASET).setPanId(0xfffe).build();
assertThat(dataset.getPanId()).isEqualTo(0xfffe);
}
@Test
public void builder_setInvalidPanId_throwsIllegalArgument() {
- Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+ Builder builder = new Builder();
assertThrows(IllegalArgumentException.class, () -> builder.setPanId(0xffff));
}
@Test
public void builder_setInvalidChannel_throwsIllegalArgument() {
- Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+ Builder builder = new Builder();
assertThrows(IllegalArgumentException.class, () -> builder.setChannel(0, 0));
assertThrows(IllegalArgumentException.class, () -> builder.setChannel(0, 27));
@@ -529,9 +531,7 @@
@Test
public void builder_setValid2P4GhzChannel_success() {
ActiveOperationalDataset dataset =
- new Builder(ActiveOperationalDataset.createRandomDataset())
- .setChannel(CHANNEL_PAGE_24_GHZ, 16)
- .build();
+ new Builder(DEFAULT_DATASET).setChannel(CHANNEL_PAGE_24_GHZ, 16).build();
assertThat(dataset.getChannel()).isEqualTo(16);
assertThat(dataset.getChannelPage()).isEqualTo(CHANNEL_PAGE_24_GHZ);
@@ -540,23 +540,21 @@
@Test
public void builder_setValidNetworkName_success() {
ActiveOperationalDataset dataset =
- new Builder(ActiveOperationalDataset.createRandomDataset())
- .setNetworkName("ot-network")
- .build();
+ new Builder(DEFAULT_DATASET).setNetworkName("ot-network").build();
assertThat(dataset.getNetworkName()).isEqualTo("ot-network");
}
@Test
public void builder_setEmptyNetworkName_throwsIllegalArgument() {
- Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+ Builder builder = new Builder();
assertThrows(IllegalArgumentException.class, () -> builder.setNetworkName(""));
}
@Test
public void builder_setTooLongNetworkName_throwsIllegalArgument() {
- Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+ Builder builder = new Builder();
assertThrows(
IllegalArgumentException.class, () -> builder.setNetworkName("openthread-network"));
@@ -564,7 +562,7 @@
@Test
public void builder_setTooLongUtf8NetworkName_throwsIllegalArgument() {
- Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+ Builder builder = new Builder();
// UTF-8 encoded length of "我的线程网络" is 18 bytes which exceeds the max length
assertThrows(IllegalArgumentException.class, () -> builder.setNetworkName("我的线程网络"));
@@ -573,9 +571,7 @@
@Test
public void builder_setValidUtf8NetworkName_success() {
ActiveOperationalDataset dataset =
- new Builder(ActiveOperationalDataset.createRandomDataset())
- .setNetworkName("我的网络")
- .build();
+ new Builder(DEFAULT_DATASET).setNetworkName("我的网络").build();
assertThat(dataset.getNetworkName()).isEqualTo("我的网络");
}
@@ -584,8 +580,7 @@
public void builder_setValidPskc_success() {
byte[] pskc = base16().decode("A245479C836D551B9CA557F7B9D351B4");
- ActiveOperationalDataset dataset =
- new Builder(ActiveOperationalDataset.createRandomDataset()).setPskc(pskc).build();
+ ActiveOperationalDataset dataset = new Builder(DEFAULT_DATASET).setPskc(pskc).build();
assertThat(dataset.getPskc()).isEqualTo(pskc);
}
@@ -593,18 +588,18 @@
@Test
public void builder_setTooLongPskc_throwsIllegalArgument() {
byte[] tooLongPskc = base16().decode("A245479C836D551B9CA557F7B9D351B400");
- Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+ Builder builder = new Builder();
assertThrows(IllegalArgumentException.class, () -> builder.setPskc(tooLongPskc));
}
@Test
public void builder_setValidChannelMask_success() {
- Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
SparseArray<byte[]> channelMask = new SparseArray<byte[]>(1);
channelMask.put(0, new byte[] {0x00, 0x00, 0x01, 0x00});
- ActiveOperationalDataset dataset = builder.setChannelMask(channelMask).build();
+ ActiveOperationalDataset dataset =
+ new Builder(DEFAULT_DATASET).setChannelMask(channelMask).build();
SparseArray<byte[]> resultChannelMask = dataset.getChannelMask();
assertThat(resultChannelMask.size()).isEqualTo(1);
@@ -613,7 +608,7 @@
@Test
public void builder_setEmptyChannelMask_throwsIllegalArgument() {
- Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+ Builder builder = new Builder();
assertThrows(
IllegalArgumentException.class,
@@ -623,7 +618,7 @@
@Test
public void builder_setValidActiveTimestamp_success() {
ActiveOperationalDataset dataset =
- new Builder(ActiveOperationalDataset.createRandomDataset())
+ new Builder(DEFAULT_DATASET)
.setActiveTimestamp(
new OperationalDatasetTimestamp(
/* seconds= */ 1,
@@ -638,7 +633,7 @@
@Test
public void builder_wrongMeshLocalPrefixLength_throwsIllegalArguments() {
- Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+ Builder builder = new Builder();
// The Mesh-Local Prefix length must be 64 bits
assertThrows(
@@ -656,7 +651,7 @@
@Test
public void builder_meshLocalPrefixNotStartWith0xfd_throwsIllegalArguments() {
- Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+ Builder builder = new Builder();
assertThrows(
IllegalArgumentException.class,
@@ -666,9 +661,7 @@
@Test
public void builder_setValidMeshLocalPrefix_success() {
ActiveOperationalDataset dataset =
- new Builder(ActiveOperationalDataset.createRandomDataset())
- .setMeshLocalPrefix(new IpPrefix("fd00::/64"))
- .build();
+ new Builder(DEFAULT_DATASET).setMeshLocalPrefix(new IpPrefix("fd00::/64")).build();
assertThat(dataset.getMeshLocalPrefix()).isEqualTo(new IpPrefix("fd00::/64"));
}
@@ -676,7 +669,7 @@
@Test
public void builder_setValid1P2SecurityPolicy_success() {
ActiveOperationalDataset dataset =
- new Builder(ActiveOperationalDataset.createRandomDataset())
+ new Builder(DEFAULT_DATASET)
.setSecurityPolicy(
new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}))
.build();
@@ -689,7 +682,7 @@
@Test
public void builder_setValid1P1SecurityPolicy_success() {
ActiveOperationalDataset dataset =
- new Builder(ActiveOperationalDataset.createRandomDataset())
+ new Builder(DEFAULT_DATASET)
.setSecurityPolicy(new SecurityPolicy(672, new byte[] {(byte) 0xff}))
.build();
diff --git a/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java b/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
index 7a49957..0bb18ce 100644
--- a/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
@@ -25,8 +25,10 @@
import android.net.IpPrefix;
import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
+import android.util.SparseArray;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -37,20 +39,36 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.net.InetAddress;
import java.time.Duration;
/** Tests for {@link PendingOperationalDataset}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class PendingOperationalDatasetTest {
- private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
- ActiveOperationalDataset.createRandomDataset();
+ private static ActiveOperationalDataset createActiveDataset() throws Exception {
+ SparseArray<byte[]> channelMask = new SparseArray<>(1);
+ channelMask.put(0, new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
+
+ return new ActiveOperationalDataset.Builder()
+ .setActiveTimestamp(new OperationalDatasetTimestamp(100, 10, false))
+ .setExtendedPanId(new byte[] {0, 1, 2, 3, 4, 5, 6, 7})
+ .setPanId(12345)
+ .setNetworkName("defaultNet")
+ .setChannel(0, 18)
+ .setChannelMask(channelMask)
+ .setPskc(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15})
+ .setNetworkKey(new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1})
+ .setMeshLocalPrefix(new IpPrefix(InetAddress.getByName("fd00::1"), 64))
+ .setSecurityPolicy(new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}))
+ .build();
+ }
@Test
- public void parcelable_parcelingIsLossLess() {
+ public void parcelable_parcelingIsLossLess() throws Exception {
PendingOperationalDataset dataset =
new PendingOperationalDataset(
- DEFAULT_ACTIVE_DATASET,
+ createActiveDataset(),
new OperationalDatasetTimestamp(31536000, 200, false),
Duration.ofHours(100));
@@ -58,9 +76,15 @@
}
@Test
- public void equalityTests() {
- ActiveOperationalDataset activeDataset1 = ActiveOperationalDataset.createRandomDataset();
- ActiveOperationalDataset activeDataset2 = ActiveOperationalDataset.createRandomDataset();
+ public void equalityTests() throws Exception {
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(createActiveDataset())
+ .setNetworkName("net1")
+ .build();
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(createActiveDataset())
+ .setNetworkName("net2")
+ .build();
new EqualsTester()
.addEqualityGroup(
@@ -103,14 +127,15 @@
}
@Test
- public void constructor_correctValuesAreSet() {
+ public void constructor_correctValuesAreSet() throws Exception {
+ final ActiveOperationalDataset activeDataset = createActiveDataset();
PendingOperationalDataset dataset =
new PendingOperationalDataset(
- DEFAULT_ACTIVE_DATASET,
+ activeDataset,
new OperationalDatasetTimestamp(31536000, 200, false),
Duration.ofHours(100));
- assertThat(dataset.getActiveOperationalDataset()).isEqualTo(DEFAULT_ACTIVE_DATASET);
+ assertThat(dataset.getActiveOperationalDataset()).isEqualTo(activeDataset);
assertThat(dataset.getPendingTimestamp())
.isEqualTo(new OperationalDatasetTimestamp(31536000, 200, false));
assertThat(dataset.getDelayTimer()).isEqualTo(Duration.ofHours(100));
@@ -166,33 +191,35 @@
}
@Test
- public void fromThreadTlvs_completePendingDatasetTlvs_success() {
+ public void fromThreadTlvs_completePendingDatasetTlvs_success() throws Exception {
+ final ActiveOperationalDataset activeDataset = createActiveDataset();
+
// Type Length Value
// 0x33 0x08 0x0000000000010000 (Pending Timestamp TLV)
// 0x34 0x04 0x0000012C (Delay Timer TLV)
final byte[] pendingTimestampAndDelayTimerTlvs =
base16().decode("3308000000000001000034040000012C");
final byte[] pendingDatasetTlvs =
- Bytes.concat(
- pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs());
+ Bytes.concat(pendingTimestampAndDelayTimerTlvs, activeDataset.toThreadTlvs());
PendingOperationalDataset dataset =
PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs);
- assertThat(dataset.getActiveOperationalDataset()).isEqualTo(DEFAULT_ACTIVE_DATASET);
+ assertThat(dataset.getActiveOperationalDataset()).isEqualTo(activeDataset);
assertThat(dataset.getPendingTimestamp())
.isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
assertThat(dataset.getDelayTimer()).isEqualTo(Duration.ofMillis(300));
}
@Test
- public void fromThreadTlvs_PendingTimestampTlvIsMissing_throwsIllegalArgument() {
+ public void fromThreadTlvs_PendingTimestampTlvIsMissing_throwsIllegalArgument()
+ throws Exception {
// Type Length Value
// 0x34 0x04 0x00000064 (Delay Timer TLV)
final byte[] pendingTimestampAndDelayTimerTlvs = base16().decode("34040000012C");
final byte[] pendingDatasetTlvs =
Bytes.concat(
- pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs());
+ pendingTimestampAndDelayTimerTlvs, createActiveDataset().toThreadTlvs());
assertThrows(
IllegalArgumentException.class,
@@ -200,13 +227,13 @@
}
@Test
- public void fromThreadTlvs_delayTimerTlvIsMissing_throwsIllegalArgument() {
+ public void fromThreadTlvs_delayTimerTlvIsMissing_throwsIllegalArgument() throws Exception {
// Type Length Value
// 0x33 0x08 0x0000000000010000 (Pending Timestamp TLV)
final byte[] pendingTimestampAndDelayTimerTlvs = base16().decode("33080000000000010000");
final byte[] pendingDatasetTlvs =
Bytes.concat(
- pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs());
+ pendingTimestampAndDelayTimerTlvs, createActiveDataset().toThreadTlvs());
assertThrows(
IllegalArgumentException.class,
@@ -214,8 +241,8 @@
}
@Test
- public void fromThreadTlvs_activeDatasetTlvs_throwsIllegalArgument() {
- final byte[] activeDatasetTlvs = DEFAULT_ACTIVE_DATASET.toThreadTlvs();
+ public void fromThreadTlvs_activeDatasetTlvs_throwsIllegalArgument() throws Exception {
+ final byte[] activeDatasetTlvs = createActiveDataset().toThreadTlvs();
assertThrows(
IllegalArgumentException.class,
@@ -232,10 +259,10 @@
}
@Test
- public void toThreadTlvs_conversionIsLossLess() {
+ public void toThreadTlvs_conversionIsLossLess() throws Exception {
PendingOperationalDataset dataset1 =
new PendingOperationalDataset(
- DEFAULT_ACTIVE_DATASET,
+ createActiveDataset(),
new OperationalDatasetTimestamp(31536000, 200, false),
Duration.ofHours(100));
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index b3118f4..362ff39 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -16,58 +16,695 @@
package android.net.thread.cts;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
+import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
+import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
+import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeNotNull;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.Manifest.permission;
import android.content.Context;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.OperationalDatasetTimestamp;
+import android.net.thread.PendingOperationalDataset;
import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
+import android.net.thread.ThreadNetworkController.StateCallback;
+import android.net.thread.ThreadNetworkException;
import android.net.thread.ThreadNetworkManager;
import android.os.Build;
+import android.os.OutcomeReceiver;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.LargeTest;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
+import com.google.common.util.concurrent.SettableFuture;
+
+import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
/** CTS tests for {@link ThreadNetworkController}. */
-@SmallTest
+@LargeTest
@RunWith(DevSdkIgnoreRunner.class)
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Thread is available on only U+
public class ThreadNetworkControllerTest {
+ private static final int CALLBACK_TIMEOUT_MILLIS = 1000;
+ private static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
+ "android.permission.THREAD_NETWORK_PRIVILEGED";
+
@Rule public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
private final Context mContext = ApplicationProvider.getApplicationContext();
+ private ExecutorService mExecutor;
private ThreadNetworkManager mManager;
+ private Set<String> mGrantedPermissions;
+
@Before
public void setUp() {
+ mExecutor = Executors.newSingleThreadExecutor();
mManager = mContext.getSystemService(ThreadNetworkManager.class);
+ mGrantedPermissions = new HashSet<String>();
// TODO: we will also need it in tearDown(), it's better to have a Rule to skip
// tests if a feature is not available.
assumeNotNull(mManager);
}
+ @After
+ public void tearDown() throws Exception {
+ if (mManager != null) {
+ leaveAndWait();
+ dropAllPermissions();
+ }
+ }
+
private List<ThreadNetworkController> getAllControllers() {
return mManager.getAllThreadNetworkControllers();
}
+ private void leaveAndWait() throws Exception {
+ grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<Void> future = SettableFuture.create();
+ controller.leave(mExecutor, future::set);
+ future.get();
+ }
+ }
+
+ private void grantPermissions(String... permissions) {
+ for (String permission : permissions) {
+ mGrantedPermissions.add(permission);
+ }
+ String[] allPermissions = new String[mGrantedPermissions.size()];
+ mGrantedPermissions.toArray(allPermissions);
+ getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(allPermissions);
+ }
+
+ private static void dropAllPermissions() {
+ getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+ }
+
+ private static ActiveOperationalDataset newRandomizedDataset(
+ String networkName, ThreadNetworkController controller) throws Exception {
+ SettableFuture<ActiveOperationalDataset> future = SettableFuture.create();
+ controller.createRandomizedDataset(networkName, directExecutor(), future::set);
+ return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ }
+
+ private static boolean isAttached(ThreadNetworkController controller) throws Exception {
+ return ThreadNetworkController.isAttached(getDeviceRole(controller));
+ }
+
+ private static int getDeviceRole(ThreadNetworkController controller) throws Exception {
+ SettableFuture<Integer> future = SettableFuture.create();
+ StateCallback callback = future::set;
+ controller.registerStateCallback(directExecutor(), callback);
+ int role = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ controller.unregisterStateCallback(callback);
+ return role;
+ }
+
+ private static int waitForStateAnyOf(
+ ThreadNetworkController controller, List<Integer> deviceRoles) throws Exception {
+ SettableFuture<Integer> future = SettableFuture.create();
+ StateCallback callback =
+ newRole -> {
+ if (deviceRoles.contains(newRole)) {
+ future.set(newRole);
+ }
+ };
+ controller.registerStateCallback(directExecutor(), callback);
+ int role = future.get();
+ controller.unregisterStateCallback(callback);
+ return role;
+ }
+
+ private static ActiveOperationalDataset getActiveOperationalDataset(
+ ThreadNetworkController controller) throws Exception {
+ SettableFuture<ActiveOperationalDataset> future = SettableFuture.create();
+ OperationalDatasetCallback callback = future::set;
+ controller.registerOperationalDatasetCallback(directExecutor(), callback);
+ ActiveOperationalDataset dataset = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ controller.unregisterOperationalDatasetCallback(callback);
+ return dataset;
+ }
+
+ private static PendingOperationalDataset getPendingOperationalDataset(
+ ThreadNetworkController controller) throws Exception {
+ SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
+ SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+ controller.registerOperationalDatasetCallback(
+ directExecutor(), newDatasetCallback(activeFuture, pendingFuture));
+ return pendingFuture.get();
+ }
+
+ private static OperationalDatasetCallback newDatasetCallback(
+ SettableFuture<ActiveOperationalDataset> activeFuture,
+ SettableFuture<PendingOperationalDataset> pendingFuture) {
+ return new OperationalDatasetCallback() {
+ @Override
+ public void onActiveOperationalDatasetChanged(
+ ActiveOperationalDataset activeOpDataset) {
+ activeFuture.set(activeOpDataset);
+ }
+
+ @Override
+ public void onPendingOperationalDatasetChanged(
+ PendingOperationalDataset pendingOpDataset) {
+ pendingFuture.set(pendingOpDataset);
+ }
+ };
+ }
+
@Test
public void getThreadVersion_returnsAtLeastThreadVersion1P3() {
for (ThreadNetworkController controller : getAllControllers()) {
assertThat(controller.getThreadVersion()).isAtLeast(THREAD_VERSION_1_3);
}
}
+
+ @Test
+ public void registerStateCallback_permissionsGranted_returnsCurrentStates() throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<Integer> deviceRole = SettableFuture.create();
+ StateCallback callback = deviceRole::set;
+
+ try {
+ controller.registerStateCallback(mExecutor, callback);
+
+ assertThat(deviceRole.get()).isEqualTo(DEVICE_ROLE_STOPPED);
+ } finally {
+ controller.unregisterStateCallback(callback);
+ }
+ }
+ }
+
+ @Test
+ public void registerStateCallback_noPermissions_throwsSecurityException() throws Exception {
+ dropAllPermissions();
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ assertThrows(
+ SecurityException.class,
+ () -> controller.registerStateCallback(mExecutor, role -> {}));
+ }
+ }
+
+ @Test
+ public void registerStateCallback_alreadyRegistered_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<Integer> deviceRole = SettableFuture.create();
+ StateCallback callback = role -> deviceRole.set(role);
+ controller.registerStateCallback(mExecutor, callback);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> controller.registerStateCallback(mExecutor, callback));
+ }
+ }
+
+ @Test
+ public void unregisterStateCallback_noPermissions_throwsSecurityException() throws Exception {
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<Integer> deviceRole = SettableFuture.create();
+ StateCallback callback = role -> deviceRole.set(role);
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ controller.registerStateCallback(mExecutor, callback);
+
+ try {
+ dropAllPermissions();
+ assertThrows(
+ SecurityException.class,
+ () -> controller.unregisterStateCallback(callback));
+ } finally {
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ controller.unregisterStateCallback(callback);
+ }
+ }
+ }
+
+ @Test
+ public void unregisterStateCallback_callbackRegistered_success() throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<Integer> deviceRole = SettableFuture.create();
+ StateCallback callback = role -> deviceRole.set(role);
+ controller.registerStateCallback(mExecutor, callback);
+
+ controller.unregisterStateCallback(callback);
+ }
+ }
+
+ @Test
+ public void unregisterStateCallback_callbackNotRegistered_throwsIllegalArgumentException()
+ throws Exception {
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<Integer> deviceRole = SettableFuture.create();
+ StateCallback callback = role -> deviceRole.set(role);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> controller.unregisterStateCallback(callback));
+ }
+ }
+
+ @Test
+ public void unregisterStateCallback_alreadyUnregistered_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<Integer> deviceRole = SettableFuture.create();
+ StateCallback callback = deviceRole::set;
+ controller.registerStateCallback(mExecutor, callback);
+ controller.unregisterStateCallback(callback);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> controller.unregisterStateCallback(callback));
+ }
+ }
+
+ @Test
+ public void registerOperationalDatasetCallback_permissionsGranted_returnsCurrentStates()
+ throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
+ SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+
+ try {
+ controller.registerOperationalDatasetCallback(mExecutor, callback);
+
+ assertThat(activeFuture.get()).isNull();
+ assertThat(pendingFuture.get()).isNull();
+ } finally {
+ controller.unregisterOperationalDatasetCallback(callback);
+ }
+ }
+ }
+
+ @Test
+ public void registerOperationalDatasetCallback_noPermissions_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
+ SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+
+ assertThrows(
+ SecurityException.class,
+ () -> controller.registerOperationalDatasetCallback(mExecutor, callback));
+ }
+ }
+
+ @Test
+ public void unregisterOperationalDatasetCallback_callbackRegistered_success() throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
+ SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+ controller.registerOperationalDatasetCallback(mExecutor, callback);
+
+ controller.unregisterOperationalDatasetCallback(callback);
+ }
+ }
+
+ @Test
+ public void unregisterOperationalDatasetCallback_noPermissions_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
+ SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+ grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ controller.registerOperationalDatasetCallback(mExecutor, callback);
+
+ try {
+ dropAllPermissions();
+ assertThrows(
+ SecurityException.class,
+ () -> controller.unregisterOperationalDatasetCallback(callback));
+ } finally {
+ grantPermissions(
+ permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ controller.unregisterOperationalDatasetCallback(callback);
+ }
+ }
+ }
+
+ private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
+ SettableFuture<V> future) {
+ return new OutcomeReceiver<V, ThreadNetworkException>() {
+ @Override
+ public void onResult(V result) {
+ future.set(result);
+ }
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ future.setException(e);
+ }
+ };
+ }
+
+ @Test
+ public void join_withPrivilegedPermission_success() throws Exception {
+ grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
+ SettableFuture<Void> joinFuture = SettableFuture.create();
+
+ controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get();
+
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ assertThat(isAttached(controller)).isTrue();
+ assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset);
+ }
+ }
+
+ @Test
+ public void join_withoutPrivilegedPermission_throwsSecurityException() throws Exception {
+ dropAllPermissions();
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
+
+ assertThrows(
+ SecurityException.class,
+ () -> controller.join(activeDataset, mExecutor, v -> {}));
+ }
+ }
+
+ @Test
+ public void join_concurrentRequests_firstOneIsAborted() throws Exception {
+ grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ final byte[] KEY_1 = new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
+ final byte[] KEY_2 = new byte[] {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2};
+ for (ThreadNetworkController controller : getAllControllers()) {
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(
+ newRandomizedDataset("TestNet", controller))
+ .setNetworkKey(KEY_1)
+ .build();
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(activeDataset1)
+ .setNetworkKey(KEY_2)
+ .build();
+ SettableFuture<Void> joinFuture1 = SettableFuture.create();
+ SettableFuture<Void> joinFuture2 = SettableFuture.create();
+
+ controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture1));
+ controller.join(activeDataset2, mExecutor, newOutcomeReceiver(joinFuture2));
+
+ ThreadNetworkException thrown =
+ (ThreadNetworkException)
+ assertThrows(ExecutionException.class, joinFuture1::get).getCause();
+ assertThat(thrown.getErrorCode()).isEqualTo(ERROR_ABORTED);
+ joinFuture2.get();
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ assertThat(isAttached(controller)).isTrue();
+ assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
+ }
+ }
+
+ @Test
+ public void leave_withPrivilegedPermission_success() throws Exception {
+ grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
+ SettableFuture<Void> joinFuture = SettableFuture.create();
+ SettableFuture<Void> leaveFuture = SettableFuture.create();
+ controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get();
+
+ controller.leave(mExecutor, newOutcomeReceiver(leaveFuture));
+ leaveFuture.get();
+
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
+ }
+ }
+
+ @Test
+ public void leave_withoutPrivilegedPermission_throwsSecurityException() {
+ dropAllPermissions();
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ assertThrows(SecurityException.class, () -> controller.leave(mExecutor, v -> {}));
+ }
+ }
+
+ @Test
+ public void leave_concurrentRequests_bothSuccess() throws Exception {
+ grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
+ SettableFuture<Void> joinFuture = SettableFuture.create();
+ SettableFuture<Void> leaveFuture1 = SettableFuture.create();
+ SettableFuture<Void> leaveFuture2 = SettableFuture.create();
+ controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get();
+
+ controller.leave(mExecutor, newOutcomeReceiver(leaveFuture1));
+ controller.leave(mExecutor, newOutcomeReceiver(leaveFuture2));
+
+ leaveFuture1.get();
+ leaveFuture2.get();
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
+ }
+ }
+
+ @Test
+ public void scheduleMigration_withPrivilegedPermission_success() throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(
+ newRandomizedDataset("TestNet", controller))
+ .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+ .setExtendedPanId(new byte[] {1, 1, 1, 1, 1, 1, 1, 1})
+ .build();
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(activeDataset1)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+ .setNetworkName("ThreadNet2")
+ .build();
+ PendingOperationalDataset pendingDataset2 =
+ new PendingOperationalDataset(
+ activeDataset2,
+ OperationalDatasetTimestamp.fromInstant(Instant.now()),
+ Duration.ofSeconds(30));
+ SettableFuture<Void> joinFuture = SettableFuture.create();
+ SettableFuture<Void> migrateFuture = SettableFuture.create();
+ controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get();
+
+ controller.scheduleMigration(
+ pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture));
+
+ migrateFuture.get();
+ Thread.sleep(35 * 1000);
+ assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
+ assertThat(getPendingOperationalDataset(controller)).isNull();
+ }
+ }
+
+ @Test
+ public void scheduleMigration_whenNotAttached_failWithPreconditionError() throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ PendingOperationalDataset pendingDataset =
+ new PendingOperationalDataset(
+ newRandomizedDataset("TestNet", controller),
+ OperationalDatasetTimestamp.fromInstant(Instant.now()),
+ Duration.ofSeconds(30));
+ SettableFuture<Void> migrateFuture = SettableFuture.create();
+
+ controller.scheduleMigration(
+ pendingDataset, mExecutor, newOutcomeReceiver(migrateFuture));
+
+ ThreadNetworkException thrown =
+ (ThreadNetworkException)
+ assertThrows(ExecutionException.class, migrateFuture::get).getCause();
+ assertThat(thrown.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
+ }
+ }
+
+ @Test
+ public void scheduleMigration_secondRequestHasSmallerTimestamp_rejectedByLeader()
+ throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ final ActiveOperationalDataset activeDataset =
+ new ActiveOperationalDataset.Builder(
+ newRandomizedDataset("testNet", controller))
+ .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+ .build();
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+ .setNetworkName("testNet1")
+ .build();
+ PendingOperationalDataset pendingDataset1 =
+ new PendingOperationalDataset(
+ activeDataset1,
+ new OperationalDatasetTimestamp(100, 0, false),
+ Duration.ofSeconds(30));
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
+ .setNetworkName("testNet2")
+ .build();
+ PendingOperationalDataset pendingDataset2 =
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(20, 0, false),
+ Duration.ofSeconds(30));
+ SettableFuture<Void> joinFuture = SettableFuture.create();
+ SettableFuture<Void> migrateFuture1 = SettableFuture.create();
+ SettableFuture<Void> migrateFuture2 = SettableFuture.create();
+ controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get();
+
+ controller.scheduleMigration(
+ pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
+ migrateFuture1.get();
+ controller.scheduleMigration(
+ pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
+
+ ThreadNetworkException thrown =
+ (ThreadNetworkException)
+ assertThrows(ExecutionException.class, migrateFuture2::get).getCause();
+ assertThat(thrown.getErrorCode()).isEqualTo(ERROR_REJECTED_BY_PEER);
+ }
+ }
+
+ @Test
+ public void scheduleMigration_secondRequestHasLargerTimestamp_success() throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ final ActiveOperationalDataset activeDataset =
+ new ActiveOperationalDataset.Builder(
+ newRandomizedDataset("validName", controller))
+ .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+ .build();
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+ .setNetworkName("testNet1")
+ .build();
+ PendingOperationalDataset pendingDataset1 =
+ new PendingOperationalDataset(
+ activeDataset1,
+ new OperationalDatasetTimestamp(100, 0, false),
+ Duration.ofSeconds(30));
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
+ .setNetworkName("testNet2")
+ .build();
+ PendingOperationalDataset pendingDataset2 =
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(200, 0, false),
+ Duration.ofSeconds(30));
+ SettableFuture<Void> joinFuture = SettableFuture.create();
+ SettableFuture<Void> migrateFuture1 = SettableFuture.create();
+ SettableFuture<Void> migrateFuture2 = SettableFuture.create();
+ controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get();
+
+ controller.scheduleMigration(
+ pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
+ migrateFuture1.get();
+ controller.scheduleMigration(
+ pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
+
+ migrateFuture2.get();
+ Thread.sleep(35 * 1000);
+ assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
+ assertThat(getPendingOperationalDataset(controller)).isNull();
+ }
+ }
+
+ @Test
+ public void createRandomizedDataset_wrongNetworkNameLength_throwsIllegalArgumentException() {
+ for (ThreadNetworkController controller : getAllControllers()) {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> controller.createRandomizedDataset("", mExecutor, dataset -> {}));
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ controller.createRandomizedDataset(
+ "ANetNameIs17Bytes", mExecutor, dataset -> {}));
+ }
+ }
+
+ @Test
+ public void createRandomizedDataset_validNetworkName_success() throws Exception {
+ for (ThreadNetworkController controller : getAllControllers()) {
+ ActiveOperationalDataset dataset = newRandomizedDataset("validName", controller);
+
+ assertThat(dataset.getNetworkName()).isEqualTo("validName");
+ assertThat(dataset.getPanId()).isLessThan(0xffff);
+ assertThat(dataset.getChannelMask().size()).isAtLeast(1);
+ assertThat(dataset.getExtendedPanId()).hasLength(8);
+ assertThat(dataset.getNetworkKey()).hasLength(16);
+ assertThat(dataset.getPskc()).hasLength(16);
+ assertThat(dataset.getMeshLocalPrefix().getPrefixLength()).isEqualTo(64);
+ assertThat(dataset.getMeshLocalPrefix().getRawAddress()[0]).isEqualTo((byte) 0xfd);
+ }
+ }
}
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
index 3a087c7..5863673 100644
--- a/thread/tests/unit/Android.bp
+++ b/thread/tests/unit/Android.bp
@@ -36,6 +36,7 @@
"ctstestrunner-axt",
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
+ "guava",
"guava-android-testlib",
"net-tests-utils",
"truth",
diff --git a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
index 78eb3d0..7284968 100644
--- a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
+++ b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
@@ -20,14 +20,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import android.net.IpPrefix;
import android.net.thread.ActiveOperationalDataset.Builder;
import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
import android.util.SparseArray;
@@ -61,7 +54,7 @@
// PAN ID: 0xD9A0
// PSKc: A245479C836D551B9CA557F7B9D351B4
// Security Policy: 672 onrcb
- private static final byte[] VALID_DATASET =
+ private static final byte[] VALID_DATASET_TLVS =
base16().decode(
"0E080000000000010000000300001335060004001FFFE002"
+ "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
@@ -83,7 +76,7 @@
@Test
public void fromThreadTlvs_containsUnknownTlvs_unknownTlvsRetained() {
- byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET, "AA01FFBB020102");
+ byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET_TLVS, "AA01FFBB020102");
ActiveOperationalDataset dataset1 =
ActiveOperationalDataset.fromThreadTlvs(datasetWithUnknownTlvs);
@@ -98,66 +91,8 @@
}
@Test
- public void createRandomDataset_fieldsAreRandomized() {
- // Always return the max bounded value
- doAnswer(invocation -> (int) invocation.getArgument(0) - 1)
- .when(mockRandom)
- .nextInt(anyInt());
- doAnswer(
- invocation -> {
- byte[] output = invocation.getArgument(0);
- for (int i = 0; i < output.length; ++i) {
- output[i] = (byte) (i + 10);
- }
- return null;
- })
- .when(mockRandom)
- .nextBytes(any(byte[].class));
- doAnswer(
- invocation -> {
- byte[] output = invocation.getArgument(0);
- for (int i = 0; i < output.length; ++i) {
- output[i] = (byte) (i + 30);
- }
- return null;
- })
- .when(mockSecureRandom)
- .nextBytes(any(byte[].class));
-
- ActiveOperationalDataset dataset =
- ActiveOperationalDataset.createRandomDataset(mockRandom, mockSecureRandom);
-
- assertThat(dataset.getActiveTimestamp())
- .isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
- assertThat(dataset.getExtendedPanId())
- .isEqualTo(new byte[] {10, 11, 12, 13, 14, 15, 16, 17});
- assertThat(dataset.getMeshLocalPrefix())
- .isEqualTo(new IpPrefix("fd0b:0c0d:0e0f:1011::/64"));
- verify(mockRandom, times(2)).nextBytes(any(byte[].class));
- assertThat(dataset.getPanId()).isEqualTo(0xfffe); // PAN ID <= 0xfffe
- verify(mockRandom, times(1)).nextInt(eq(0xffff));
- assertThat(dataset.getChannel()).isEqualTo(26);
- verify(mockRandom, times(1)).nextInt(eq(16));
- assertThat(dataset.getChannelPage()).isEqualTo(0);
- assertThat(dataset.getChannelMask().size()).isEqualTo(1);
- assertThat(dataset.getPskc())
- .isEqualTo(
- new byte[] {
- 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45
- });
- assertThat(dataset.getNetworkKey())
- .isEqualTo(
- new byte[] {
- 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45
- });
- verify(mockSecureRandom, times(2)).nextBytes(any(byte[].class));
- assertThat(dataset.getSecurityPolicy())
- .isEqualTo(new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}));
- }
-
- @Test
public void builder_buildWithTooLongTlvs_throwsIllegalState() {
- Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+ Builder builder = new Builder(ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET_TLVS));
for (int i = 0; i < 10; i++) {
builder.addUnknownTlv(i, new byte[20]);
}
@@ -167,7 +102,8 @@
@Test
public void builder_setUnknownTlvs_success() {
- ActiveOperationalDataset dataset1 = ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET);
+ ActiveOperationalDataset dataset1 =
+ ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET_TLVS);
SparseArray<byte[]> unknownTlvs = new SparseArray<>(2);
unknownTlvs.put(0x33, new byte[] {1, 2, 3});
unknownTlvs.put(0x44, new byte[] {1, 2, 3, 4});