Merge changes from topic "resolv_cancellation" into main
* changes:
Add CTS test for DnsResolver cancellation
Add DnsResolverTapTest
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 39009cb..c301397 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,15 +1,13 @@
[Builtin Hooks]
bpfmt = true
clang_format = true
-ktfmt = true
[Builtin Hooks Options]
clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp,hpp
-ktfmt = --kotlinlang-style
[Hook Scripts]
checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
-ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --no-verify-format -f ${PREUPLOAD_FILES}
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --disabled-rules comment-wrapping -f ${PREUPLOAD_FILES}
hidden_api_txt_checksorted_hook = ${REPO_ROOT}/tools/platform-compat/hiddenapi/checksorted_sha.sh ${PREUPLOAD_COMMIT} ${REPO_ROOT}
diff --git a/TEST_MAPPING b/TEST_MAPPING
index c1bc31e..1d2041b 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -427,6 +427,9 @@
"automotive-mumd-presubmit": [
{
"name": "CtsNetTestCases"
+ },
+ {
+ "name": "CtsNetTestCasesUpdateStatsPermission"
}
],
"imports": [
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 5cf5528..4d173a5 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -63,6 +63,7 @@
static_libs: [
"androidx.annotation_annotation",
"connectivity-net-module-utils-bpf",
+ "com.android.net.flags-aconfig-java",
"modules-utils-build",
"modules-utils-statemachine",
"networkstack-client",
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 0c05354..19dd492 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -155,7 +155,10 @@
"framework-connectivity",
"framework-connectivity-t",
"framework-tethering",
- ],
+ ] + select(release_flag("RELEASE_MOVE_VCN_TO_MAINLINE"), {
+ true: ["framework-connectivity-b"],
+ default: [],
+ }),
apex_available: ["com.android.tethering"],
// The bootclasspath_fragments that provide APIs on which this depends.
@@ -195,6 +198,7 @@
"android.net.http",
"android.net.netstats",
"android.net.util",
+ "android.net.vcn",
],
// The following packages and all their subpackages currently only
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 2f3307a..e2498e4 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -33,6 +33,10 @@
// Using for test only
"//cts/tests/netlegacy22.api",
+
+ // TODO: b/374174952 Remove it when VCN CTS is moved to Connectivity/
+ "//cts/tests/tests/vcn",
+
"//external/sl4a:__subpackages__",
"//frameworks/base/core/tests/bandwidthtests",
"//frameworks/base/core/tests/benchmarks",
diff --git a/Tethering/common/TetheringLib/api/current.txt b/Tethering/common/TetheringLib/api/current.txt
index d802177..932e801 100644
--- a/Tethering/common/TetheringLib/api/current.txt
+++ b/Tethering/common/TetheringLib/api/current.txt
@@ -1 +1,71 @@
// Signature format: 2.0
+package android.net {
+
+ public final class TetheringInterface implements android.os.Parcelable {
+ ctor public TetheringInterface(int, @NonNull String);
+ ctor @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public TetheringInterface(int, @NonNull String, @Nullable android.net.wifi.SoftApConfiguration);
+ method public int describeContents();
+ method @NonNull public String getInterface();
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable @RequiresPermission(value=android.Manifest.permission.NETWORK_SETTINGS, conditional=true) public android.net.wifi.SoftApConfiguration getSoftApConfiguration();
+ method public int getType();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringInterface> CREATOR;
+ }
+
+ public class TetheringManager {
+ method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.TetheringEventCallback);
+ method @RequiresPermission(value=android.Manifest.permission.TETHER_PRIVILEGED, conditional=true) public void startTethering(@NonNull android.net.TetheringManager.TetheringRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @RequiresPermission(value=android.Manifest.permission.TETHER_PRIVILEGED, conditional=true) public void stopTethering(@NonNull android.net.TetheringManager.TetheringRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StopTetheringCallback);
+ method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.ACCESS_NETWORK_STATE}) public void unregisterTetheringEventCallback(@NonNull android.net.TetheringManager.TetheringEventCallback);
+ field public static final int CONNECTIVITY_SCOPE_GLOBAL = 1; // 0x1
+ field public static final int TETHERING_WIFI = 0; // 0x0
+ field public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12; // 0xc
+ field public static final int TETHER_ERROR_DISABLE_FORWARDING_ERROR = 9; // 0x9
+ field @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public static final int TETHER_ERROR_DUPLICATE_REQUEST = 18; // 0x12
+ field public static final int TETHER_ERROR_ENABLE_FORWARDING_ERROR = 8; // 0x8
+ field public static final int TETHER_ERROR_ENTITLEMENT_UNKNOWN = 13; // 0xd
+ field public static final int TETHER_ERROR_IFACE_CFG_ERROR = 10; // 0xa
+ field public static final int TETHER_ERROR_INTERNAL_ERROR = 5; // 0x5
+ field public static final int TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION = 15; // 0xf
+ field public static final int TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14; // 0xe
+ field public static final int TETHER_ERROR_NO_ERROR = 0; // 0x0
+ field public static final int TETHER_ERROR_PROVISIONING_FAILED = 11; // 0xb
+ field public static final int TETHER_ERROR_SERVICE_UNAVAIL = 2; // 0x2
+ field public static final int TETHER_ERROR_TETHER_IFACE_ERROR = 6; // 0x6
+ field public static final int TETHER_ERROR_UNAVAIL_IFACE = 4; // 0x4
+ field public static final int TETHER_ERROR_UNKNOWN_IFACE = 1; // 0x1
+ field @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public static final int TETHER_ERROR_UNKNOWN_REQUEST = 17; // 0x11
+ field public static final int TETHER_ERROR_UNKNOWN_TYPE = 16; // 0x10
+ field public static final int TETHER_ERROR_UNSUPPORTED = 3; // 0x3
+ field public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = 7; // 0x7
+ }
+
+ public static interface TetheringManager.StartTetheringCallback {
+ method public default void onTetheringFailed(int);
+ method public default void onTetheringStarted();
+ }
+
+ @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public static interface TetheringManager.StopTetheringCallback {
+ method public default void onStopTetheringFailed(int);
+ method public default void onStopTetheringSucceeded();
+ }
+
+ public static interface TetheringManager.TetheringEventCallback {
+ method public default void onTetheredInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
+ }
+
+ public static final class TetheringManager.TetheringRequest implements android.os.Parcelable {
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public int describeContents();
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable public android.net.wifi.SoftApConfiguration getSoftApConfiguration();
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringManager.TetheringRequest> CREATOR;
+ }
+
+ public static class TetheringManager.TetheringRequest.Builder {
+ ctor public TetheringManager.TetheringRequest.Builder(int);
+ method @NonNull public android.net.TetheringManager.TetheringRequest build();
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setSoftApConfiguration(@Nullable android.net.wifi.SoftApConfiguration);
+ }
+
+}
+
diff --git a/Tethering/common/TetheringLib/api/module-lib-current.txt b/Tethering/common/TetheringLib/api/module-lib-current.txt
index e893894..3ba8e1b 100644
--- a/Tethering/common/TetheringLib/api/module-lib-current.txt
+++ b/Tethering/common/TetheringLib/api/module-lib-current.txt
@@ -22,7 +22,7 @@
method public boolean isTetheringSupported(@NonNull String);
method public void requestLatestTetheringEntitlementResult(int, @NonNull android.os.ResultReceiver, boolean);
method @Deprecated public int setUsbTethering(boolean);
- method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void startTethering(int, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
+ method @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void startTethering(int, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
method @Deprecated public int tether(@NonNull String);
method @Deprecated public int untether(@NonNull String);
}
@@ -47,9 +47,14 @@
}
public static final class TetheringManager.TetheringRequest implements android.os.Parcelable {
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable public String getInterfaceName();
method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable public String getPackageName();
method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public int getUid();
}
+ public static class TetheringManager.TetheringRequest.Builder {
+ method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public android.net.TetheringManager.TetheringRequest.Builder setInterfaceName(@Nullable String);
+ }
+
}
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index 0e85956..c0c0abc 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -19,26 +19,11 @@
field @NonNull public static final android.os.Parcelable.Creator<android.net.TetheredClient.AddressInfo> CREATOR;
}
- public final class TetheringInterface implements android.os.Parcelable {
- ctor public TetheringInterface(int, @NonNull String);
- ctor @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public TetheringInterface(int, @NonNull String, @Nullable android.net.wifi.SoftApConfiguration);
- method public int describeContents();
- method @NonNull public String getInterface();
- method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable @RequiresPermission(value=android.Manifest.permission.NETWORK_SETTINGS, conditional=true) public android.net.wifi.SoftApConfiguration getSoftApConfiguration();
- method public int getType();
- method public void writeToParcel(@NonNull android.os.Parcel, int);
- field @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringInterface> CREATOR;
- }
-
public class TetheringManager {
- method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.TetheringEventCallback);
- method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void requestLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.OnTetheringEntitlementResultListener);
- method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void startTethering(@NonNull android.net.TetheringManager.TetheringRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
- method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopAllTethering();
- method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopTethering(int);
- method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.ACCESS_NETWORK_STATE}) public void unregisterTetheringEventCallback(@NonNull android.net.TetheringManager.TetheringEventCallback);
+ method @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void requestLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.OnTetheringEntitlementResultListener);
+ method @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void stopAllTethering();
+ method @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void stopTethering(int);
field @Deprecated public static final String ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED";
- field public static final int CONNECTIVITY_SCOPE_GLOBAL = 1; // 0x1
field public static final int CONNECTIVITY_SCOPE_LOCAL = 2; // 0x2
field public static final String EXTRA_ACTIVE_LOCAL_ONLY = "android.net.extra.ACTIVE_LOCAL_ONLY";
field public static final String EXTRA_ACTIVE_TETHER = "tetherArray";
@@ -50,25 +35,7 @@
field public static final int TETHERING_NCM = 4; // 0x4
field public static final int TETHERING_USB = 1; // 0x1
field @FlaggedApi("com.android.net.flags.tethering_request_virtual") public static final int TETHERING_VIRTUAL = 7; // 0x7
- field public static final int TETHERING_WIFI = 0; // 0x0
field public static final int TETHERING_WIFI_P2P = 3; // 0x3
- field public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12; // 0xc
- field public static final int TETHER_ERROR_DISABLE_FORWARDING_ERROR = 9; // 0x9
- field public static final int TETHER_ERROR_ENABLE_FORWARDING_ERROR = 8; // 0x8
- field public static final int TETHER_ERROR_ENTITLEMENT_UNKNOWN = 13; // 0xd
- field public static final int TETHER_ERROR_IFACE_CFG_ERROR = 10; // 0xa
- field public static final int TETHER_ERROR_INTERNAL_ERROR = 5; // 0x5
- field public static final int TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION = 15; // 0xf
- field public static final int TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14; // 0xe
- field public static final int TETHER_ERROR_NO_ERROR = 0; // 0x0
- field public static final int TETHER_ERROR_PROVISIONING_FAILED = 11; // 0xb
- field public static final int TETHER_ERROR_SERVICE_UNAVAIL = 2; // 0x2
- field public static final int TETHER_ERROR_TETHER_IFACE_ERROR = 6; // 0x6
- field public static final int TETHER_ERROR_UNAVAIL_IFACE = 4; // 0x4
- field public static final int TETHER_ERROR_UNKNOWN_IFACE = 1; // 0x1
- field public static final int TETHER_ERROR_UNKNOWN_TYPE = 16; // 0x10
- field public static final int TETHER_ERROR_UNSUPPORTED = 3; // 0x3
- field public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = 7; // 0x7
field public static final int TETHER_HARDWARE_OFFLOAD_FAILED = 2; // 0x2
field public static final int TETHER_HARDWARE_OFFLOAD_STARTED = 1; // 0x1
field public static final int TETHER_HARDWARE_OFFLOAD_STOPPED = 0; // 0x0
@@ -78,11 +45,6 @@
method public void onTetheringEntitlementResult(int);
}
- public static interface TetheringManager.StartTetheringCallback {
- method public default void onTetheringFailed(int);
- method public default void onTetheringStarted();
- }
-
public static interface TetheringManager.TetheringEventCallback {
method public default void onClientsChanged(@NonNull java.util.Collection<android.net.TetheredClient>);
method public default void onError(@NonNull String, int);
@@ -93,31 +55,23 @@
method public default void onTetherableInterfacesChanged(@NonNull java.util.List<java.lang.String>);
method public default void onTetherableInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
method public default void onTetheredInterfacesChanged(@NonNull java.util.List<java.lang.String>);
- method public default void onTetheredInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
method public default void onTetheringSupported(boolean);
method public default void onUpstreamChanged(@Nullable android.net.Network);
}
public static final class TetheringManager.TetheringRequest implements android.os.Parcelable {
- method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public int describeContents();
method @Nullable public android.net.LinkAddress getClientStaticIpv4Address();
method public int getConnectivityScope();
method @Nullable public android.net.LinkAddress getLocalIpv4Address();
method public boolean getShouldShowEntitlementUi();
- method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @Nullable public android.net.wifi.SoftApConfiguration getSoftApConfiguration();
method public int getTetheringType();
method public boolean isExemptFromEntitlementCheck();
- method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") public void writeToParcel(@NonNull android.os.Parcel, int);
- field @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringManager.TetheringRequest> CREATOR;
}
public static class TetheringManager.TetheringRequest.Builder {
- ctor public TetheringManager.TetheringRequest.Builder(int);
- method @NonNull public android.net.TetheringManager.TetheringRequest build();
method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setConnectivityScope(int);
method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setExemptFromEntitlementCheck(boolean);
method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setShouldShowEntitlementUi(boolean);
- method @FlaggedApi("com.android.net.flags.tethering_with_soft_ap_config") @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setSoftApConfiguration(@Nullable android.net.wifi.SoftApConfiguration);
method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setStaticIpv4Addresses(@NonNull android.net.LinkAddress, @NonNull android.net.LinkAddress);
}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java
index 0464fe0..250179a 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java
@@ -21,7 +21,6 @@
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
-import android.annotation.SystemApi;
import android.net.TetheringManager.TetheringType;
import android.net.wifi.SoftApConfiguration;
import android.os.Parcel;
@@ -33,9 +32,8 @@
/**
* The mapping of tethering interface and type.
- * @hide
*/
-@SystemApi
+@SuppressLint("UnflaggedApi")
public final class TetheringInterface implements Parcelable {
private final int mType;
private final String mInterface;
@@ -57,12 +55,14 @@
}
/** Get tethering type. */
+ @SuppressLint("UnflaggedApi")
public int getType() {
return mType;
}
/** Get tethering interface. */
@NonNull
+ @SuppressLint("UnflaggedApi")
public String getInterface() {
return mInterface;
}
@@ -75,11 +75,13 @@
@FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
@RequiresPermission(value = android.Manifest.permission.NETWORK_SETTINGS, conditional = true)
@Nullable
+ @SuppressLint("UnflaggedApi")
public SoftApConfiguration getSoftApConfiguration() {
return mSoftApConfig;
}
@Override
+ @SuppressLint("UnflaggedApi")
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(mType);
dest.writeString(mInterface);
@@ -87,11 +89,13 @@
}
@Override
+ @SuppressLint("UnflaggedApi")
public int hashCode() {
return Objects.hash(mType, mInterface, mSoftApConfig);
}
@Override
+ @SuppressLint("UnflaggedApi")
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof TetheringInterface)) return false;
final TetheringInterface other = (TetheringInterface) obj;
@@ -100,11 +104,13 @@
}
@Override
+ @SuppressLint("UnflaggedApi")
public int describeContents() {
return 0;
}
@NonNull
+ @SuppressLint("UnflaggedApi")
public static final Creator<TetheringInterface> CREATOR = new Creator<TetheringInterface>() {
@NonNull
@Override
@@ -116,6 +122,7 @@
@NonNull
@Override
+ @SuppressLint("UnflaggedApi")
public TetheringInterface[] newArray(int size) {
return new TetheringInterface[size];
}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index bc771da..e81cbf0 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -63,9 +63,8 @@
* <p> The primary responsibilities of this class are to provide the APIs for applications to
* start tethering, stop tethering, query configuration and query status.
*
- * @hide
*/
-@SystemApi
+@SuppressLint({"NotCloseable", "UnflaggedApi"})
public class TetheringManager {
private static final String TAG = TetheringManager.class.getSimpleName();
private static final int DEFAULT_TIMEOUT_MS = 60_000;
@@ -95,36 +94,46 @@
* {@code TetheringManager.EXTRA_ERRORED_TETHER} to indicate
* the current state of tethering. Each include a list of
* interface names in that state (may be empty).
+ * @hide
*
* @deprecated New client should use TetheringEventCallback instead.
*/
@Deprecated
+ @SystemApi
public static final String ACTION_TETHER_STATE_CHANGED =
"android.net.conn.TETHER_STATE_CHANGED";
/**
* gives a String[] listing all the interfaces configured for
* tethering and currently available for tethering.
+ * @hide
*/
+ @SystemApi
public static final String EXTRA_AVAILABLE_TETHER = "availableArray";
/**
* gives a String[] listing all the interfaces currently in local-only
* mode (ie, has DHCPv4+IPv6-ULA support and no packet forwarding)
+ * @hide
*/
+ @SystemApi
public static final String EXTRA_ACTIVE_LOCAL_ONLY = "android.net.extra.ACTIVE_LOCAL_ONLY";
/**
* gives a String[] listing all the interfaces currently tethered
* (ie, has DHCPv4 support and packets potentially forwarded/NATed)
+ * @hide
*/
+ @SystemApi
public static final String EXTRA_ACTIVE_TETHER = "tetherArray";
/**
* gives a String[] listing all the interfaces we tried to tether and
* failed. Use {@link #getLastTetherError} to find the error code
* for any interfaces listed here.
+ * @hide
*/
+ @SystemApi
public static final String EXTRA_ERRORED_TETHER = "erroredArray";
/** @hide */
@@ -136,6 +145,7 @@
TETHERING_WIFI_P2P,
TETHERING_NCM,
TETHERING_ETHERNET,
+ TETHERING_VIRTUAL,
})
public @interface TetheringType {
}
@@ -143,44 +153,57 @@
/**
* Invalid tethering type.
* @see #startTethering.
+ * @hide
*/
+ @SystemApi
public static final int TETHERING_INVALID = -1;
/**
* Wifi tethering type.
* @see #startTethering.
*/
+ @SuppressLint("UnflaggedApi")
public static final int TETHERING_WIFI = 0;
/**
* USB tethering type.
* @see #startTethering.
+ * @hide
*/
+ @SystemApi
public static final int TETHERING_USB = 1;
/**
* Bluetooth tethering type.
* @see #startTethering.
+ * @hide
*/
+ @SystemApi
public static final int TETHERING_BLUETOOTH = 2;
/**
* Wifi P2p tethering type.
* Wifi P2p tethering is set through events automatically, and don't
* need to start from #startTethering.
+ * @hide
*/
+ @SystemApi
public static final int TETHERING_WIFI_P2P = 3;
/**
* Ncm local tethering type.
* @see #startTethering(TetheringRequest, Executor, StartTetheringCallback)
+ * @hide
*/
+ @SystemApi
public static final int TETHERING_NCM = 4;
/**
* Ethernet tethering type.
* @see #startTethering(TetheringRequest, Executor, StartTetheringCallback)
+ * @hide
*/
+ @SystemApi
public static final int TETHERING_ETHERNET = 5;
/**
@@ -253,30 +276,62 @@
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
TETHER_ERROR_SERVICE_UNAVAIL,
+ TETHER_ERROR_UNSUPPORTED,
TETHER_ERROR_INTERNAL_ERROR,
TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION,
TETHER_ERROR_UNKNOWN_TYPE,
+ TETHER_ERROR_DUPLICATE_REQUEST,
})
public @interface StartTetheringError {
}
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ TETHER_ERROR_NO_ERROR,
+ TETHER_ERROR_UNKNOWN_REQUEST,
+ })
+ public @interface StopTetheringError {
+ }
+
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_NO_ERROR = 0;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_UNKNOWN_IFACE = 1;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_SERVICE_UNAVAIL = 2;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_UNSUPPORTED = 3;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_UNAVAIL_IFACE = 4;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_INTERNAL_ERROR = 5;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_TETHER_IFACE_ERROR = 6;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = 7;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_ENABLE_FORWARDING_ERROR = 8;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_DISABLE_FORWARDING_ERROR = 9;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_IFACE_CFG_ERROR = 10;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_PROVISIONING_FAILED = 11;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_ENTITLEMENT_UNKNOWN = 13;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION = 15;
+ @SuppressLint("UnflaggedApi")
public static final int TETHER_ERROR_UNKNOWN_TYPE = 16;
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ public static final int TETHER_ERROR_UNKNOWN_REQUEST = 17;
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ public static final int TETHER_ERROR_DUPLICATE_REQUEST = 18;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@@ -288,11 +343,23 @@
public @interface TetherOffloadStatus {
}
- /** Tethering offload status is stopped. */
+ /**
+ * Tethering offload status is stopped.
+ * @hide
+ */
+ @SystemApi
public static final int TETHER_HARDWARE_OFFLOAD_STOPPED = 0;
- /** Tethering offload status is started. */
+ /**
+ * Tethering offload status is started.
+ * @hide
+ */
+ @SystemApi
public static final int TETHER_HARDWARE_OFFLOAD_STARTED = 1;
- /** Fail to start tethering offload. */
+ /**
+ * Fail to start tethering offload.
+ * @hide
+ */
+ @SystemApi
public static final int TETHER_HARDWARE_OFFLOAD_FAILED = 2;
/**
@@ -321,7 +388,9 @@
// up and be sent from a worker thread; later, they are always sent from the caller thread.
// Considering that it's just oneway binder calls, and ordering is preserved, this seems
// better than inconsistent behavior persisting after boot.
- if (connector != null) {
+ // If system server restarted, mConnectorSupplier might temporarily return a stale (i.e.
+ // dead) version of TetheringService.
+ if (connector != null && connector.isBinderAlive()) {
mConnector = ITetheringConnector.Stub.asInterface(connector);
} else {
startPollingForConnector();
@@ -356,9 +425,8 @@
} catch (InterruptedException e) {
// Not much to do here, the system needs to wait for the connector
}
-
final IBinder connector = mConnectorSupplier.get();
- if (connector != null) {
+ if (connector != null && connector.isBinderAlive()) {
onTetheringConnected(ITetheringConnector.Stub.asInterface(connector));
return;
}
@@ -686,11 +754,14 @@
* Indicates that this tethering connection will provide connectivity beyond this device (e.g.,
* global Internet access).
*/
+ @SuppressLint("UnflaggedApi")
public static final int CONNECTIVITY_SCOPE_GLOBAL = 1;
/**
* Indicates that this tethering connection will only provide local connectivity.
+ * @hide
*/
+ @SystemApi
public static final int CONNECTIVITY_SCOPE_LOCAL = 2;
/**
@@ -718,6 +789,7 @@
/**
* Use with {@link #startTethering} to specify additional parameters when starting tethering.
*/
+ @SuppressLint("UnflaggedApi")
public static final class TetheringRequest implements Parcelable {
/** A configuration set for TetheringRequest. */
private final TetheringRequestParcel mRequestParcel;
@@ -761,10 +833,12 @@
}
/** Builder used to create TetheringRequest. */
+ @SuppressLint({"UnflaggedApi", "StaticFinalBuilder"})
public static class Builder {
private final TetheringRequestParcel mBuilderParcel;
/** Default constructor of Builder. */
+ @SuppressLint("UnflaggedApi")
public Builder(@TetheringType final int type) {
mBuilderParcel = new TetheringRequestParcel();
mBuilderParcel.tetheringType = type;
@@ -775,6 +849,7 @@
mBuilderParcel.connectivityScope = getDefaultConnectivityScope(type);
mBuilderParcel.uid = Process.INVALID_UID;
mBuilderParcel.softApConfig = null;
+ mBuilderParcel.interfaceName = null;
}
/**
@@ -785,7 +860,9 @@
*
* @param localIPv4Address The preferred local IPv4 link address to use.
* @param clientAddress The static client address.
+ * @hide
*/
+ @SystemApi
@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
@NonNull
public Builder setStaticIpv4Addresses(@NonNull final LinkAddress localIPv4Address,
@@ -801,7 +878,11 @@
return this;
}
- /** Start tethering without entitlement checks. */
+ /**
+ * Start tethering without entitlement checks.
+ * @hide
+ */
+ @SystemApi
@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
@NonNull
public Builder setExemptFromEntitlementCheck(boolean exempt) {
@@ -812,7 +893,9 @@
/**
* If an entitlement check is needed, sets whether to show the entitlement UI or to
* perform a silent entitlement check. By default, the entitlement UI is shown.
+ * @hide
*/
+ @SystemApi
@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
@NonNull
public Builder setShouldShowEntitlementUi(boolean showUi) {
@@ -821,8 +904,39 @@
}
/**
- * Sets the connectivity scope to be provided by this tethering downstream.
+ * Sets the name of the interface. Currently supported only for
+ * - {@link #TETHERING_VIRTUAL}.
+ * - {@link #TETHERING_WIFI} (for Local-only Hotspot)
+ * - {@link #TETHERING_WIFI_P2P}
+ * @hide
*/
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.NETWORK_SETTINGS,
+ android.Manifest.permission.NETWORK_STACK,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ })
+ @NonNull
+ @SystemApi(client = MODULE_LIBRARIES)
+ public Builder setInterfaceName(@Nullable final String interfaceName) {
+ switch (mBuilderParcel.tetheringType) {
+ case TETHERING_VIRTUAL:
+ case TETHERING_WIFI_P2P:
+ case TETHERING_WIFI:
+ break;
+ default:
+ throw new IllegalArgumentException("Interface name cannot be set for"
+ + " tethering type " + interfaceName);
+ }
+ mBuilderParcel.interfaceName = interfaceName;
+ return this;
+ }
+
+ /**
+ * Sets the connectivity scope to be provided by this tethering downstream.
+ * @hide
+ */
+ @SystemApi
@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
@NonNull
public Builder setConnectivityScope(@ConnectivityScope int scope) {
@@ -842,6 +956,8 @@
* If TETHERING_WIFI is already enabled and a new request is made with a different
* SoftApConfiguration, the request will be accepted if the device can support an
* additional tethering Wi-Fi AP interface. Otherwise, the request will be rejected.
+ * </p>
+ * Non-system callers using TETHERING_WIFI must specify a SoftApConfiguration.
*
* @param softApConfig SoftApConfiguration to use.
* @throws IllegalArgumentException if the tethering type isn't TETHERING_WIFI.
@@ -860,6 +976,7 @@
/** Build {@link TetheringRequest} with the currently set configuration. */
@NonNull
+ @SuppressLint("UnflaggedApi")
public TetheringRequest build() {
return new TetheringRequest(mBuilderParcel);
}
@@ -868,7 +985,9 @@
/**
* Get the local IPv4 address, if one was configured with
* {@link Builder#setStaticIpv4Addresses}.
+ * @hide
*/
+ @SystemApi
@Nullable
public LinkAddress getLocalIpv4Address() {
return mRequestParcel.localIPv4Address;
@@ -877,35 +996,64 @@
/**
* Get the static IPv4 address of the client, if one was configured with
* {@link Builder#setStaticIpv4Addresses}.
+ * @hide
*/
+ @SystemApi
@Nullable
public LinkAddress getClientStaticIpv4Address() {
return mRequestParcel.staticClientAddress;
}
- /** Get tethering type. */
+ /**
+ * Get tethering type.
+ * @hide
+ */
+ @SystemApi
@TetheringType
public int getTetheringType() {
return mRequestParcel.tetheringType;
}
- /** Get connectivity type */
+ /**
+ * Get connectivity type
+ * @hide
+ */
+ @SystemApi
@ConnectivityScope
public int getConnectivityScope() {
return mRequestParcel.connectivityScope;
}
- /** Check if exempt from entitlement check. */
+ /**
+ * Check if exempt from entitlement check.
+ * @hide
+ */
+ @SystemApi
public boolean isExemptFromEntitlementCheck() {
return mRequestParcel.exemptFromEntitlementCheck;
}
- /** Check if show entitlement ui. */
+ /**
+ * Check if show entitlement ui.
+ * @hide
+ */
+ @SystemApi
public boolean getShouldShowEntitlementUi() {
return mRequestParcel.showProvisioningUi;
}
/**
+ * Get interface name.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ @Nullable
+ @SystemApi(client = MODULE_LIBRARIES)
+ public String getInterfaceName() {
+ return mRequestParcel.interfaceName;
+ }
+
+ /**
* Check whether the two addresses are ipv4 and in the same prefix.
* @hide
*/
@@ -996,7 +1144,11 @@
return mRequestParcel;
}
- /** String of TetheringRequest detail. */
+ /**
+ * String of TetheringRequest detail.
+ * @hide
+ */
+ @SystemApi
public String toString() {
StringJoiner sj = new StringJoiner(", ", "TetheringRequest[ ", " ]");
sj.add(typeToString(mRequestParcel.tetheringType));
@@ -1022,13 +1174,31 @@
if (mRequestParcel.packageName != null) {
sj.add("packageName=" + mRequestParcel.packageName);
}
+ if (mRequestParcel.interfaceName != null) {
+ sj.add("interfaceName=" + mRequestParcel.interfaceName);
+ }
return sj.toString();
}
+ /**
+ * @hide
+ */
+ @SystemApi
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof TetheringRequest otherRequest)) return false;
+ if (!equalsIgnoreUidPackage(otherRequest)) return false;
+ TetheringRequestParcel parcel = getParcel();
+ TetheringRequestParcel otherParcel = otherRequest.getParcel();
+ return parcel.uid == otherParcel.uid
+ && Objects.equals(parcel.packageName, otherParcel.packageName);
+ }
+
+ /**
+ * @hide
+ */
+ public boolean equalsIgnoreUidPackage(TetheringRequest otherRequest) {
TetheringRequestParcel parcel = getParcel();
TetheringRequestParcel otherParcel = otherRequest.getParcel();
return parcel.tetheringType == otherParcel.tetheringType
@@ -1038,27 +1208,32 @@
&& parcel.showProvisioningUi == otherParcel.showProvisioningUi
&& parcel.connectivityScope == otherParcel.connectivityScope
&& Objects.equals(parcel.softApConfig, otherParcel.softApConfig)
- && parcel.uid == otherParcel.uid
- && Objects.equals(parcel.packageName, otherParcel.packageName);
+ && Objects.equals(parcel.interfaceName, otherParcel.interfaceName);
}
+ /**
+ * @hide
+ */
+ @SystemApi
@Override
public int hashCode() {
TetheringRequestParcel parcel = getParcel();
return Objects.hash(parcel.tetheringType, parcel.localIPv4Address,
parcel.staticClientAddress, parcel.exemptFromEntitlementCheck,
parcel.showProvisioningUi, parcel.connectivityScope, parcel.softApConfig,
- parcel.uid, parcel.packageName);
+ parcel.uid, parcel.packageName, parcel.interfaceName);
}
}
/**
* Callback for use with {@link #startTethering} to find out whether tethering succeeded.
*/
+ @SuppressLint("UnflaggedApi")
public interface StartTetheringCallback {
/**
* Called when tethering has been successfully started.
*/
+ @SuppressLint("UnflaggedApi")
default void onTetheringStarted() {}
/**
@@ -1066,26 +1241,40 @@
*
* @param error The error that caused the failure.
*/
+ @SuppressLint("UnflaggedApi")
default void onTetheringFailed(@StartTetheringError final int error) {}
}
/**
+ * Callback for use with {@link #stopTethering} to find out whether stop tethering succeeded.
+ */
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ public interface StopTetheringCallback {
+ /**
+ * Called when tethering has been successfully stopped.
+ */
+ default void onStopTetheringSucceeded() {}
+
+ /**
+ * Called when starting tethering failed.
+ *
+ * @param error The error that caused the failure.
+ */
+ default void onStopTetheringFailed(@StopTetheringError final int error) {}
+ }
+
+ /**
* Starts tethering and runs tether provisioning for the given type if needed. If provisioning
* fails, stopTethering will be called automatically.
*
- * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
- * fail if a tethering entitlement check is required.
- *
* @param request a {@link TetheringRequest} which can specify the preferred configuration.
* @param executor {@link Executor} to specify the thread upon which the callback of
* TetheringRequest will be invoked.
* @param callback A callback that will be called to indicate the success status of the
* tethering start request.
*/
- @RequiresPermission(anyOf = {
- android.Manifest.permission.TETHER_PRIVILEGED,
- android.Manifest.permission.WRITE_SETTINGS
- })
+ @RequiresPermission(value = android.Manifest.permission.TETHER_PRIVILEGED, conditional = true)
+ @SuppressLint("UnflaggedApi")
public void startTethering(@NonNull final TetheringRequest request,
@NonNull final Executor executor, @NonNull final StartTetheringCallback callback) {
final String callerPkg = mContext.getOpPackageName();
@@ -1111,18 +1300,12 @@
* Starts tethering and runs tether provisioning for the given type if needed. If provisioning
* fails, stopTethering will be called automatically.
*
- * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
- * fail if a tethering entitlement check is required.
- *
* @param type The tethering type, on of the {@code TetheringManager#TETHERING_*} constants.
* @param executor {@link Executor} to specify the thread upon which the callback of
* TetheringRequest will be invoked.
* @hide
*/
- @RequiresPermission(anyOf = {
- android.Manifest.permission.TETHER_PRIVILEGED,
- android.Manifest.permission.WRITE_SETTINGS
- })
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
@SystemApi(client = MODULE_LIBRARIES)
public void startTethering(int type, @NonNull final Executor executor,
@NonNull final StartTetheringCallback callback) {
@@ -1133,13 +1316,10 @@
* Stops tethering for the given type. Also cancels any provisioning rechecks for that type if
* applicable.
*
- * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
- * fail if a tethering entitlement check is required.
+ * @hide
*/
- @RequiresPermission(anyOf = {
- android.Manifest.permission.TETHER_PRIVILEGED,
- android.Manifest.permission.WRITE_SETTINGS
- })
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+ @SystemApi
public void stopTethering(@TetheringType final int type) {
final String callerPkg = mContext.getOpPackageName();
Log.i(TAG, "stopTethering caller:" + callerPkg);
@@ -1157,9 +1337,22 @@
}
/**
+ * Stops tethering for the given request. Operation will fail with
+ * {@link #TETHER_ERROR_UNKNOWN_REQUEST} if there is no request that matches it.
+ */
+ @RequiresPermission(value = android.Manifest.permission.TETHER_PRIVILEGED, conditional = true)
+ @FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
+ public void stopTethering(@NonNull TetheringRequest request,
+ @NonNull final Executor executor, @NonNull final StopTetheringCallback callback) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
* Callback for use with {@link #getLatestTetheringEntitlementResult} to find out whether
* entitlement succeeded.
+ * @hide
*/
+ @SystemApi
public interface OnTetheringEntitlementResultListener {
/**
* Called to notify entitlement result.
@@ -1180,9 +1373,6 @@
* {@link #TETHER_ERROR_ENTITLEMENT_UNKNOWN} will be returned. If {@code showEntitlementUi} is
* true, entitlement will be run.
*
- * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
- * fail if a tethering entitlement check is required.
- *
* @param type the downstream type of tethering. Must be one of {@code #TETHERING_*} constants.
* @param showEntitlementUi a boolean indicating whether to check result for the UI-based
* entitlement check or the silent entitlement check.
@@ -1190,11 +1380,10 @@
* @param listener an {@link OnTetheringEntitlementResultListener} which will be called to
* notify the caller of the result of entitlement check. The listener may be called zero
* or one time.
+ * @hide
*/
- @RequiresPermission(anyOf = {
- android.Manifest.permission.TETHER_PRIVILEGED,
- android.Manifest.permission.WRITE_SETTINGS
- })
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
public void requestLatestTetheringEntitlementResult(@TetheringType int type,
boolean showEntitlementUi,
@NonNull Executor executor,
@@ -1238,6 +1427,7 @@
* Callback for use with {@link registerTetheringEventCallback} to find out tethering
* upstream status.
*/
+ @SuppressLint("UnflaggedApi")
public interface TetheringEventCallback {
/**
* Called when tethering supported status changed.
@@ -1249,7 +1439,9 @@
* policy restrictions.
*
* @param supported whether any tethering type is supported.
+ * @hide
*/
+ @SystemApi
default void onTetheringSupported(boolean supported) {}
/**
@@ -1274,7 +1466,9 @@
*
* @param network the {@link Network} of tethering upstream. Null means tethering doesn't
* have any upstream.
+ * @hide
*/
+ @SystemApi
default void onUpstreamChanged(@Nullable Network network) {}
/**
@@ -1301,7 +1495,9 @@
* <p>This will be called immediately after the callback is registered, and may be called
* multiple times later upon changes.
* @param interfaces The list of tetherable interface names.
+ * @hide
*/
+ @SystemApi
default void onTetherableInterfacesChanged(@NonNull List<String> interfaces) {}
/**
@@ -1311,7 +1507,9 @@
* <p>This will be called immediately after the callback is registered, and may be called
* multiple times later upon changes.
* @param interfaces The set of TetheringInterface of currently tetherable interface.
+ * @hide
*/
+ @SystemApi
default void onTetherableInterfacesChanged(@NonNull Set<TetheringInterface> interfaces) {
// By default, the new callback calls the old callback, so apps
// implementing the old callback just work.
@@ -1324,7 +1522,9 @@
* <p>This will be called immediately after the callback is registered, and may be called
* multiple times later upon changes.
* @param interfaces The lit of 0 or more String of currently tethered interface names.
+ * @hide
*/
+ @SystemApi
default void onTetheredInterfacesChanged(@NonNull List<String> interfaces) {}
/**
@@ -1335,6 +1535,7 @@
* @param interfaces The set of 0 or more TetheringInterface of currently tethered
* interface.
*/
+ @SuppressLint("UnflaggedApi")
default void onTetheredInterfacesChanged(@NonNull Set<TetheringInterface> interfaces) {
// By default, the new callback calls the old callback, so apps
// implementing the old callback just work.
@@ -1347,7 +1548,9 @@
* <p>This will be called immediately after the callback is registered, and may be called
* multiple times later upon changes.
* @param interfaces The list of 0 or more String of active local-only interface names.
+ * @hide
*/
+ @SystemApi
default void onLocalOnlyInterfacesChanged(@NonNull List<String> interfaces) {}
/**
@@ -1357,7 +1560,9 @@
* multiple times later upon changes.
* @param interfaces The set of 0 or more TetheringInterface of active local-only
* interface.
+ * @hide
*/
+ @SystemApi
default void onLocalOnlyInterfacesChanged(@NonNull Set<TetheringInterface> interfaces) {
// By default, the new callback calls the old callback, so apps
// implementing the old callback just work.
@@ -1371,7 +1576,9 @@
* on the interface is an error, and may be called multiple times later upon changes.
* @param ifName Name of the interface.
* @param error One of {@code TetheringManager#TETHER_ERROR_*}.
+ * @hide
*/
+ @SystemApi
default void onError(@NonNull String ifName, @TetheringIfaceError int error) {}
/**
@@ -1381,7 +1588,9 @@
* on the interface is an error, and may be called multiple times later upon changes.
* @param iface The interface that experienced the error.
* @param error One of {@code TetheringManager#TETHER_ERROR_*}.
+ * @hide
*/
+ @SystemApi
default void onError(@NonNull TetheringInterface iface, @TetheringIfaceError int error) {
// By default, the new callback calls the old callback, so apps
// implementing the old callback just work.
@@ -1398,7 +1607,9 @@
* clients may still be reported by this callback after disconnection as the system cannot
* determine if they are still connected.
* @param clients The new set of tethered clients; the collection is not ordered.
+ * @hide
*/
+ @SystemApi
default void onClientsChanged(@NonNull Collection<TetheredClient> clients) {}
/**
@@ -1406,7 +1617,9 @@
*
* <p>This will be called immediately after the callback is registered.
* @param status The offload status.
+ * @hide
*/
+ @SystemApi
default void onOffloadStatusChanged(@TetherOffloadStatus int status) {}
}
@@ -1500,6 +1713,7 @@
* @param callback the callback to be called when tethering has change events.
*/
@RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
+ @SuppressLint("UnflaggedApi")
public void registerTetheringEventCallback(@NonNull Executor executor,
@NonNull TetheringEventCallback callback) {
Objects.requireNonNull(executor);
@@ -1658,6 +1872,7 @@
Manifest.permission.TETHER_PRIVILEGED,
Manifest.permission.ACCESS_NETWORK_STATE
})
+ @SuppressLint("UnflaggedApi")
public void unregisterTetheringEventCallback(@NonNull final TetheringEventCallback callback) {
Objects.requireNonNull(callback);
@@ -1848,14 +2063,10 @@
/**
* Stop all active tethering.
- *
- * <p>Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will
- * fail if a tethering entitlement check is required.
+ * @hide
*/
- @RequiresPermission(anyOf = {
- android.Manifest.permission.TETHER_PRIVILEGED,
- android.Manifest.permission.WRITE_SETTINGS
- })
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
public void stopAllTethering() {
final String callerPkg = mContext.getOpPackageName();
Log.i(TAG, "stopAllTethering caller:" + callerPkg);
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
index 789d5bb..97c9b9a 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
@@ -33,4 +33,5 @@
SoftApConfiguration softApConfig;
int uid;
String packageName;
+ String interfaceName;
}
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index 47e2848..6d857b1 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -1,3 +1,6 @@
+# Keep JNI registered methods
+-keepclasseswithmembers,includedescriptorclasses class * { native <methods>; }
+
# Keep class's integer static field for MessageUtils to parsing their name.
-keepclassmembers class com.android.server.**,android.net.**,com.android.networkstack.** {
static final % POLICY_*;
@@ -7,18 +10,6 @@
static final % EVENT_*;
}
--keep class com.android.networkstack.tethering.util.BpfMap {
- native <methods>;
-}
-
--keep class com.android.networkstack.tethering.util.TcUtils {
- native <methods>;
-}
-
--keep class com.android.networkstack.tethering.util.TetheringUtils {
- native <methods>;
-}
-
# Ensure runtime-visible field annotations are kept when using R8 full mode.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
-keep interface com.android.networkstack.tethering.util.Struct$Field {
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index ebc9e4e..6229f6d 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -16,9 +16,16 @@
package android.net.ip;
+import static android.net.INetd.LOCAL_NET_ID;
import static android.net.RouteInfo.RTN_UNICAST;
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_NCM;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.TetheringManager.TETHERING_WIGIG;
import static android.net.TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
@@ -45,7 +52,6 @@
import android.net.MacAddress;
import android.net.RouteInfo;
import android.net.TetheredClient;
-import android.net.TetheringManager;
import android.net.TetheringManager.TetheringRequest;
import android.net.dhcp.DhcpLeaseParcelable;
import android.net.dhcp.DhcpServerCallbacks;
@@ -168,10 +174,10 @@
/**
* Request Tethering change.
*
- * @param request the TetheringRequest this IpServer was enabled with.
+ * @param tetheringType the downstream type of this IpServer.
* @param enabled enable or disable tethering.
*/
- public void requestEnableTethering(TetheringRequest request, boolean enabled) { }
+ public void requestEnableTethering(int tetheringType, boolean enabled) { }
}
/** Capture IpServer dependencies, for injection. */
@@ -589,8 +595,8 @@
@NonNull final Inet4Address dnsServer, @NonNull LinkAddress serverAddr,
@Nullable Inet4Address clientAddr) {
final boolean changePrefixOnDecline =
- (mInterfaceType == TetheringManager.TETHERING_NCM && clientAddr == null);
- final int subnetPrefixLength = mInterfaceType == TetheringManager.TETHERING_WIFI_P2P
+ (mInterfaceType == TETHERING_NCM && clientAddr == null);
+ final int subnetPrefixLength = mInterfaceType == TETHERING_WIFI_P2P
? mP2pLeasesSubnetPrefixLength : 0 /* default value */;
return new DhcpServingParamsParcelExt()
@@ -690,10 +696,10 @@
final IpPrefix ipv4Prefix = asIpPrefix(mIpv4Address);
final Boolean setIfaceUp;
- if (mInterfaceType == TetheringManager.TETHERING_WIFI
- || mInterfaceType == TetheringManager.TETHERING_WIFI_P2P
- || mInterfaceType == TetheringManager.TETHERING_ETHERNET
- || mInterfaceType == TetheringManager.TETHERING_WIGIG) {
+ if (mInterfaceType == TETHERING_WIFI
+ || mInterfaceType == TETHERING_WIFI_P2P
+ || mInterfaceType == TETHERING_ETHERNET
+ || mInterfaceType == TETHERING_WIGIG) {
// The WiFi and Ethernet stack has ownership of the interface up/down state.
// It is unclear whether the Bluetooth or USB stacks will manage their own
// state.
@@ -719,12 +725,12 @@
private boolean shouldNotConfigureBluetoothInterface() {
// Before T, bluetooth tethering configures the interface elsewhere.
- return (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) && !SdkLevel.isAtLeastT();
+ return (mInterfaceType == TETHERING_BLUETOOTH) && !SdkLevel.isAtLeastT();
}
private boolean shouldUseWifiP2pDedicatedIp() {
return mIsWifiP2pDedicatedIpEnabled
- && mInterfaceType == TetheringManager.TETHERING_WIFI_P2P;
+ && mInterfaceType == TETHERING_WIFI_P2P;
}
private LinkAddress requestIpv4Address(final int scope, final boolean useLastAddress) {
@@ -844,12 +850,13 @@
}
}
- private void removeRoutesFromLocalNetwork(@NonNull final List<RouteInfo> toBeRemoved) {
- final int removalFailures = NetdUtils.removeRoutesFromLocalNetwork(
- mNetd, toBeRemoved);
+ private void removeRoutesFromNetworkAndLinkProperties(int netId,
+ @NonNull final List<RouteInfo> toBeRemoved) {
+ final int removalFailures = NetdUtils.removeRoutesFromNetwork(
+ mNetd, netId, toBeRemoved);
if (removalFailures > 0) {
- mLog.e(String.format("Failed to remove %d IPv6 routes from local table.",
- removalFailures));
+ mLog.e("Failed to remove " + removalFailures
+ + " IPv6 routes from network " + netId + ".");
}
for (RouteInfo route : toBeRemoved) mLinkProperties.removeRoute(route);
@@ -879,14 +886,15 @@
}
}
- private void addRoutesToLocalNetwork(@NonNull final List<RouteInfo> toBeAdded) {
+ private void addRoutesToNetworkAndLinkProperties(int netId,
+ @NonNull final List<RouteInfo> toBeAdded) {
// It's safe to call addInterfaceToNetwork() even if
- // the interface is already in the local_network.
- addInterfaceToNetwork(INetd.LOCAL_NET_ID, mIfaceName);
+ // the interface is already in the network.
+ addInterfaceToNetwork(netId, mIfaceName);
try {
// Add routes from local network. Note that adding routes that
// already exist does not cause an error (EEXIST is silently ignored).
- NetdUtils.addRoutesToLocalNetwork(mNetd, mIfaceName, toBeAdded);
+ NetdUtils.addRoutesToNetwork(mNetd, netId, mIfaceName, toBeAdded);
} catch (IllegalStateException e) {
mLog.e("Failed to add IPv4/v6 routes to local table: " + e);
return;
@@ -899,7 +907,8 @@
ArraySet<IpPrefix> deprecatedPrefixes, ArraySet<IpPrefix> newPrefixes) {
// [1] Remove the routes that are deprecated.
if (!deprecatedPrefixes.isEmpty()) {
- removeRoutesFromLocalNetwork(getLocalRoutesFor(mIfaceName, deprecatedPrefixes));
+ removeRoutesFromNetworkAndLinkProperties(LOCAL_NET_ID,
+ getLocalRoutesFor(mIfaceName, deprecatedPrefixes));
}
// [2] Add only the routes that have not previously been added.
@@ -910,7 +919,8 @@
}
if (!addedPrefixes.isEmpty()) {
- addRoutesToLocalNetwork(getLocalRoutesFor(mIfaceName, addedPrefixes));
+ addRoutesToNetworkAndLinkProperties(LOCAL_NET_ID,
+ getLocalRoutesFor(mIfaceName, addedPrefixes));
}
}
}
@@ -1114,7 +1124,8 @@
}
try {
- NetdUtils.tetherInterface(mNetd, mIfaceName, asIpPrefix(mIpv4Address));
+ NetdUtils.tetherInterface(mNetd, LOCAL_NET_ID, mIfaceName,
+ asIpPrefix(mIpv4Address));
} catch (RemoteException | ServiceSpecificException | IllegalStateException e) {
mLog.e("Error Tethering", e);
mLastError = TETHER_ERROR_TETHER_IFACE_ERROR;
@@ -1136,7 +1147,7 @@
stopIPv6();
try {
- NetdUtils.untetherInterface(mNetd, mIfaceName);
+ NetdUtils.untetherInterface(mNetd, LOCAL_NET_ID, mIfaceName);
} catch (RemoteException | ServiceSpecificException e) {
mLastError = TETHER_ERROR_UNTETHER_IFACE_ERROR;
mLog.e("Failed to untether interface: " + e);
@@ -1178,8 +1189,8 @@
handleNewPrefixRequest((IpPrefix) message.obj);
break;
case CMD_NOTIFY_PREFIX_CONFLICT:
- mLog.i("restart tethering: " + mIfaceName);
- mCallback.requestEnableTethering(mTetheringRequest, false /* enabled */);
+ mLog.i("restart tethering: " + mInterfaceType);
+ mCallback.requestEnableTethering(mInterfaceType, false /* enabled */);
transitionTo(mWaitingForRestartState);
break;
case CMD_SERVICE_FAILED_TO_START:
@@ -1213,14 +1224,14 @@
return;
}
- // Remove deprecated routes from local network.
- removeRoutesFromLocalNetwork(
- Collections.singletonList(getDirectConnectedRoute(deprecatedLinkAddress)));
+ // Remove deprecated routes from downstream network.
+ removeRoutesFromNetworkAndLinkProperties(LOCAL_NET_ID,
+ List.of(getDirectConnectedRoute(deprecatedLinkAddress)));
mLinkProperties.removeLinkAddress(deprecatedLinkAddress);
- // Add new routes to local network.
- addRoutesToLocalNetwork(
- Collections.singletonList(getDirectConnectedRoute(mIpv4Address)));
+ // Add new routes to downstream network.
+ addRoutesToNetworkAndLinkProperties(LOCAL_NET_ID,
+ List.of(getDirectConnectedRoute(mIpv4Address)));
mLinkProperties.addLinkAddress(mIpv4Address);
// Update local DNS caching server with new IPv4 address, otherwise, dnsmasq doesn't
@@ -1463,12 +1474,12 @@
case CMD_TETHER_UNREQUESTED:
transitionTo(mInitialState);
mLog.i("Untethered (unrequested) and restarting " + mIfaceName);
- mCallback.requestEnableTethering(mTetheringRequest, true /* enabled */);
+ mCallback.requestEnableTethering(mInterfaceType, true /* enabled */);
break;
case CMD_INTERFACE_DOWN:
transitionTo(mUnavailableState);
mLog.i("Untethered (interface down) and restarting " + mIfaceName);
- mCallback.requestEnableTethering(mTetheringRequest, true /* enabled */);
+ mCallback.requestEnableTethering(mInterfaceType, true /* enabled */);
break;
default:
return false;
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index cd57c8d..a942166 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -154,7 +154,9 @@
// Only launch entitlement UI for the current user if it is allowed to
// change tethering. This usually means the system user or the admin users in HSUM.
- if (SdkLevel.isAtLeastT()) {
+ // TODO (b/382624069): Figure out whether it is safe to call createContextAsUser
+ // from secondary user. And re-enable the check or remove the code accordingly.
+ if (false) {
// Create a user context for the current foreground user as UserManager#isAdmin()
// operates on the context user.
final int currentUserId = getCurrentUser();
@@ -167,6 +169,11 @@
} else {
mLog.e("Current user (" + currentUserId
+ ") is not allowed to perform entitlement check.");
+ // If the user is not allowed to perform an entitlement check
+ // (e.g., a non-admin user), notify the receiver immediately.
+ // This is necessary because the entitlement check app cannot
+ // be launched to conduct the check and deliver the results.
+ receiver.send(TETHER_ERROR_PROVISIONING_FAILED, null);
return null;
}
} else {
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index f33ef37..4f07f58 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -67,6 +67,9 @@
import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_FORCE_USB_FUNCTIONS;
import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE;
import static com.android.networkstack.tethering.UpstreamNetworkMonitor.isCellular;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI;
+import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P;
import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_MAIN_SM;
import android.app.usage.NetworkStatsManager;
@@ -145,6 +148,7 @@
import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
import com.android.networkstack.tethering.metrics.TetheringMetrics;
+import com.android.networkstack.tethering.metrics.TetheringStatsLog;
import com.android.networkstack.tethering.util.InterfaceSet;
import com.android.networkstack.tethering.util.PrefixUtils;
import com.android.networkstack.tethering.util.VersionedBroadcastListener;
@@ -451,6 +455,10 @@
return mSettingsObserver;
}
+ boolean isTetheringWithSoftApConfigEnabled() {
+ return mDeps.isTetheringWithSoftApConfigEnabled();
+ }
+
/**
* Start to register callbacks.
* Call this function when tethering is ready to handle callback events.
@@ -664,8 +672,9 @@
final TetheringRequest unfinishedRequest = mActiveTetheringRequests.get(type);
// If tethering is already enabled with a different request,
// disable before re-enabling.
- if (unfinishedRequest != null && !unfinishedRequest.equals(request)) {
- enableTetheringInternal(type, false /* disabled */, null);
+ if (unfinishedRequest != null && !unfinishedRequest.equalsIgnoreUidPackage(request)) {
+ enableTetheringInternal(type, false /* disabled */,
+ unfinishedRequest.getInterfaceName(), null);
mEntitlementMgr.stopProvisioningIfNeeded(type);
}
mActiveTetheringRequests.put(type, request);
@@ -676,7 +685,7 @@
mEntitlementMgr.startProvisioningIfNeeded(type,
request.getShouldShowEntitlementUi());
}
- enableTetheringInternal(type, true /* enabled */, listener);
+ enableTetheringInternal(type, true /* enabled */, request.getInterfaceName(), listener);
mTetheringMetrics.createBuilder(type, callerPkg);
});
}
@@ -689,7 +698,7 @@
void stopTetheringInternal(int type) {
mActiveTetheringRequests.remove(type);
- enableTetheringInternal(type, false /* disabled */, null);
+ enableTetheringInternal(type, false /* disabled */, null, null);
mEntitlementMgr.stopProvisioningIfNeeded(type);
}
@@ -698,7 +707,7 @@
* schedule provisioning rechecks for the specified interface.
*/
private void enableTetheringInternal(int type, boolean enable,
- final IIntResultListener listener) {
+ String iface, final IIntResultListener listener) {
int result = TETHER_ERROR_NO_ERROR;
switch (type) {
case TETHERING_WIFI:
@@ -717,7 +726,7 @@
result = setEthernetTethering(enable);
break;
case TETHERING_VIRTUAL:
- result = setVirtualMachineTethering(enable);
+ result = setVirtualMachineTethering(enable, iface);
break;
default:
Log.w(TAG, "Invalid tether type.");
@@ -972,10 +981,13 @@
}
}
- private int setVirtualMachineTethering(final boolean enable) {
- // TODO(340377643): Use bridge ifname when it's introduced, not fixed TAP ifname.
+ private int setVirtualMachineTethering(final boolean enable, String iface) {
if (enable) {
- mConfiguredVirtualIface = "avf_tap_fixed";
+ if (TextUtils.isEmpty(iface)) {
+ mConfiguredVirtualIface = "avf_tap_fixed";
+ } else {
+ mConfiguredVirtualIface = iface;
+ }
enableIpServing(
TETHERING_VIRTUAL,
mConfiguredVirtualIface,
@@ -989,6 +1001,23 @@
void tether(String iface, int requestedState, final IIntResultListener listener) {
mHandler.post(() -> {
+ switch (ifaceNameToType(iface)) {
+ case TETHERING_WIFI:
+ TetheringStatsLog.write(
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI
+ );
+ break;
+ case TETHERING_WIFI_P2P:
+ TetheringStatsLog.write(
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_P2P
+ );
+ break;
+ default:
+ // Do nothing
+ break;
+ }
try {
listener.onResult(tether(iface, requestedState));
} catch (RemoteException e) { }
@@ -2085,7 +2114,7 @@
}
mRoutingCoordinator.maybeRemoveDeprecatedUpstreams();
- mUpstreamNetworkMonitor.startObserveAllNetworks();
+ mUpstreamNetworkMonitor.startObserveUpstreamNetworks();
// TODO: De-duplicate with updateUpstreamWanted() below.
if (upstreamWanted()) {
@@ -2203,9 +2232,9 @@
break;
}
case EVENT_REQUEST_CHANGE_DOWNSTREAM: {
- final boolean enabled = message.arg1 == 1;
- final TetheringRequest request = (TetheringRequest) message.obj;
- enableTetheringInternal(request.getTetheringType(), enabled, null);
+ final int tetheringType = message.arg1;
+ final Boolean enabled = (Boolean) message.obj;
+ enableTetheringInternal(tetheringType, enabled, null, null);
break;
}
default:
@@ -2783,9 +2812,9 @@
}
@Override
- public void requestEnableTethering(TetheringRequest request, boolean enabled) {
+ public void requestEnableTethering(int tetheringType, boolean enabled) {
mTetherMainSM.sendMessage(TetherMainSM.EVENT_REQUEST_CHANGE_DOWNSTREAM,
- enabled ? 1 : 0, 0, request);
+ tetheringType, 0, enabled ? Boolean.TRUE : Boolean.FALSE);
}
}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index d89bf4d..8e17085 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -37,6 +37,7 @@
import androidx.annotation.RequiresApi;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
import com.android.net.module.util.RoutingCoordinatorManager;
import com.android.net.module.util.RoutingCoordinatorService;
import com.android.net.module.util.SharedLog;
@@ -208,4 +209,12 @@
public int getBinderCallingUid() {
return Binder.getCallingUid();
}
+
+ /**
+ * Wrapper for tethering_with_soft_ap_config feature flag.
+ */
+ public boolean isTetheringWithSoftApConfigEnabled() {
+ return Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM
+ && Flags.tetheringWithSoftApConfig();
+ }
}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index 3cb5f99..0c44a38 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -133,9 +133,11 @@
@Override
public void startTethering(TetheringRequestParcel request, String callerPkg,
String callingAttributionTag, IIntResultListener listener) {
+ boolean onlyAllowPrivileged = request.exemptFromEntitlementCheck
+ || request.interfaceName != null;
if (checkAndNotifyCommonError(callerPkg,
callingAttributionTag,
- request.exemptFromEntitlementCheck /* onlyAllowPrivileged */,
+ onlyAllowPrivileged,
listener)) {
return;
}
@@ -284,6 +286,10 @@
return false;
}
+ private boolean hasNetworkSettingsPermission() {
+ return checkCallingOrSelfPermission(NETWORK_SETTINGS);
+ }
+
private boolean hasNetworkStackPermission() {
return checkCallingOrSelfPermission(NETWORK_STACK)
|| checkCallingOrSelfPermission(PERMISSION_MAINLINE_NETWORK_STACK);
@@ -299,10 +305,15 @@
private boolean hasTetherChangePermission(final String callerPkg,
final String callingAttributionTag, final boolean onlyAllowPrivileged) {
- if (onlyAllowPrivileged && !hasNetworkStackPermission()) return false;
+ if (onlyAllowPrivileged && !hasNetworkStackPermission()
+ && !hasNetworkSettingsPermission()) return false;
if (hasTetherPrivilegedPermission()) return true;
+ // After TetheringManager moves to public API, prevent third-party apps from being able
+ // to change tethering with only WRITE_SETTINGS permission.
+ if (mTethering.isTetheringWithSoftApConfigEnabled()) return false;
+
if (mTethering.isTetherProvisioningRequired()) return false;
int uid = getBinderCallingUid();
diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
index 7a05d74..9705d84 100644
--- a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
@@ -24,6 +24,7 @@
import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
@@ -44,6 +45,7 @@
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
import com.android.networkstack.apishim.common.ConnectivityManagerShim;
@@ -62,9 +64,10 @@
* The owner of UNM gets it to register network callbacks by calling the
* following methods :
* Calling #startTrackDefaultNetwork() to track the system default network.
- * Calling #startObserveAllNetworks() to observe all networks. Listening all
- * networks is necessary while the expression of preferred upstreams remains
- * a list of legacy connectivity types. In future, this can be revisited.
+ * Calling #startObserveUpstreamNetworks() to observe upstream networks.
+ * Listening all upstream networks is necessary while the expression of
+ * preferred upstreams remains a list of legacy connectivity types.
+ * In future, this can be revisited.
* Calling #setTryCell() to request bringing up mobile DUN or HIPRI.
*
* The methods and data members of this class are only to be accessed and
@@ -94,7 +97,7 @@
@VisibleForTesting
public static final int TYPE_NONE = -1;
- private static final int CALLBACK_LISTEN_ALL = 1;
+ private static final int CALLBACK_LISTEN_UPSTREAM = 1;
private static final int CALLBACK_DEFAULT_INTERNET = 2;
private static final int CALLBACK_MOBILE_REQUEST = 3;
@@ -116,7 +119,7 @@
private HashSet<IpPrefix> mLocalPrefixes;
private ConnectivityManager mCM;
private EntitlementManager mEntitlementMgr;
- private NetworkCallback mListenAllCallback;
+ private NetworkCallback mListenUpstreamCallback;
private NetworkCallback mDefaultNetworkCallback;
private NetworkCallback mMobileNetworkCallback;
@@ -157,20 +160,29 @@
}
ConnectivityManagerShim mCmShim = ConnectivityManagerShimImpl.newInstance(mContext);
mDefaultNetworkCallback = new UpstreamNetworkCallback(CALLBACK_DEFAULT_INTERNET);
+ // TODO (b/382413665): By definition, a local network cannot be the system default,
+ // because it does not provide internet capability. Figure out whether this
+ // is enforced in ConnectivityService. Or what will happen for tethering if it happens.
mCmShim.registerSystemDefaultNetworkCallback(mDefaultNetworkCallback, mHandler);
if (mEntitlementMgr == null) {
mEntitlementMgr = entitle;
}
}
- /** Listen all networks. */
- public void startObserveAllNetworks() {
+ /** Listen upstream networks. */
+ public void startObserveUpstreamNetworks() {
stop();
- final NetworkRequest listenAllRequest = new NetworkRequest.Builder()
- .clearCapabilities().build();
- mListenAllCallback = new UpstreamNetworkCallback(CALLBACK_LISTEN_ALL);
- cm().registerNetworkCallback(listenAllRequest, mListenAllCallback, mHandler);
+ final NetworkRequest listenUpstreamRequest;
+ // Before V, only TV supports local agent on U, which doesn't support tethering.
+ if (SdkLevel.isAtLeastV()) {
+ listenUpstreamRequest = new NetworkRequest.Builder().clearCapabilities()
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK).build();
+ } else {
+ listenUpstreamRequest = new NetworkRequest.Builder().clearCapabilities().build();
+ }
+ mListenUpstreamCallback = new UpstreamNetworkCallback(CALLBACK_LISTEN_UPSTREAM);
+ cm().registerNetworkCallback(listenUpstreamRequest, mListenUpstreamCallback, mHandler);
}
/**
@@ -183,8 +195,8 @@
public void stop() {
setTryCell(false);
- releaseCallback(mListenAllCallback);
- mListenAllCallback = null;
+ releaseCallback(mListenUpstreamCallback);
+ mListenUpstreamCallback = null;
mNetworkMap.clear();
}
@@ -535,10 +547,10 @@
return;
}
- // Any non-LISTEN_ALL callback will necessarily concern a network that will
- // also match the LISTEN_ALL callback by construction of the LISTEN_ALL callback.
- // So it's not useful to do this work for non-LISTEN_ALL callbacks.
- if (mCallbackType == CALLBACK_LISTEN_ALL) {
+ // Any non-LISTEN_UPSTREAM callback will necessarily concern a network that will
+ // also match the LISTEN_UPSTREAM callback by construction of the LISTEN_UPSTREAM
+ // callback. So it's not useful to do this work for non-LISTEN_UPSTREAM callbacks.
+ if (mCallbackType == CALLBACK_LISTEN_UPSTREAM) {
recomputeLocalPrefixes();
}
}
@@ -555,10 +567,11 @@
}
handleLost(network);
- // Any non-LISTEN_ALL callback will necessarily concern a network that will
- // also match the LISTEN_ALL callback by construction of the LISTEN_ALL callback.
- // So it's not useful to do this work for non-LISTEN_ALL callbacks.
- if (mCallbackType == CALLBACK_LISTEN_ALL) {
+ // Any non-LISTEN_UPSTREAM callback will necessarily concern a network that will
+ // also match the LISTEN_UPSTREAM callback by construction of the
+ // LISTEN_UPSTREAM callback. So it's not useful to do this work for
+ // non-LISTEN_UPSTREAM callbacks.
+ if (mCallbackType == CALLBACK_LISTEN_UPSTREAM) {
recomputeLocalPrefixes();
}
}
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 01f3af9..1323f28 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -332,15 +332,16 @@
// seconds. See b/289881008.
private static final int EXPANDED_TIMEOUT_MS = 30000;
- MyTetheringEventCallback(String iface) {
- mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
+ MyTetheringEventCallback(int tetheringType, String iface) {
+ mIface = new TetheringInterface(tetheringType, iface);
mExpectedUpstream = null;
mAcceptAnyUpstream = true;
}
- MyTetheringEventCallback(String iface, @NonNull Network expectedUpstream) {
+ MyTetheringEventCallback(
+ int tetheringType, String iface, @NonNull Network expectedUpstream) {
Objects.requireNonNull(expectedUpstream);
- mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
+ mIface = new TetheringInterface(tetheringType, iface);
mExpectedUpstream = expectedUpstream;
mAcceptAnyUpstream = false;
}
@@ -392,12 +393,12 @@
}
public void awaitInterfaceTethered() throws Exception {
- assertTrue("Ethernet not tethered after " + EXPANDED_TIMEOUT_MS + "ms",
+ assertTrue("Interface is not tethered after " + EXPANDED_TIMEOUT_MS + "ms",
mTetheringStartedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
public void awaitInterfaceLocalOnly() throws Exception {
- assertTrue("Ethernet not local-only after " + EXPANDED_TIMEOUT_MS + "ms",
+ assertTrue("Interface is not local-only after " + EXPANDED_TIMEOUT_MS + "ms",
mLocalOnlyStartedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
@@ -501,15 +502,17 @@
sCallbackErrors.add(error);
}
- protected static MyTetheringEventCallback enableEthernetTethering(String iface,
+ protected static MyTetheringEventCallback enableTethering(String iface,
TetheringRequest request, Network expectedUpstream) throws Exception {
- // Enable ethernet tethering with null expectedUpstream means the test accept any upstream
- // after etherent tethering started.
+ // Enable tethering with null expectedUpstream means the test accept any upstream after
+ // tethering started.
final MyTetheringEventCallback callback;
if (expectedUpstream != null) {
- callback = new MyTetheringEventCallback(iface, expectedUpstream);
+ callback =
+ new MyTetheringEventCallback(
+ request.getTetheringType(), iface, expectedUpstream);
} else {
- callback = new MyTetheringEventCallback(iface);
+ callback = new MyTetheringEventCallback(request.getTetheringType(), iface);
}
runAsShell(NETWORK_SETTINGS, () -> {
sTm.registerTetheringEventCallback(c -> c.run() /* executor */, callback);
@@ -521,7 +524,7 @@
StartTetheringCallback startTetheringCallback = new StartTetheringCallback() {
@Override
public void onTetheringStarted() {
- Log.d(TAG, "Ethernet tethering started");
+ Log.d(TAG, "Tethering started");
tetheringStartedLatch.countDown();
}
@@ -530,8 +533,8 @@
addCallbackError("Unexpectedly got onTetheringFailed");
}
};
- Log.d(TAG, "Starting Ethernet tethering");
- runAsShell(TETHER_PRIVILEGED, () -> {
+ Log.d(TAG, "Starting tethering");
+ runAsShell(TETHER_PRIVILEGED, NETWORK_SETTINGS, () -> {
sTm.startTethering(request, c -> c.run() /* executor */, startTetheringCallback);
// Binder call is an async call. Need to hold the shell permission until tethering
// started. This helps to avoid the test become flaky.
@@ -557,7 +560,7 @@
protected static MyTetheringEventCallback enableEthernetTethering(String iface,
Network expectedUpstream) throws Exception {
- return enableEthernetTethering(iface,
+ return enableTethering(iface,
new TetheringRequest.Builder(TETHERING_ETHERNET)
.setShouldShowEntitlementUi(false).build(), expectedUpstream);
}
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 1bbea94..5c8d347 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -19,7 +19,9 @@
import static android.Manifest.permission.DUMP;
import static android.net.InetAddresses.parseNumericAddress;
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_VIRTUAL;
import static android.net.TetheringTester.TestDnsPacket;
import static android.net.TetheringTester.buildIcmpEchoPacketV4;
import static android.net.TetheringTester.buildUdpPacket;
@@ -236,7 +238,8 @@
downstreamReader = makePacketReader(fd, mtu);
tetheringEventCallback = enableEthernetTethering(downstreamIface.getInterfaceName(),
null /* any upstream */);
- checkTetheredClientCallbacks(downstreamReader, tetheringEventCallback);
+ checkTetheredClientCallbacks(
+ downstreamReader, TETHERING_ETHERNET, tetheringEventCallback);
} finally {
maybeStopTapPacketReader(downstreamReader);
maybeCloseTestInterface(downstreamIface);
@@ -267,7 +270,8 @@
downstreamReader = makePacketReader(fd, getMTU(downstreamIface));
tetheringEventCallback = enableEthernetTethering(downstreamIface.getInterfaceName(),
null /* any upstream */);
- checkTetheredClientCallbacks(downstreamReader, tetheringEventCallback);
+ checkTetheredClientCallbacks(
+ downstreamReader, TETHERING_ETHERNET, tetheringEventCallback);
} finally {
maybeStopTapPacketReader(downstreamReader);
maybeCloseTestInterface(downstreamIface);
@@ -302,7 +306,7 @@
final String localAddr = "192.0.2.3/28";
final String clientAddr = "192.0.2.2/28";
- tetheringEventCallback = enableEthernetTethering(iface,
+ tetheringEventCallback = enableTethering(iface,
requestWithStaticIpv4(localAddr, clientAddr), null /* any upstream */);
tetheringEventCallback.awaitInterfaceTethered();
@@ -368,8 +372,7 @@
final TetheringRequest request = new TetheringRequest.Builder(TETHERING_ETHERNET)
.setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build();
- tetheringEventCallback = enableEthernetTethering(iface, request,
- null /* any upstream */);
+ tetheringEventCallback = enableTethering(iface, request, null /* any upstream */);
tetheringEventCallback.awaitInterfaceLocalOnly();
// makePacketReader only works after tethering is started, because until then the
@@ -424,6 +427,7 @@
}
private void checkTetheredClientCallbacks(final PollPacketReader packetReader,
+ final int tetheringType,
final MyTetheringEventCallback tetheringEventCallback) throws Exception {
// Create a fake client.
byte[] clientMacAddr = new byte[6];
@@ -438,7 +442,7 @@
// Check the MAC address.
assertEquals(MacAddress.fromBytes(clientMacAddr), client.getMacAddress());
- assertEquals(TETHERING_ETHERNET, client.getTetheringType());
+ assertEquals(tetheringType, client.getTetheringType());
// Check the hostname.
assertEquals(1, client.getAddresses().size());
@@ -475,8 +479,7 @@
private void assertInvalidStaticIpv4Request(String iface, String local, String client)
throws Exception {
try {
- enableEthernetTethering(iface, requestWithStaticIpv4(local, client),
- null /* any upstream */);
+ enableTethering(iface, requestWithStaticIpv4(local, client), null /* any upstream */);
fail("Unexpectedly accepted invalid IPv4 configuration: " + local + ", " + client);
} catch (IllegalArgumentException | NullPointerException expected) { }
}
@@ -1180,4 +1183,32 @@
TX_UDP_PACKET_COUNT * (TX_UDP_PACKET_SIZE + IPV6_HEADER_LEN - IPV4_HEADER_MIN_LEN),
newEgress4.bytes - oldEgress4.bytes);
}
+
+ @Test
+ public void testTetheringVirtual() throws Exception {
+ assumeFalse(isInterfaceForTetheringAvailable());
+ setIncludeTestInterfaces(true);
+
+ TestNetworkInterface downstreamIface = null;
+ MyTetheringEventCallback tetheringEventCallback = null;
+ PollPacketReader downstreamReader = null;
+ try {
+ downstreamIface = createTestInterface();
+ String iface = downstreamIface.getInterfaceName();
+ final TetheringRequest request = new TetheringRequest.Builder(TETHERING_VIRTUAL)
+ .setConnectivityScope(CONNECTIVITY_SCOPE_GLOBAL)
+ .setInterfaceName(iface)
+ .build();
+ tetheringEventCallback = enableTethering(iface, request, null /* any upstream */);
+
+ FileDescriptor fd = downstreamIface.getFileDescriptor().getFileDescriptor();
+ downstreamReader = makePacketReader(fd, getMTU(downstreamIface));
+ checkTetheredClientCallbacks(
+ downstreamReader, TETHERING_VIRTUAL, tetheringEventCallback);
+ } finally {
+ maybeStopTapPacketReader(downstreamReader);
+ maybeCloseTestInterface(downstreamIface);
+ maybeUnregisterTetheringEventCallback(tetheringEventCallback);
+ }
+ }
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
index 8626b18..16ebbbb 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -38,7 +38,6 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
-import static com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
@@ -84,6 +83,7 @@
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.ArrayTrackRecord;
import com.android.net.module.util.SharedLog;
import com.android.testutils.DevSdkIgnoreRule;
@@ -187,8 +187,9 @@
if (intent != null) {
assertUiTetherProvisioningIntent(type, config, receiver, intent);
uiProvisionCount++;
+ // If the intent is null, the result is sent by the underlying method.
+ receiver.send(fakeEntitlementResult, null);
}
- receiver.send(fakeEntitlementResult, null);
return intent;
}
@@ -348,99 +349,43 @@
public void testRequestLastEntitlementCacheValue() throws Exception {
// 1. Entitlement check is not required.
mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
- ResultReceiver receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
- mLooper.dispatchAll();
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_NO_ERROR, true);
assertEquals(0, mDeps.uiProvisionCount);
mDeps.reset();
setupForRequiredProvisioning();
// 2. No cache value and don't need to run entitlement check.
- receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
- mLooper.dispatchAll();
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_ENTITLEMENT_UNKNOWN, false);
assertEquals(0, mDeps.uiProvisionCount);
mDeps.reset();
// 3. No cache value and ui entitlement check is needed.
mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
- receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_PROVISIONING_FAILED, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
- mLooper.dispatchAll();
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_PROVISIONING_FAILED, true);
assertEquals(1, mDeps.uiProvisionCount);
mDeps.reset();
// 4. Cache value is TETHER_ERROR_PROVISIONING_FAILED and don't need to run entitlement
// check.
mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
- receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_PROVISIONING_FAILED, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
- mLooper.dispatchAll();
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_PROVISIONING_FAILED, false);
assertEquals(0, mDeps.uiProvisionCount);
mDeps.reset();
// 5. Cache value is TETHER_ERROR_PROVISIONING_FAILED and ui entitlement check is needed.
mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
- receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
- mLooper.dispatchAll();
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_NO_ERROR, true);
assertEquals(1, mDeps.uiProvisionCount);
mDeps.reset();
// 6. Cache value is TETHER_ERROR_NO_ERROR.
mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
- receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
- mLooper.dispatchAll();
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_NO_ERROR, true);
assertEquals(0, mDeps.uiProvisionCount);
mDeps.reset();
// 7. Test get value for other downstream type.
- receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_USB, receiver, false);
- mLooper.dispatchAll();
+ assertLatestEntitlementResult(TETHERING_USB, TETHER_ERROR_ENTITLEMENT_UNKNOWN, false);
assertEquals(0, mDeps.uiProvisionCount);
mDeps.reset();
// 8. Test get value for invalid downstream type.
mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
- receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI_P2P, receiver, true);
- mLooper.dispatchAll();
+ assertLatestEntitlementResult(TETHERING_WIFI_P2P, TETHER_ERROR_ENTITLEMENT_UNKNOWN, true);
assertEquals(0, mDeps.uiProvisionCount);
mDeps.reset();
}
@@ -646,20 +591,40 @@
.onTetherProvisioningFailed(TETHERING_WIFI, FAILED_TETHERING_REASON);
}
- @IgnoreUpTo(SC_V2)
@Test
- public void testUiProvisioningMultiUser_aboveT() {
- doTestUiProvisioningMultiUser(true, 1);
- doTestUiProvisioningMultiUser(false, 0);
- }
-
- @IgnoreAfter(SC_V2)
- @Test
- public void testUiProvisioningMultiUser_belowT() {
+ public void testUiProvisioningMultiUser() {
doTestUiProvisioningMultiUser(true, 1);
doTestUiProvisioningMultiUser(false, 1);
}
+ private static class TestableResultReceiver extends ResultReceiver {
+ private static final long DEFAULT_TIMEOUT_MS = 200L;
+ private final ArrayTrackRecord<Integer>.ReadHead mHistory =
+ new ArrayTrackRecord<Integer>().newReadHead();
+
+ TestableResultReceiver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ mHistory.add(resultCode);
+ }
+
+ void expectResult(int resultCode) {
+ final int event = mHistory.poll(DEFAULT_TIMEOUT_MS, it -> true);
+ assertEquals(resultCode, event);
+ }
+ }
+
+ void assertLatestEntitlementResult(int downstreamType, int expectedCode,
+ boolean showEntitlementUi) {
+ final TestableResultReceiver receiver = new TestableResultReceiver(null);
+ mEnMgr.requestLatestTetheringEntitlementResult(downstreamType, receiver, showEntitlementUi);
+ mLooper.dispatchAll();
+ receiver.expectResult(expectedCode);
+ }
+
private void doTestUiProvisioningMultiUser(boolean isAdminUser, int expectedUiProvisionCount) {
setupForRequiredProvisioning();
doReturn(isAdminUser).when(mUserManager).isAdminUser();
@@ -671,10 +636,19 @@
mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
mLooper.dispatchAll();
assertEquals(expectedUiProvisionCount, mDeps.uiProvisionCount);
+ if (expectedUiProvisionCount == 0) { // Failed to launch entitlement UI.
+ assertLatestEntitlementResult(TETHERING_USB, TETHER_ERROR_PROVISIONING_FAILED, false);
+ verify(mTetherProvisioningFailedListener).onTetherProvisioningFailed(TETHERING_USB,
+ FAILED_TETHERING_REASON);
+ } else {
+ assertLatestEntitlementResult(TETHERING_USB, TETHER_ERROR_NO_ERROR, false);
+ verify(mTetherProvisioningFailedListener, never()).onTetherProvisioningFailed(anyInt(),
+ anyString());
+ }
}
@Test
- public void testsetExemptedDownstreamType() throws Exception {
+ public void testSetExemptedDownstreamType() {
setupForRequiredProvisioning();
// Cellular upstream is not permitted when no entitlement result.
assertFalse(mEnMgr.isCellularUpstreamPermitted());
@@ -737,14 +711,7 @@
setupCarrierConfig(false);
setupForRequiredProvisioning();
mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
- ResultReceiver receiver = new ResultReceiver(null) {
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- assertEquals(TETHER_ERROR_PROVISIONING_FAILED, resultCode);
- }
- };
- mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
- mLooper.dispatchAll();
+ assertLatestEntitlementResult(TETHERING_WIFI, TETHER_ERROR_PROVISIONING_FAILED, false);
assertEquals(0, mDeps.uiProvisionCount);
mDeps.reset();
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index 1608e1a..f9e3a6a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -186,7 +186,8 @@
// - Test bluetooth prefix is reserved.
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
getSubAddress(mBluetoothAddress.getAddress().getAddress()));
- final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer);
+ final LinkAddress hotspotAddress = requestStickyDownstreamAddress(mHotspotIpServer,
+ CONNECTIVITY_SCOPE_GLOBAL);
final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress);
assertNotEquals(asIpPrefix(mBluetoothAddress), hotspotPrefix);
releaseDownstream(mHotspotIpServer);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
index b2cbf75..51ba140 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
@@ -18,6 +18,7 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static com.android.networkstack.apishim.common.ShimUtils.isAtLeastS;
@@ -41,6 +42,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.modules.utils.build.SdkLevel;
+
import java.util.Map;
import java.util.Objects;
@@ -119,12 +122,15 @@
&& mLegacyTypeMap.isEmpty();
}
- boolean isListeningForAll() {
- final NetworkCapabilities empty = new NetworkCapabilities();
- empty.clearAll();
+ boolean isListeningForUpstream() {
+ final NetworkCapabilities upstreamNc = new NetworkCapabilities();
+ upstreamNc.clearAll();
+ if (SdkLevel.isAtLeastV()) {
+ upstreamNc.addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK);
+ }
for (NetworkRequestInfo nri : mListening.values()) {
- if (nri.request.networkCapabilities.equalRequestableCapabilities(empty)) {
+ if (nri.request.networkCapabilities.equalRequestableCapabilities(upstreamNc)) {
return true;
}
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
index 0dbf772..cc80251 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -17,9 +17,11 @@
package com.android.networkstack.tethering;
import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.Manifest.permission.TETHER_PRIVILEGED;
import static android.Manifest.permission.WRITE_SETTINGS;
import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.net.TetheringManager.TETHERING_VIRTUAL;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION;
import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
@@ -152,27 +154,43 @@
}
private void runAsNoPermission(final TestTetheringCall test) throws Exception {
- runTetheringCall(test, true /* isTetheringAllowed */, new String[0]);
+ runTetheringCall(test, true /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+ new String[0]);
}
private void runAsTetherPrivileged(final TestTetheringCall test) throws Exception {
- runTetheringCall(test, true /* isTetheringAllowed */, TETHER_PRIVILEGED);
+ runTetheringCall(test, true /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+ TETHER_PRIVILEGED);
}
private void runAsAccessNetworkState(final TestTetheringCall test) throws Exception {
- runTetheringCall(test, true /* isTetheringAllowed */, ACCESS_NETWORK_STATE);
+ runTetheringCall(test, true /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+ ACCESS_NETWORK_STATE);
}
private void runAsWriteSettings(final TestTetheringCall test) throws Exception {
- runTetheringCall(test, true /* isTetheringAllowed */, WRITE_SETTINGS);
+ runTetheringCall(test, true /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+ WRITE_SETTINGS);
+ }
+
+ private void runAsWriteSettingsWhenWriteSettingsAllowed(
+ final TestTetheringCall test) throws Exception {
+ runTetheringCall(test, true /* isTetheringAllowed */, true /* isWriteSettingsAllowed */,
+ WRITE_SETTINGS);
}
private void runAsTetheringDisallowed(final TestTetheringCall test) throws Exception {
- runTetheringCall(test, false /* isTetheringAllowed */, TETHER_PRIVILEGED);
+ runTetheringCall(test, false /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+ TETHER_PRIVILEGED);
+ }
+
+ private void runAsNetworkSettings(final TestTetheringCall test) throws Exception {
+ runTetheringCall(test, true /* isTetheringAllowed */, false /* isWriteSettingsAllowed */,
+ NETWORK_SETTINGS, TETHER_PRIVILEGED);
}
private void runTetheringCall(final TestTetheringCall test, boolean isTetheringAllowed,
- String... permissions) throws Exception {
+ boolean isWriteSettingsAllowed, String... permissions) throws Exception {
// Allow the test to run even if ACCESS_NETWORK_STATE was granted at the APK level
if (!CollectionUtils.contains(permissions, ACCESS_NETWORK_STATE)) {
mMockConnector.setPermission(ACCESS_NETWORK_STATE, PERMISSION_DENIED);
@@ -182,6 +200,8 @@
try {
when(mTethering.isTetheringSupported()).thenReturn(true);
when(mTethering.isTetheringAllowed()).thenReturn(isTetheringAllowed);
+ when(mTethering.isTetheringWithSoftApConfigEnabled())
+ .thenReturn(!isWriteSettingsAllowed);
test.runTetheringCall(new TestTetheringResult());
} finally {
mUiAutomation.dropShellPermissionIdentity();
@@ -207,7 +227,7 @@
runAsNoPermission((result) -> {
mTetheringConnector.tether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
result);
- verify(mTethering).isTetherProvisioningRequired();
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
verifyNoMoreInteractionsForTethering();
});
@@ -218,7 +238,16 @@
});
runAsWriteSettings((result) -> {
+ mTetheringConnector.tether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
runTether(result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
verify(mTethering).isTetherProvisioningRequired();
verifyNoMoreInteractionsForTethering();
});
@@ -246,7 +275,7 @@
runAsNoPermission((result) -> {
mTetheringConnector.untether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
result);
- verify(mTethering).isTetherProvisioningRequired();
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
verifyNoMoreInteractionsForTethering();
});
@@ -257,7 +286,16 @@
});
runAsWriteSettings((result) -> {
+ mTetheringConnector.untether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
runUnTether(result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
verify(mTethering).isTetherProvisioningRequired();
verifyNoMoreInteractionsForTethering();
});
@@ -291,7 +329,7 @@
runAsNoPermission((result) -> {
mTetheringConnector.setUsbTethering(true /* enable */, TEST_CALLER_PKG,
TEST_ATTRIBUTION_TAG, result);
- verify(mTethering).isTetherProvisioningRequired();
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
verifyNoMoreInteractionsForTethering();
});
@@ -302,7 +340,16 @@
});
runAsWriteSettings((result) -> {
+ mTetheringConnector.setUsbTethering(true /* enable */, TEST_CALLER_PKG,
+ TEST_ATTRIBUTION_TAG, result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
runSetUsbTethering(result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
verify(mTethering).isTetherProvisioningRequired();
verifyNoMoreInteractionsForTethering();
});
@@ -335,7 +382,7 @@
runAsNoPermission((result) -> {
mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
result);
- verify(mTethering).isTetherProvisioningRequired();
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
verifyNoMoreInteractionsForTethering();
});
@@ -355,7 +402,16 @@
});
runAsWriteSettings((result) -> {
+ mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
runStartTethering(result, request);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
verify(mTethering).isTetherProvisioningRequired();
verifyNoMoreInteractionsForTethering();
});
@@ -370,6 +426,32 @@
});
}
+ @Test
+ public void testStartTetheringWithInterfaceSucceeds() throws Exception {
+ final TetheringRequestParcel request = new TetheringRequestParcel();
+ request.tetheringType = TETHERING_VIRTUAL;
+ request.interfaceName = "avf_tap_fixed";
+
+ runAsNetworkSettings((result) -> {
+ runStartTethering(result, request);
+ verifyNoMoreInteractionsForTethering();
+ });
+ }
+
+ @Test
+ public void testStartTetheringNoNetworkStackPermissionWithInterfaceFails() throws Exception {
+ final TetheringRequestParcel request = new TetheringRequestParcel();
+ request.tetheringType = TETHERING_VIRTUAL;
+ request.interfaceName = "avf_tap_fixed";
+
+ runAsTetherPrivileged((result) -> {
+ mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+ }
+
private void runStartTetheringAndVerifyNoPermission(final TestTetheringResult result)
throws Exception {
final TetheringRequestParcel request = new TetheringRequestParcel();
@@ -414,7 +496,7 @@
runAsNoPermission((result) -> {
mTetheringConnector.stopTethering(TETHERING_WIFI, TEST_CALLER_PKG,
TEST_ATTRIBUTION_TAG, result);
- verify(mTethering).isTetherProvisioningRequired();
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
verifyNoMoreInteractionsForTethering();
});
@@ -425,7 +507,16 @@
});
runAsWriteSettings((result) -> {
+ mTetheringConnector.stopTethering(TETHERING_WIFI, TEST_CALLER_PKG,
+ TEST_ATTRIBUTION_TAG, result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
runStopTethering(result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
verify(mTethering).isTetherProvisioningRequired();
verifyNoMoreInteractionsForTethering();
});
@@ -454,11 +545,13 @@
public void testRequestLatestTetheringEntitlementResult() throws Exception {
// Run as no permission.
final MyResultReceiver result = new MyResultReceiver(null);
- mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
- true /* showEntitlementUi */, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG);
- verify(mTethering).isTetherProvisioningRequired();
- result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
- verifyNoMoreInteractions(mTethering);
+ runAsNoPermission((none) -> {
+ mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
+ true /* showEntitlementUi */, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
runAsTetherPrivileged((none) -> {
runRequestLatestTetheringEntitlementResult();
@@ -469,22 +562,30 @@
mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
true /* showEntitlementUi */, TEST_WRONG_PACKAGE, TEST_ATTRIBUTION_TAG);
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
- verifyNoMoreInteractions(mTethering);
+ verifyNoMoreInteractionsForTethering();
});
runAsWriteSettings((none) -> {
+ mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
+ true /* showEntitlementUi */, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettingsWhenWriteSettingsAllowed((none) -> {
runRequestLatestTetheringEntitlementResult();
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
verify(mTethering).isTetherProvisioningRequired();
verifyNoMoreInteractionsForTethering();
});
runAsTetheringDisallowed((none) -> {
- final MyResultReceiver receiver = new MyResultReceiver(null);
- mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver,
+ mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
true /* showEntitlementUi */, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG);
verify(mTethering).isTetheringSupported();
verify(mTethering).isTetheringAllowed();
- receiver.assertResult(TETHER_ERROR_UNSUPPORTED);
+ result.assertResult(TETHER_ERROR_UNSUPPORTED);
verifyNoMoreInteractionsForTethering();
});
}
@@ -568,7 +669,7 @@
public void testStopAllTethering() throws Exception {
runAsNoPermission((result) -> {
mTetheringConnector.stopAllTethering(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
- verify(mTethering).isTetherProvisioningRequired();
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
verifyNoMoreInteractionsForTethering();
});
@@ -579,7 +680,15 @@
});
runAsWriteSettings((result) -> {
+ mTetheringConnector.stopAllTethering(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
runStopAllTethering(result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
verify(mTethering).isTetherProvisioningRequired();
verifyNoMoreInteractionsForTethering();
});
@@ -605,7 +714,7 @@
runAsNoPermission((result) -> {
mTetheringConnector.isTetheringSupported(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
result);
- verify(mTethering).isTetherProvisioningRequired();
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
verifyNoMoreInteractionsForTethering();
});
@@ -616,7 +725,16 @@
});
runAsWriteSettings((result) -> {
+ mTetheringConnector.isTetheringSupported(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
+ result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
+ result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ verifyNoMoreInteractionsForTethering();
+ });
+
+ runAsWriteSettingsWhenWriteSettingsAllowed((result) -> {
runIsTetheringSupported(result);
+ verify(mTethering).isTetheringWithSoftApConfigEnabled();
verify(mTethering).isTetherProvisioningRequired();
verifyNoMoreInteractionsForTethering();
});
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 17f5081..97758cf 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -47,6 +47,7 @@
import static android.net.TetheringManager.TETHERING_ETHERNET;
import static android.net.TetheringManager.TETHERING_NCM;
import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_VIRTUAL;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
@@ -255,6 +256,7 @@
private static final String TEST_P2P_IFNAME = "test_p2p-p2p0-0";
private static final String TEST_NCM_IFNAME = "test_ncm0";
private static final String TEST_ETH_IFNAME = "test_eth0";
+ private static final String TEST_VIRT_IFNAME = "test_virt0";
private static final String TEST_BT_IFNAME = "test_pan0";
private static final String TETHERING_NAME = "Tethering";
private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
@@ -265,8 +267,9 @@
private static final String TEST_P2P_REGEX = "test_p2p-p2p\\d-.*";
private static final String TEST_BT_REGEX = "test_pan\\d";
private static final int TEST_CALLER_UID = 1000;
+ private static final int TEST_CALLER_UID_2 = 2000;
private static final String TEST_CALLER_PKG = "com.test.tethering";
-
+ private static final String TEST_CALLER_PKG_2 = "com.test.tethering2";
private static final int CELLULAR_NETID = 100;
private static final int WIFI_NETID = 101;
private static final int DUN_NETID = 102;
@@ -417,11 +420,12 @@
|| ifName.equals(TEST_P2P_IFNAME)
|| ifName.equals(TEST_NCM_IFNAME)
|| ifName.equals(TEST_ETH_IFNAME)
- || ifName.equals(TEST_BT_IFNAME));
+ || ifName.equals(TEST_BT_IFNAME)
+ || ifName.equals(TEST_VIRT_IFNAME));
final String[] ifaces = new String[] {
TEST_RNDIS_IFNAME, TEST_WLAN_IFNAME, TEST_WLAN2_IFNAME, TEST_WIFI_IFNAME,
TEST_MOBILE_IFNAME, TEST_DUN_IFNAME, TEST_P2P_IFNAME, TEST_NCM_IFNAME,
- TEST_ETH_IFNAME};
+ TEST_ETH_IFNAME, TEST_VIRT_IFNAME};
return new InterfaceParams(ifName,
CollectionUtils.indexOf(ifaces, ifName) + IFINDEX_OFFSET,
MacAddress.ALL_ZEROS_ADDRESS);
@@ -674,7 +678,8 @@
when(mNetd.interfaceGetList())
.thenReturn(new String[] {
TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_WLAN2_IFNAME, TEST_RNDIS_IFNAME,
- TEST_P2P_IFNAME, TEST_NCM_IFNAME, TEST_ETH_IFNAME, TEST_BT_IFNAME});
+ TEST_P2P_IFNAME, TEST_NCM_IFNAME, TEST_ETH_IFNAME, TEST_BT_IFNAME,
+ TEST_VIRT_IFNAME});
when(mResources.getString(R.string.config_wifi_tether_enable)).thenReturn("");
mInterfaceConfiguration = new InterfaceConfigurationParcel();
mInterfaceConfiguration.flags = new String[0];
@@ -765,12 +770,12 @@
}
private TetheringRequest createTetheringRequest(final int type) {
- return createTetheringRequest(type, null, null, false, CONNECTIVITY_SCOPE_GLOBAL);
+ return createTetheringRequest(type, null, null, false, CONNECTIVITY_SCOPE_GLOBAL, null);
}
private TetheringRequest createTetheringRequest(final int type,
final LinkAddress localIPv4Address, final LinkAddress staticClientAddress,
- final boolean exempt, final int scope) {
+ final boolean exempt, final int scope, final String interfaceName) {
TetheringRequest.Builder builder = new TetheringRequest.Builder(type)
.setExemptFromEntitlementCheck(exempt)
.setConnectivityScope(scope)
@@ -778,7 +783,13 @@
if (localIPv4Address != null && staticClientAddress != null) {
builder.setStaticIpv4Addresses(localIPv4Address, staticClientAddress);
}
- return builder.build();
+ if (interfaceName != null) {
+ builder.setInterfaceName(interfaceName);
+ }
+ TetheringRequest request = builder.build();
+ request.setUid(TEST_CALLER_UID);
+ request.setPackageName(TEST_CALLER_PKG);
+ return request;
}
@NonNull
@@ -1025,7 +1036,7 @@
verify(mWifiManager).updateInterfaceIpState(TEST_WLAN_IFNAME, expectedState);
verifyNoMoreInteractions(mWifiManager);
- verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+ verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
if (isLocalOnly) {
// There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY.
verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE);
@@ -1253,7 +1264,7 @@
// Start USB tethering with no current upstream.
prepareUsbTethering();
sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
- inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+ inOrder.verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
// Pretend cellular connected and expect the upstream to be set.
@@ -1852,7 +1863,7 @@
// Start USB tethering with no current upstream.
prepareUsbTethering();
sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
- inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+ inOrder.verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
ArgumentCaptor<NetworkCallback> captor = ArgumentCaptor.forClass(NetworkCallback.class);
inOrder.verify(mCm).requestNetwork(any(), eq(0), eq(TYPE_MOBILE_DUN), any(),
@@ -2580,7 +2591,7 @@
verify(mNetd, times(1)).tetherStartWithConfiguration(any());
verifyNoMoreInteractions(mNetd);
verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
- verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks();
+ verify(mUpstreamNetworkMonitor, times(1)).startObserveUpstreamNetworks();
// There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY
verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE);
@@ -2779,10 +2790,21 @@
verify(mUsbManager, never()).setCurrentFunctions(UsbManager.FUNCTION_NONE);
reset(mUsbManager);
+ // Enable USB tethering again with the same request but different uid/package and expect no
+ // change to USB.
+ TetheringRequest differentUidPackage = createTetheringRequest(TETHERING_USB);
+ differentUidPackage.setUid(TEST_CALLER_UID_2);
+ differentUidPackage.setPackageName(TEST_CALLER_PKG_2);
+ mTethering.startTethering(differentUidPackage, TEST_CALLER_PKG_2, secondResult);
+ mLooper.dispatchAll();
+ secondResult.assertHasResult();
+ verify(mUsbManager, never()).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+ reset(mUsbManager);
+
// Enable USB tethering with a different request and expect that USB is stopped and
// started.
mTethering.startTethering(createTetheringRequest(TETHERING_USB,
- serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
+ serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL, null),
TEST_CALLER_PKG, thirdResult);
mLooper.dispatchAll();
thirdResult.assertHasResult();
@@ -2813,7 +2835,7 @@
final ArgumentCaptor<DhcpServingParamsParcel> dhcpParamsCaptor =
ArgumentCaptor.forClass(DhcpServingParamsParcel.class);
mTethering.startTethering(createTetheringRequest(TETHERING_USB,
- serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
+ serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL, null),
TEST_CALLER_PKG, null);
mLooper.dispatchAll();
verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM);
@@ -2942,7 +2964,7 @@
setupForRequiredProvisioning();
final TetheringRequest wifiNotExemptRequest =
createTetheringRequest(TETHERING_WIFI, null, null, false,
- CONNECTIVITY_SCOPE_GLOBAL);
+ CONNECTIVITY_SCOPE_GLOBAL, null);
mTethering.startTethering(wifiNotExemptRequest, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
@@ -2956,7 +2978,7 @@
setupForRequiredProvisioning();
final TetheringRequest wifiExemptRequest =
createTetheringRequest(TETHERING_WIFI, null, null, true,
- CONNECTIVITY_SCOPE_GLOBAL);
+ CONNECTIVITY_SCOPE_GLOBAL, null);
mTethering.startTethering(wifiExemptRequest, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
@@ -3750,7 +3772,7 @@
verifyInterfaceServingModeStarted(TEST_P2P_IFNAME);
verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_AVAILABLE_TETHER);
verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
- verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+ verify(mUpstreamNetworkMonitor).startObserveUpstreamNetworks();
// Verify never enable upstream if only P2P active.
verify(mUpstreamNetworkMonitor, never()).setTryCell(true);
assertEquals(TETHER_ERROR_NO_ERROR, mTethering.getLastErrorForTest(TEST_P2P_IFNAME));
@@ -3779,4 +3801,18 @@
// TODO: Test that a request for hotspot mode doesn't interfere with an
// already operating tethering mode interface.
+
+ @Test
+ public void testVirtualTetheringWithInterfaceName() throws Exception {
+ initTetheringOnTestThread();
+ final TetheringRequest virtualTetheringRequest =
+ createTetheringRequest(TETHERING_VIRTUAL, null, null, false,
+ CONNECTIVITY_SCOPE_GLOBAL, TEST_VIRT_IFNAME);
+ assertEquals(TEST_VIRT_IFNAME, virtualTetheringRequest.getInterfaceName());
+ mTethering.startTethering(virtualTetheringRequest, TEST_CALLER_PKG, null);
+ mLooper.dispatchAll();
+ assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_VIRT_IFNAME);
+ mTethering.stopTethering(TETHERING_VIRTUAL);
+ mLooper.dispatchAll();
+ }
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
index 90fd709..f192492 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
@@ -36,7 +36,6 @@
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -141,7 +140,7 @@
assertTrue(mCM.hasNoCallbacks());
mUNM.startTrackDefaultNetwork(mEntitleMgr);
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
assertEquals(1, mCM.mTrackingDefault.size());
mUNM.stop();
@@ -149,13 +148,13 @@
}
@Test
- public void testListensForAllNetworks() throws Exception {
+ public void testListensForUpstreamNetworks() throws Exception {
assertTrue(mCM.mListening.isEmpty());
mUNM.startTrackDefaultNetwork(mEntitleMgr);
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
assertFalse(mCM.mListening.isEmpty());
- assertTrue(mCM.isListeningForAll());
+ assertTrue(mCM.isListeningForUpstream());
mUNM.stop();
assertTrue(mCM.onlyHasDefaultCallbacks());
@@ -179,7 +178,7 @@
assertTrue(TestConnectivityManager.looksLikeDefaultRequest(requestCaptor.getValue()));
}
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
verify(mCM, times(1)).registerNetworkCallback(
any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
@@ -192,7 +191,7 @@
assertFalse(mUNM.mobileNetworkRequested());
assertEquals(0, mCM.mRequested.size());
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
assertFalse(mUNM.mobileNetworkRequested());
assertEquals(0, mCM.mRequested.size());
@@ -215,7 +214,7 @@
assertFalse(mUNM.mobileNetworkRequested());
assertEquals(0, mCM.mRequested.size());
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
verify(mCM, times(1)).registerNetworkCallback(
any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
assertFalse(mUNM.mobileNetworkRequested());
@@ -251,7 +250,7 @@
assertFalse(mUNM.mobileNetworkRequested());
assertEquals(0, mCM.mRequested.size());
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
assertFalse(mUNM.mobileNetworkRequested());
assertEquals(0, mCM.mRequested.size());
@@ -271,7 +270,7 @@
@Test
public void testUpdateMobileRequiresDun() throws Exception {
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
// Test going from no-DUN to DUN correctly re-registers callbacks.
mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */);
@@ -301,7 +300,7 @@
preferredTypes.add(TYPE_WIFI);
mUNM.startTrackDefaultNetwork(mEntitleMgr);
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
// There are no networks, so there is nothing to select.
assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
@@ -374,7 +373,7 @@
@Test
public void testGetCurrentPreferredUpstream() throws Exception {
mUNM.startTrackDefaultNetwork(mEntitleMgr);
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */);
mUNM.setTryCell(true);
@@ -446,7 +445,7 @@
@Test
public void testLocalPrefixes() throws Exception {
mUNM.startTrackDefaultNetwork(mEntitleMgr);
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
// [0] Test minimum set of local prefixes.
Set<IpPrefix> local = mUNM.getLocalPrefixes();
@@ -558,7 +557,7 @@
preferredTypes.add(TYPE_MOBILE_HIPRI);
preferredTypes.add(TYPE_WIFI);
mUNM.startTrackDefaultNetwork(mEntitleMgr);
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
// Setup wifi and make wifi as default network.
final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
wifiAgent.fakeConnect();
@@ -579,7 +578,7 @@
final String ipv6Addr1 = "2001:db8:4:fd00:827a:bfff:fe6f:374d/64";
final String ipv6Addr2 = "2003:aa8:3::123/64";
mUNM.startTrackDefaultNetwork(mEntitleMgr);
- mUNM.startObserveAllNetworks();
+ mUNM.startObserveUpstreamNetworks();
mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */);
mUNM.setTryCell(true);
diff --git a/bpf/headers/BpfMapTest.cpp b/bpf/headers/BpfMapTest.cpp
index 862114d..33b88fa 100644
--- a/bpf/headers/BpfMapTest.cpp
+++ b/bpf/headers/BpfMapTest.cpp
@@ -250,5 +250,10 @@
expectMapEmpty(testMap);
}
+TEST_F(BpfMapTest, testGTSbitmapTestOpen) {
+ BpfMap<int, uint64_t> bitmap;
+ ASSERT_RESULT_OK(bitmap.init("/sys/fs/bpf/tethering/map_test_bitmap"));
+}
+
} // namespace bpf
} // namespace android
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index c2a1d6e..ce144a7 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -1514,6 +1514,7 @@
REQUIRE(5, 15, 136)
REQUIRE(6, 1, 57)
REQUIRE(6, 6, 0)
+ REQUIRE(6, 12, 0)
#undef REQUIRE
@@ -1544,15 +1545,14 @@
*
* Additionally the 32-bit kernel jit support is poor,
* and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
+ * Note, however, that TV and Wear devices will continue to support 32-bit userspace
+ * on ARM64.
*/
if (isUserspace32bit() && isAtLeastKernelVersion(6, 2, 0)) {
// Stuff won't work reliably, but...
- if (isTV()) {
- // exempt TVs... they don't really need functional advanced networking
- ALOGW("[TV] 32-bit userspace unsupported on 6.2+ kernels.");
- } else if (isWear() && isArm()) {
- // exempt Arm Wear devices (arm32 ABI is far less problematic than x86-32)
- ALOGW("[Arm Wear] 32-bit userspace unsupported on 6.2+ kernels.");
+ if (isArm() && (isTV() || isWear())) {
+ // exempt Arm TV or Wear devices (arm32 ABI is far less problematic than x86-32)
+ ALOGW("[Arm TV/Wear] 32-bit userspace unsupported on 6.2+ kernels.");
} else if (first_api_level <= __ANDROID_API_T__ && isArm()) {
// also exempt Arm devices upgrading with major kernel rev from T-
// might possibly be better for them to run with a newer kernel...
@@ -1566,8 +1566,8 @@
}
}
- // Note: 6.6 is highest version supported by Android V (sdk=35), so this is for sdk=36+
- if (isUserspace32bit() && isAtLeastKernelVersion(6, 7, 0)) {
+ // On handheld, 6.6 is highest version supported by Android V (sdk=35), so this is for sdk=36+
+ if (!isArm() && isUserspace32bit() && isAtLeastKernelVersion(6, 7, 0)) {
ALOGE("64-bit userspace required on 6.7+ kernels.");
return 1;
}
@@ -1659,17 +1659,17 @@
}
// unreachable before U QPR3
- {
+ if (exists(uprobestatsBpfLoader)) {
ALOGI("done, transferring control to uprobestatsbpfload.");
const char *args[] = {
uprobestatsBpfLoader,
NULL,
};
execve(args[0], (char **)args, envp);
+ ALOGI("unable to execute uprobestatsbpfload, transferring control to "
+ "platform bpfloader.");
}
- ALOGI("unable to execute uprobestatsbpfload, transferring control to "
- "platform bpfloader.");
// platform BpfLoader *needs* to run as root
const char * args[] = { platformBpfLoader, NULL, };
execve(args[0], (char**)args, envp);
diff --git a/bpf/syscall_wrappers/include/BpfSyscallWrappers.h b/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
index 73cef89..a31445a 100644
--- a/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
+++ b/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
@@ -16,24 +16,20 @@
#pragma once
+#include <android-base/unique_fd.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/bpf.h>
#include <linux/unistd.h>
#include <sys/file.h>
-#ifdef BPF_FD_JUST_USE_INT
- #define BPF_FD_TYPE int
- #define BPF_FD_TO_U32(x) static_cast<__u32>(x)
-#else
- #include <android-base/unique_fd.h>
- #define BPF_FD_TYPE base::unique_fd&
- #define BPF_FD_TO_U32(x) static_cast<__u32>((x).get())
-#endif
namespace android {
namespace bpf {
+using ::android::base::borrowed_fd;
+using ::android::base::unique_fd;
+
inline uint64_t ptr_to_u64(const void * const x) {
return (uint64_t)(uintptr_t)x;
}
@@ -69,58 +65,59 @@
// 'inner_map_fd' is basically a template specifying {map_type, key_size, value_size, max_entries, map_flags}
// of the inner map type (and possibly only key_size/value_size actually matter?).
inline int createOuterMap(bpf_map_type map_type, uint32_t key_size, uint32_t value_size,
- uint32_t max_entries, uint32_t map_flags, const BPF_FD_TYPE inner_map_fd) {
+ uint32_t max_entries, uint32_t map_flags,
+ const borrowed_fd& inner_map_fd) {
return bpf(BPF_MAP_CREATE, {
.map_type = map_type,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries,
.map_flags = map_flags,
- .inner_map_fd = BPF_FD_TO_U32(inner_map_fd),
+ .inner_map_fd = static_cast<__u32>(inner_map_fd.get()),
});
}
-inline int writeToMapEntry(const BPF_FD_TYPE map_fd, const void* key, const void* value,
+inline int writeToMapEntry(const borrowed_fd& map_fd, const void* key, const void* value,
uint64_t flags) {
return bpf(BPF_MAP_UPDATE_ELEM, {
- .map_fd = BPF_FD_TO_U32(map_fd),
+ .map_fd = static_cast<__u32>(map_fd.get()),
.key = ptr_to_u64(key),
.value = ptr_to_u64(value),
.flags = flags,
});
}
-inline int findMapEntry(const BPF_FD_TYPE map_fd, const void* key, void* value) {
+inline int findMapEntry(const borrowed_fd& map_fd, const void* key, void* value) {
return bpf(BPF_MAP_LOOKUP_ELEM, {
- .map_fd = BPF_FD_TO_U32(map_fd),
+ .map_fd = static_cast<__u32>(map_fd.get()),
.key = ptr_to_u64(key),
.value = ptr_to_u64(value),
});
}
-inline int deleteMapEntry(const BPF_FD_TYPE map_fd, const void* key) {
+inline int deleteMapEntry(const borrowed_fd& map_fd, const void* key) {
return bpf(BPF_MAP_DELETE_ELEM, {
- .map_fd = BPF_FD_TO_U32(map_fd),
+ .map_fd = static_cast<__u32>(map_fd.get()),
.key = ptr_to_u64(key),
});
}
-inline int getNextMapKey(const BPF_FD_TYPE map_fd, const void* key, void* next_key) {
+inline int getNextMapKey(const borrowed_fd& map_fd, const void* key, void* next_key) {
return bpf(BPF_MAP_GET_NEXT_KEY, {
- .map_fd = BPF_FD_TO_U32(map_fd),
+ .map_fd = static_cast<__u32>(map_fd.get()),
.key = ptr_to_u64(key),
.next_key = ptr_to_u64(next_key),
});
}
-inline int getFirstMapKey(const BPF_FD_TYPE map_fd, void* firstKey) {
+inline int getFirstMapKey(const borrowed_fd& map_fd, void* firstKey) {
return getNextMapKey(map_fd, NULL, firstKey);
}
-inline int bpfFdPin(const BPF_FD_TYPE map_fd, const char* pathname) {
+inline int bpfFdPin(const borrowed_fd& map_fd, const char* pathname) {
return bpf(BPF_OBJ_PIN, {
.pathname = ptr_to_u64(pathname),
- .bpf_fd = BPF_FD_TO_U32(map_fd),
+ .bpf_fd = static_cast<__u32>(map_fd.get()),
});
}
@@ -131,22 +128,15 @@
});
}
-int bpfGetFdMapId(const BPF_FD_TYPE map_fd);
+int bpfGetFdMapId(const borrowed_fd& map_fd);
inline int bpfLock(int fd, short type) {
if (fd < 0) return fd; // pass any errors straight through
#ifdef BPF_MAP_LOCKLESS_FOR_TEST
return fd;
#endif
-#ifdef BPF_FD_JUST_USE_INT
int mapId = bpfGetFdMapId(fd);
int saved_errno = errno;
-#else
- base::unique_fd ufd(fd);
- int mapId = bpfGetFdMapId(ufd);
- int saved_errno = errno;
- (void)ufd.release();
-#endif
// 4.14+ required to fetch map id, but we don't want to call isAtLeastKernelVersion
if (mapId == -1 && saved_errno == EINVAL) return fd;
if (mapId <= 0) abort(); // should not be possible
@@ -193,37 +183,35 @@
}
inline bool usableProgram(const char* pathname) {
- int fd = retrieveProgram(pathname);
- bool ok = (fd >= 0);
- if (ok) close(fd);
- return ok;
+ unique_fd fd(retrieveProgram(pathname));
+ return fd.ok();
}
-inline int attachProgram(bpf_attach_type type, const BPF_FD_TYPE prog_fd,
- const BPF_FD_TYPE cg_fd, uint32_t flags = 0) {
+inline int attachProgram(bpf_attach_type type, const borrowed_fd& prog_fd,
+ const borrowed_fd& cg_fd, uint32_t flags = 0) {
return bpf(BPF_PROG_ATTACH, {
- .target_fd = BPF_FD_TO_U32(cg_fd),
- .attach_bpf_fd = BPF_FD_TO_U32(prog_fd),
+ .target_fd = static_cast<__u32>(cg_fd.get()),
+ .attach_bpf_fd = static_cast<__u32>(prog_fd.get()),
.attach_type = type,
.attach_flags = flags,
});
}
-inline int detachProgram(bpf_attach_type type, const BPF_FD_TYPE cg_fd) {
+inline int detachProgram(bpf_attach_type type, const borrowed_fd& cg_fd) {
return bpf(BPF_PROG_DETACH, {
- .target_fd = BPF_FD_TO_U32(cg_fd),
+ .target_fd = static_cast<__u32>(cg_fd.get()),
.attach_type = type,
});
}
-inline int queryProgram(const BPF_FD_TYPE cg_fd,
+inline int queryProgram(const borrowed_fd& cg_fd,
enum bpf_attach_type attach_type,
__u32 query_flags = 0,
__u32 attach_flags = 0) {
int prog_id = -1; // equivalent to an array of one integer.
bpf_attr arg = {
.query = {
- .target_fd = BPF_FD_TO_U32(cg_fd),
+ .target_fd = static_cast<__u32>(cg_fd.get()),
.attach_type = attach_type,
.query_flags = query_flags,
.attach_flags = attach_flags,
@@ -237,21 +225,21 @@
return prog_id; // return actual id
}
-inline int detachSingleProgram(bpf_attach_type type, const BPF_FD_TYPE prog_fd,
- const BPF_FD_TYPE cg_fd) {
+inline int detachSingleProgram(bpf_attach_type type, const borrowed_fd& prog_fd,
+ const borrowed_fd& cg_fd) {
return bpf(BPF_PROG_DETACH, {
- .target_fd = BPF_FD_TO_U32(cg_fd),
- .attach_bpf_fd = BPF_FD_TO_U32(prog_fd),
+ .target_fd = static_cast<__u32>(cg_fd.get()),
+ .attach_bpf_fd = static_cast<__u32>(prog_fd.get()),
.attach_type = type,
});
}
// Available in 4.12 and later kernels.
-inline int runProgram(const BPF_FD_TYPE prog_fd, const void* data,
+inline int runProgram(const borrowed_fd& prog_fd, const void* data,
const uint32_t data_size) {
return bpf(BPF_PROG_RUN, {
.test = {
- .prog_fd = BPF_FD_TO_U32(prog_fd),
+ .prog_fd = static_cast<__u32>(prog_fd.get()),
.data_size_in = data_size,
.data_in = ptr_to_u64(data),
},
@@ -265,10 +253,10 @@
// supported/returned by the running kernel. We do this by checking it is fully
// within the bounds of the struct size as reported by the kernel.
#define DEFINE_BPF_GET_FD(TYPE, NAME, FIELD) \
-inline int bpfGetFd ## NAME(const BPF_FD_TYPE fd) { \
+inline int bpfGetFd ## NAME(const borrowed_fd& fd) { \
struct bpf_ ## TYPE ## _info info = {}; \
union bpf_attr attr = { .info = { \
- .bpf_fd = BPF_FD_TO_U32(fd), \
+ .bpf_fd = static_cast<__u32>(fd.get()), \
.info_len = sizeof(info), \
.info = ptr_to_u64(&info), \
}}; \
@@ -283,19 +271,16 @@
// All 7 of these fields are already present in Linux v4.14 (even ACK 4.14-P)
// while BPF_OBJ_GET_INFO_BY_FD is not implemented at all in v4.9 (even ACK 4.9-Q)
-DEFINE_BPF_GET_FD(map, MapType, type) // int bpfGetFdMapType(const BPF_FD_TYPE map_fd)
-DEFINE_BPF_GET_FD(map, MapId, id) // int bpfGetFdMapId(const BPF_FD_TYPE map_fd)
-DEFINE_BPF_GET_FD(map, KeySize, key_size) // int bpfGetFdKeySize(const BPF_FD_TYPE map_fd)
-DEFINE_BPF_GET_FD(map, ValueSize, value_size) // int bpfGetFdValueSize(const BPF_FD_TYPE map_fd)
-DEFINE_BPF_GET_FD(map, MaxEntries, max_entries) // int bpfGetFdMaxEntries(const BPF_FD_TYPE map_fd)
-DEFINE_BPF_GET_FD(map, MapFlags, map_flags) // int bpfGetFdMapFlags(const BPF_FD_TYPE map_fd)
-DEFINE_BPF_GET_FD(prog, ProgId, id) // int bpfGetFdProgId(const BPF_FD_TYPE prog_fd)
+DEFINE_BPF_GET_FD(map, MapType, type) // int bpfGetFdMapType(const borrowed_fd& map_fd)
+DEFINE_BPF_GET_FD(map, MapId, id) // int bpfGetFdMapId(const borrowed_fd& map_fd)
+DEFINE_BPF_GET_FD(map, KeySize, key_size) // int bpfGetFdKeySize(const borrowed_fd& map_fd)
+DEFINE_BPF_GET_FD(map, ValueSize, value_size) // int bpfGetFdValueSize(const borrowed_fd& map_fd)
+DEFINE_BPF_GET_FD(map, MaxEntries, max_entries) // int bpfGetFdMaxEntries(const borrowed_fd& map_fd)
+DEFINE_BPF_GET_FD(map, MapFlags, map_flags) // int bpfGetFdMapFlags(const borrowed_fd& map_fd)
+DEFINE_BPF_GET_FD(prog, ProgId, id) // int bpfGetFdProgId(const borrowed_fd& prog_fd)
#undef DEFINE_BPF_GET_FD
} // namespace bpf
} // namespace android
-#undef BPF_FD_TO_U32
-#undef BPF_FD_TYPE
-#undef BPF_FD_JUST_USE_INT
diff --git a/clatd/main.c b/clatd/main.c
index f888041..7aa1671 100644
--- a/clatd/main.c
+++ b/clatd/main.c
@@ -37,7 +37,7 @@
/* function: stop_loop
* signal handler: stop the event loop
*/
-static void stop_loop() { running = 0; };
+static void stop_loop(__attribute__((unused)) int unused) { running = 0; };
/* function: print_help
* in case the user is running this on the command line
diff --git a/common/FlaggedApi.bp b/common/FlaggedApi.bp
index 39ff2d4..f89ff9d 100644
--- a/common/FlaggedApi.bp
+++ b/common/FlaggedApi.bp
@@ -17,6 +17,7 @@
aconfig_declarations {
name: "com.android.net.flags-aconfig",
package: "com.android.net.flags",
+ exportable: true,
container: "com.android.tethering",
srcs: ["flags.aconfig"],
visibility: ["//packages/modules/Connectivity:__subpackages__"],
@@ -32,6 +33,17 @@
],
}
+java_aconfig_library {
+ name: "com.android.net.flags-aconfig-java-export",
+ aconfig_declarations: "com.android.net.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ min_sdk_version: "30",
+ apex_available: [
+ "com.android.wifi",
+ ],
+ mode: "exported",
+}
+
aconfig_declarations {
name: "com.android.net.thread.flags-aconfig",
package: "com.android.net.thread.flags",
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 17ef94b..60a827b 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -156,3 +156,12 @@
bug: "354619988"
is_fixed_read_only: true
}
+
+flag {
+ name: "ipv6_over_ble"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "API flag for IPv6 over BLE"
+ bug: "372936361"
+ is_fixed_read_only: true
+}
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index 7551b92..9d6d356 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -102,6 +102,7 @@
java_library {
name: "framework-connectivity-t-pre-jarjar",
defaults: ["framework-connectivity-t-defaults"],
+ installable: false,
libs: [
"framework-bluetooth.stubs.module_lib",
"framework-wifi.stubs.module_lib",
@@ -172,6 +173,10 @@
// Tests using hidden APIs
"//cts/tests/netlegacy22.api",
"//cts/tests/tests/app.usage", // NetworkUsageStatsTest
+
+ // TODO: b/374174952 Remove it when VCN CTS is moved to Connectivity/
+ "//cts/tests/tests/vcn",
+
"//external/sl4a:__subpackages__",
"//frameworks/base/core/tests/bandwidthtests",
"//frameworks/base/core/tests/benchmarks",
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index 9ae0cf7..d66482c 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -210,6 +210,23 @@
package android.net.nsd {
+ @FlaggedApi("com.android.net.flags.ipv6_over_ble") public final class AdvertisingRequest implements android.os.Parcelable {
+ method public int describeContents();
+ method public long getFlags();
+ method public int getProtocolType();
+ method @NonNull public android.net.nsd.NsdServiceInfo getServiceInfo();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.AdvertisingRequest> CREATOR;
+ field public static final long FLAG_SKIP_PROBING = 2L; // 0x2L
+ }
+
+ @FlaggedApi("com.android.net.flags.ipv6_over_ble") public static final class AdvertisingRequest.Builder {
+ ctor public AdvertisingRequest.Builder(@NonNull android.net.nsd.NsdServiceInfo);
+ method @NonNull public android.net.nsd.AdvertisingRequest build();
+ method @NonNull public android.net.nsd.AdvertisingRequest.Builder setFlags(long);
+ method @NonNull public android.net.nsd.AdvertisingRequest.Builder setProtocolType(int);
+ }
+
@FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public final class DiscoveryRequest implements android.os.Parcelable {
method public int describeContents();
method @Nullable public android.net.Network getNetwork();
@@ -288,6 +305,7 @@
method public java.util.Map<java.lang.String,byte[]> getAttributes();
method @Deprecated public java.net.InetAddress getHost();
method @NonNull public java.util.List<java.net.InetAddress> getHostAddresses();
+ method @FlaggedApi("com.android.net.flags.ipv6_over_ble") @Nullable public String getHostname();
method @Nullable public android.net.Network getNetwork();
method public int getPort();
method public String getServiceName();
diff --git a/framework-t/src/android/net/NetworkStatsAccess.java b/framework-t/src/android/net/NetworkStatsAccess.java
index 7c9b3ec..449588a 100644
--- a/framework-t/src/android/net/NetworkStatsAccess.java
+++ b/framework-t/src/android/net/NetworkStatsAccess.java
@@ -111,6 +111,12 @@
/** Returns the {@link NetworkStatsAccess.Level} for the given caller. */
public static @NetworkStatsAccess.Level int checkAccessLevel(
Context context, int callingPid, int callingUid, @Nullable String callingPackage) {
+ final int appId = UserHandle.getAppId(callingUid);
+ if (appId == Process.SYSTEM_UID) {
+ // the system can access data usage for all apps on the device.
+ // check system uid first, to avoid possible dead lock from other APIs
+ return NetworkStatsAccess.Level.DEVICE;
+ }
final DevicePolicyManager mDpm = context.getSystemService(DevicePolicyManager.class);
final TelephonyManager tm = (TelephonyManager)
context.getSystemService(Context.TELEPHONY_SERVICE);
@@ -126,16 +132,13 @@
Binder.restoreCallingIdentity(token);
}
- final int appId = UserHandle.getAppId(callingUid);
-
final boolean isNetworkStack = PermissionUtils.hasAnyPermissionOf(
context, callingPid, callingUid, android.Manifest.permission.NETWORK_STACK,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
- if (hasCarrierPrivileges || isDeviceOwner
- || appId == Process.SYSTEM_UID || isNetworkStack) {
- // Carrier-privileged apps and device owners, and the system (including the
- // network stack) can access data usage for all apps on the device.
+ if (hasCarrierPrivileges || isDeviceOwner || isNetworkStack) {
+ // Carrier-privileged apps and device owners, and the network stack
+ // can access data usage for all apps on the device.
return NetworkStatsAccess.Level.DEVICE;
}
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index caf3152..868033a 100644
--- a/framework-t/src/android/net/TrafficStats.java
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -17,6 +17,7 @@
package android.net;
import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.net.NetworkStats.UID_ALL;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
@@ -33,21 +34,25 @@
import android.content.Context;
import android.media.MediaPlayer;
import android.net.netstats.StatsResult;
+import android.net.netstats.TrafficStatsRateLimitCacheConfig;
import android.os.Binder;
import android.os.Build;
import android.os.RemoteException;
import android.os.StrictMode;
+import android.os.SystemClock;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.BinderUtils;
+import com.android.net.module.util.LruCacheWithExpiry;
import java.io.FileDescriptor;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.Socket;
import java.net.SocketException;
-
+import java.util.function.LongSupplier;
/**
* Class that provides network traffic statistics. These statistics include
@@ -182,11 +187,48 @@
/** @hide */
public static final int TAG_SYSTEM_PROBE = 0xFFFFFF42;
+ private static final StatsResult EMPTY_STATS = new StatsResult(0L, 0L, 0L, 0L);
+
+ private static final Object sRateLimitCacheLock = new Object();
+
@GuardedBy("TrafficStats.class")
+ @Nullable
private static INetworkStatsService sStatsService;
- @GuardedBy("TrafficStats.class")
+
+ // The variable will only be accessed in the test, which is effectively
+ // single-threaded.
+ @Nullable
private static INetworkStatsService sStatsServiceForTest = null;
+ // This holds the configuration for the TrafficStats rate limit caches.
+ // It will be filled with the result of a query to the service the first time
+ // the caller invokes get*Stats APIs.
+ // This variable can be accessed from any thread with the lock held.
+ @GuardedBy("sRateLimitCacheLock")
+ @Nullable
+ private static TrafficStatsRateLimitCacheConfig sRateLimitCacheConfig;
+
+ // Cache for getIfaceStats and getTotalStats binder interfaces.
+ // This variable can be accessed from any thread with the lock held,
+ // while the cache itself is thread-safe and can be accessed outside
+ // the lock.
+ @GuardedBy("sRateLimitCacheLock")
+ @Nullable
+ private static LruCacheWithExpiry<String, StatsResult> sRateLimitIfaceCache;
+
+ // Cache for getUidStats binder interface.
+ // This variable can be accessed from any thread with the lock held,
+ // while the cache itself is thread-safe and can be accessed outside
+ // the lock.
+ @GuardedBy("sRateLimitCacheLock")
+ @Nullable
+ private static LruCacheWithExpiry<Integer, StatsResult> sRateLimitUidCache;
+
+ // The variable will only be accessed in the test, which is effectively
+ // single-threaded.
+ @Nullable
+ private static LongSupplier sTimeSupplierForTest = null;
+
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
private synchronized static INetworkStatsService getStatsService() {
if (sStatsServiceForTest != null) return sStatsServiceForTest;
@@ -209,9 +251,29 @@
*/
@VisibleForTesting(visibility = PRIVATE)
public static void setServiceForTest(INetworkStatsService statsService) {
- synchronized (TrafficStats.class) {
- sStatsServiceForTest = statsService;
- }
+ sStatsServiceForTest = statsService;
+ }
+
+ /**
+ * Set time supplier for test, or null to reset.
+ *
+ * @hide
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public static void setTimeSupplierForTest(LongSupplier timeSupplier) {
+ sTimeSupplierForTest = timeSupplier;
+ }
+
+ /**
+ * Trigger query rate-limit cache config and initializing the caches.
+ *
+ * This is for test purpose.
+ *
+ * @hide
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public static void reinitRateLimitCacheForTest() {
+ maybeGetConfigAndInitRateLimitCache(true /* forceReinit */);
}
/**
@@ -254,6 +316,92 @@
sStatsService = statsManager.getBinder();
}
+ @Nullable
+ private static LruCacheWithExpiry<String, StatsResult> maybeGetRateLimitIfaceCache() {
+ if (!maybeGetConfigAndInitRateLimitCache(false /* forceReinit */)) return null;
+ synchronized (sRateLimitCacheLock) {
+ return sRateLimitIfaceCache;
+ }
+ }
+
+ @Nullable
+ private static LruCacheWithExpiry<Integer, StatsResult> maybeGetRateLimitUidCache() {
+ if (!maybeGetConfigAndInitRateLimitCache(false /* forceReinit */)) return null;
+ synchronized (sRateLimitCacheLock) {
+ return sRateLimitUidCache;
+ }
+ }
+
+ /**
+ * Gets the rate limit cache configuration and init caches if null.
+ *
+ * Gets the configuration from the service as the configuration
+ * is not expected to change dynamically. And use it to initialize
+ * rate-limit cache if not yet initialized.
+ *
+ * @return whether the rate-limit cache is enabled.
+ *
+ * @hide
+ */
+ private static boolean maybeGetConfigAndInitRateLimitCache(boolean forceReinit) {
+ // Access the service outside the lock to avoid potential deadlocks. This is
+ // especially important when the caller is a system component (e.g.,
+ // NetworkPolicyManagerService) that might hold other locks that the service
+ // also needs.
+ // Although this introduces a race condition where multiple threads might
+ // query the service concurrently, it's acceptable in this case because the
+ // configuration doesn't change dynamically. The configuration only needs to
+ // be fetched once before initializing the cache.
+ synchronized (sRateLimitCacheLock) {
+ if (sRateLimitCacheConfig != null && !forceReinit) {
+ return sRateLimitCacheConfig.isCacheEnabled;
+ }
+ }
+
+ final TrafficStatsRateLimitCacheConfig config;
+ try {
+ config = getStatsService().getRateLimitCacheConfig();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ synchronized (sRateLimitCacheLock) {
+ if (sRateLimitCacheConfig == null || forceReinit) {
+ sRateLimitCacheConfig = config;
+ initRateLimitCacheLocked();
+ }
+ }
+ return config.isCacheEnabled;
+ }
+
+ @GuardedBy("sRateLimitCacheLock")
+ private static void initRateLimitCacheLocked() {
+ // Set up rate limiting caches.
+ // Use uid cache with UID_ALL to cache total stats.
+ if (sRateLimitCacheConfig.isCacheEnabled) {
+ // A time supplier which is monotonic until device reboots, and counts
+ // time spent in sleep. This is needed to ensure the get*Stats caller
+ // won't get stale value after system time adjustment or waking up from sleep.
+ final LongSupplier realtimeSupplier = (sTimeSupplierForTest != null
+ ? sTimeSupplierForTest : () -> SystemClock.elapsedRealtime());
+ sRateLimitIfaceCache = new LruCacheWithExpiry<String, StatsResult>(
+ realtimeSupplier,
+ sRateLimitCacheConfig.expiryDurationMs,
+ sRateLimitCacheConfig.maxEntries,
+ (statsResult) -> !isEmpty(statsResult)
+ );
+ sRateLimitUidCache = new LruCacheWithExpiry<Integer, StatsResult>(
+ realtimeSupplier,
+ sRateLimitCacheConfig.expiryDurationMs,
+ sRateLimitCacheConfig.maxEntries,
+ (statsResult) -> !isEmpty(statsResult)
+ );
+ } else {
+ sRateLimitIfaceCache = null;
+ sRateLimitUidCache = null;
+ }
+ }
+
/**
* Attach the socket tagger implementation to the current process, to
* get notified when a socket's {@link FileDescriptor} is assigned to
@@ -736,6 +884,14 @@
android.Manifest.permission.NETWORK_STACK,
android.Manifest.permission.NETWORK_SETTINGS})
public static void clearRateLimitCaches() {
+ final LruCacheWithExpiry<String, StatsResult> ifaceCache = maybeGetRateLimitIfaceCache();
+ if (ifaceCache != null) {
+ ifaceCache.clear();
+ }
+ final LruCacheWithExpiry<Integer, StatsResult> uidCache = maybeGetRateLimitUidCache();
+ if (uidCache != null) {
+ uidCache.clear();
+ }
try {
getStatsService().clearTrafficStatsRateLimitCaches();
} catch (RemoteException e) {
@@ -985,35 +1141,76 @@
/** @hide */
public static long getUidStats(int uid, int type) {
- final StatsResult stats;
+ return fetchStats(maybeGetRateLimitUidCache(), uid,
+ () -> getStatsService().getUidStats(uid), type);
+ }
+
+ // Note: This method calls to the service, do not invoke this method with lock held.
+ private static <K> long fetchStats(
+ @Nullable LruCacheWithExpiry<K, StatsResult> cache, K key,
+ BinderUtils.ThrowingSupplier<StatsResult, RemoteException> statsFetcher, int type) {
try {
- stats = getStatsService().getUidStats(uid);
+ final StatsResult stats;
+ if (cache != null) {
+ stats = fetchStatsWithCache(cache, key, statsFetcher);
+ } else {
+ // Cache is not enabled, fetch directly from service.
+ stats = statsFetcher.get();
+ }
+ return getEntryValueForType(stats, type);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
- return getEntryValueForType(stats, type);
+ }
+
+ // Note: This method calls to the service, do not invoke this method with lock held.
+ @Nullable
+ private static <K> StatsResult fetchStatsWithCache(LruCacheWithExpiry<K, StatsResult> cache,
+ K key, BinderUtils.ThrowingSupplier<StatsResult, RemoteException> statsFetcher)
+ throws RemoteException {
+ // Attempt to retrieve from the cache first.
+ StatsResult stats = cache.get(key);
+
+ // Although the cache instance is thread-safe, this can still introduce a
+ // race condition between threads of the same process, potentially
+ // returning non-monotonic results. This is because there is no lock
+ // between get, fetch, and put operations. This is considered acceptable
+ // because varying thread execution speeds can also cause non-monotonic
+ // results, even with locking.
+ if (stats == null) {
+ // Cache miss, fetch from the service.
+ stats = statsFetcher.get();
+
+ // Update the cache with the fetched result if valid.
+ if (stats != null && !isEmpty(stats)) {
+ final StatsResult cachedValue = cache.putIfAbsent(key, stats);
+ if (cachedValue != null) {
+ // Some other thread cached a value after this thread
+ // originally got a cache miss. Return the cached value
+ // to ensure all returned values after caching are consistent.
+ return cachedValue;
+ }
+ }
+ }
+ return stats;
+ }
+
+ private static boolean isEmpty(StatsResult stats) {
+ return stats.equals(EMPTY_STATS);
}
/** @hide */
public static long getTotalStats(int type) {
- final StatsResult stats;
- try {
- stats = getStatsService().getTotalStats();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- return getEntryValueForType(stats, type);
+ // In practice, Bpf doesn't use UID_ALL for storing per-UID stats.
+ // Use uid cache with UID_ALL to cache total stats.
+ return fetchStats(maybeGetRateLimitUidCache(), UID_ALL,
+ () -> getStatsService().getTotalStats(), type);
}
/** @hide */
public static long getIfaceStats(String iface, int type) {
- final StatsResult stats;
- try {
- stats = getStatsService().getIfaceStats(iface);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- return getEntryValueForType(stats, type);
+ return fetchStats(maybeGetRateLimitIfaceCache(), iface,
+ () -> getStatsService().getIfaceStats(iface), type);
}
/**
diff --git a/framework-t/src/android/net/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java
index 6afb2d5..a62df65 100644
--- a/framework-t/src/android/net/nsd/AdvertisingRequest.java
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -15,12 +15,16 @@
*/
package android.net.nsd;
+import android.annotation.FlaggedApi;
import android.annotation.LongDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.net.nsd.NsdManager.ProtocolType;
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.net.flags.Flags;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.Duration;
@@ -28,16 +32,32 @@
/**
* Encapsulates parameters for {@link NsdManager#registerService}.
- * @hide
*/
-//@FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+@FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
public final class AdvertisingRequest implements Parcelable {
/**
* Only update the registration without sending exit and re-announcement.
+ * @hide
*/
public static final long NSD_ADVERTISING_UPDATE_ONLY = 1;
+ // TODO: if apps are allowed to set hostnames, the below doc should be updated to mention that
+ // passed in hostnames must also be known unique to use this flag.
+ /**
+ * Skip the probing step when advertising.
+ *
+ * <p>This must only be used when the service name ({@link NsdServiceInfo#getServiceName()} is
+ * known to be unique and cannot possibly be used by any other device on the network.
+ */
+ public static final long FLAG_SKIP_PROBING = 1 << 1;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @LongDef(flag = true, prefix = {"FLAG_"}, value = {
+ FLAG_SKIP_PROBING,
+ })
+ public @interface AdvertisingFlags {}
@NonNull
public static final Creator<AdvertisingRequest> CREATOR =
@@ -79,7 +99,7 @@
/**
* The constructor for the advertiseRequest
*/
- private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, int protocolType,
+ private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, @ProtocolType int protocolType,
long advertisingConfig, @NonNull Duration ttl) {
mServiceInfo = serviceInfo;
mProtocolType = protocolType;
@@ -88,7 +108,7 @@
}
/**
- * Returns the {@link NsdServiceInfo}
+ * @return the {@link NsdServiceInfo} describing the service to advertise.
*/
@NonNull
public NsdServiceInfo getServiceInfo() {
@@ -96,16 +116,18 @@
}
/**
- * Returns the service advertise protocol
+ * @return the service advertisement protocol.
*/
+ @ProtocolType
public int getProtocolType() {
return mProtocolType;
}
/**
- * Returns the advertising config.
+ * @return the flags affecting advertising behavior.
*/
- public long getAdvertisingConfig() {
+ @AdvertisingFlags
+ public long getFlags() {
return mAdvertisingConfig;
}
@@ -165,34 +187,45 @@
dest.writeLong(mTtl == null ? -1L : mTtl.getSeconds());
}
-// @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
/**
- * The builder for creating new {@link AdvertisingRequest} objects.
- * @hide
+ * A builder for creating new {@link AdvertisingRequest} objects.
*/
+ @FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
public static final class Builder {
@NonNull
private final NsdServiceInfo mServiceInfo;
- private final int mProtocolType;
+ private int mProtocolType;
private long mAdvertisingConfig;
@Nullable
private Duration mTtl;
+
/**
* Creates a new {@link Builder} object.
+ * @param serviceInfo the {@link NsdServiceInfo} describing the service to advertise.
+ * @param protocolType the advertising protocol to use.
+ * @hide
*/
- public Builder(@NonNull NsdServiceInfo serviceInfo, int protocolType) {
+ public Builder(@NonNull NsdServiceInfo serviceInfo, @ProtocolType int protocolType) {
mServiceInfo = serviceInfo;
mProtocolType = protocolType;
}
/**
+ * Creates a new {@link Builder} object.
+ * @param serviceInfo the {@link NsdServiceInfo} describing the service to advertise.
+ */
+ public Builder(@NonNull NsdServiceInfo serviceInfo) {
+ this(serviceInfo, NsdManager.PROTOCOL_DNS_SD);
+ }
+
+ /**
* Sets advertising configuration flags.
*
- * @param advertisingConfigFlags Bitmask of {@code AdvertisingConfig} flags.
+ * @param flags flags to use for advertising.
*/
@NonNull
- public Builder setAdvertisingConfig(long advertisingConfigFlags) {
- mAdvertisingConfig = advertisingConfigFlags;
+ public Builder setFlags(@AdvertisingFlags long flags) {
+ mAdvertisingConfig = flags;
return this;
}
@@ -232,6 +265,16 @@
return this;
}
+ /**
+ * Sets the protocol to use for advertising.
+ * @param protocolType the advertising protocol to use.
+ */
+ @NonNull
+ public Builder setProtocolType(@ProtocolType int protocolType) {
+ mProtocolType = protocolType;
+ return this;
+ }
+
/** Creates a new {@link AdvertisingRequest} object. */
@NonNull
public AdvertisingRequest build() {
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index 116bea6..426a92d 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -314,6 +314,13 @@
/** Dns based service discovery protocol */
public static final int PROTOCOL_DNS_SD = 0x0001;
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"PROTOCOL_"}, value = {
+ PROTOCOL_DNS_SD,
+ })
+ public @interface ProtocolType {}
+
/**
* The minimum TTL seconds which is allowed for a service registration.
*
@@ -1272,7 +1279,7 @@
// documented in the NsdServiceInfo.setSubtypes API instead, but this provides a limited
// option for users of the older undocumented behavior, only for subtype changes.
if (isSubtypeUpdateRequest(serviceInfo, listener)) {
- builder.setAdvertisingConfig(AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY);
+ builder.setFlags(AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY);
}
registerService(builder.build(), executor, listener);
}
@@ -1358,7 +1365,7 @@
checkProtocol(protocolType);
final int key;
// For update only request, the old listener has to be reused
- if ((advertisingRequest.getAdvertisingConfig()
+ if ((advertisingRequest.getFlags()
& AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0) {
key = updateRegisteredListener(listener, executor, serviceInfo);
} else {
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 18c59d9..6a5ab4d 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -200,19 +200,19 @@
/**
* Get the hostname.
*
- * <p>When a service is resolved, it returns the hostname of the resolved service . The top
- * level domain ".local." is omitted.
- *
- * <p>For example, it returns "MyHost" when the service's hostname is "MyHost.local.".
- *
- * @hide
+ * <p>When a service is resolved through {@link NsdManager#resolveService} or
+ * {@link NsdManager#registerServiceInfoCallback}, this returns the hostname of the resolved
+ * service. In all other cases, this will be null. The top level domain ".local." is omitted.
+ * For example, this returns "MyHost" when the service's hostname is "MyHost.local.".
*/
-// @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_HOSTNAME_ENABLED)
+ @FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
@Nullable
public String getHostname() {
return mHostname;
}
+ // TODO: if setHostname is made public, AdvertisingRequest#FLAG_SKIP_PROBING javadoc must be
+ // updated to mention that hostnames must also be known unique to use that flag.
/**
* Set a custom hostname for this service instance for registration.
*
diff --git a/framework/Android.bp b/framework/Android.bp
index 0334e11..a1c6a15 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -64,6 +64,7 @@
":net-utils-framework-common-srcs",
":framework-connectivity-api-shared-srcs",
":framework-networksecurity-sources",
+ ":statslog-framework-connectivity-java-gen",
],
aidl: {
generate_get_transaction_name: true,
@@ -74,6 +75,7 @@
// the module builds against API (the parcelable declarations exist in framework.aidl)
"frameworks/base/core/java", // For framework parcelables
"frameworks/native/aidl/binder", // For PersistableBundle.aidl
+ "packages/modules/Connectivity/Tethering/common/TetheringLib/src",
],
},
stub_only_libs: [
@@ -104,6 +106,7 @@
"androidx.annotation_annotation",
"app-compat-annotations",
"framework-connectivity-t.stubs.module_lib",
+ "framework-statsd.stubs.module_lib",
"unsupportedappusage",
],
apex_available: [
@@ -141,6 +144,7 @@
java_library {
name: "framework-connectivity-pre-jarjar",
defaults: ["framework-module-defaults"],
+ installable: false,
min_sdk_version: "30",
static_libs: [
"framework-connectivity-pre-jarjar-without-cronet",
@@ -156,6 +160,7 @@
java_defaults {
name: "CronetJavaDefaults",
srcs: [":httpclient_api_sources"],
+ static_libs: ["com.android.net.http.flags-aconfig-java"],
libs: [
"androidx.annotation_annotation",
],
@@ -187,6 +192,10 @@
// Tests using hidden APIs
"//cts/tests/netlegacy22.api",
"//cts/tests/tests/app.usage", // NetworkUsageStatsTest
+
+ // TODO: b/374174952 Remove it when VCN CTS is moved to Connectivity/
+ "//cts/tests/tests/vcn",
+
"//external/sl4a:__subpackages__",
"//frameworks/base/core/tests/bandwidthtests",
"//frameworks/base/core/tests/benchmarks",
@@ -212,6 +221,7 @@
},
aconfig_declarations: [
"com.android.net.flags-aconfig",
+ "com.android.net.http.flags-aconfig",
"com.android.networksecurity.flags-aconfig",
],
}
@@ -326,6 +336,7 @@
aidl: {
include_dirs: [
"packages/modules/Connectivity/framework/aidl-export",
+ "packages/modules/Connectivity/Tethering/common/TetheringLib/src",
"frameworks/native/aidl/binder", // For PersistableBundle.aidl
],
},
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 7bc0cf3..797c107 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -103,6 +103,7 @@
method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, int);
method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler, int);
method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.app.PendingIntent);
+ method @FlaggedApi("com.android.net.flags.ipv6_over_ble") public void reserveNetwork(@NonNull android.net.NetworkRequest, @NonNull android.os.Handler, @NonNull android.net.ConnectivityManager.NetworkCallback);
method @Deprecated public void setNetworkPreference(int);
method @Deprecated public static boolean setProcessDefaultNetwork(@Nullable android.net.Network);
method public void unregisterNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback);
@@ -151,6 +152,7 @@
method public void onLinkPropertiesChanged(@NonNull android.net.Network, @NonNull android.net.LinkProperties);
method public void onLosing(@NonNull android.net.Network, int);
method public void onLost(@NonNull android.net.Network);
+ method @FlaggedApi("com.android.net.flags.ipv6_over_ble") public void onReserved(@NonNull android.net.NetworkCapabilities);
method public void onUnavailable();
field public static final int FLAG_INCLUDE_LOCATION_INFO = 1; // 0x1
}
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index cd7307f..0129e5c 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -247,3 +247,11 @@
}
+package android.net.http {
+
+ public abstract class HttpEngine {
+ method @FlaggedApi("android.net.http.preload_httpengine_in_zygote") public static void preload();
+ }
+
+}
+
diff --git a/framework/src/android/net/CaptivePortal.java b/framework/src/android/net/CaptivePortal.java
index 4a7b601..4c534f3 100644
--- a/framework/src/android/net/CaptivePortal.java
+++ b/framework/src/android/net/CaptivePortal.java
@@ -18,10 +18,19 @@
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
+import android.annotation.TargetApi;
+import android.os.Binder;
+import android.os.Build;
import android.os.IBinder;
+import android.os.OutcomeReceiver;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.system.OsConstants;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
/**
* A class allowing apps handling the {@link ConnectivityManager#ACTION_CAPTIVE_PORTAL_SIGN_IN}
@@ -69,6 +78,15 @@
@SystemApi
public static final int APP_REQUEST_REEVALUATION_REQUIRED = APP_REQUEST_BASE + 0;
+ /**
+ * Binder object used for tracking the lifetime of the process, so CS can perform cleanup if
+ * the CaptivePortal app dies. This binder is not parcelled as part of this object. It is
+ * created in the client process and sent to the server by setDelegateUid so that the server
+ * can use it to register a death recipient.
+ *
+ */
+ private final Binder mLifetimeBinder = new Binder();
+
private final IBinder mBinder;
/** @hide */
@@ -167,4 +185,56 @@
@SystemApi
public void logEvent(int eventId, @NonNull String packageName) {
}
+
+ /**
+ * Sets the UID of the app that is allowed to perform network traffic for captive
+ * portal login.
+ *
+ * This app will be allowed to communicate directly on the captive
+ * portal by binding to the {@link android.net.Network} extra passed in the
+ * ACTION_CAPTIVE_PORTAL_SIGN_IN broadcast that contained this object.
+ *
+ * Communication will bypass network access restrictions such as VPNs and
+ * Private DNS settings, so the delegated UID must be trusted to ensure that only
+ * traffic intended for captive portal login binds to that network.
+ *
+ * By default, no UID is delegated. The delegation can be cleared by calling
+ * this method again with {@link android.os.Process.INVALID_UID}. Only one UID can
+ * be delegated at any given time.
+ *
+ * The operation is asynchronous. The uid is only guaranteed to have access when
+ * the provided OutcomeReceiver is called.
+ *
+ * @hide
+ */
+ @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
+ // OutcomeReceiver is not available on R, but the mainline version of this
+ // class is only available on S+.
+ @TargetApi(Build.VERSION_CODES.S)
+ public void setDelegateUid(int uid, @NonNull Executor executor,
+ @NonNull final OutcomeReceiver<Void, ServiceSpecificException> receiver) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(receiver);
+ try {
+ ICaptivePortal.Stub.asInterface(mBinder).setDelegateUid(
+ uid,
+ mLifetimeBinder,
+ new IIntResultListener.Stub() {
+ @Override
+ public void onResult(int resultCode) {
+ if (resultCode != 0) {
+ final String msg = "Fail to set the delegate UID " + uid
+ + ", error: " + OsConstants.errnoName(resultCode);
+ executor.execute(() -> {
+ receiver.onError(new ServiceSpecificException(resultCode, msg));
+ });
+ } else {
+ executor.execute(() -> receiver.onResult(null));
+ }
+ }
+ });
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 1ebc4a3..9016d13 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -21,6 +21,7 @@
import static android.net.NetworkRequest.Type.LISTEN;
import static android.net.NetworkRequest.Type.LISTEN_FOR_BEST;
import static android.net.NetworkRequest.Type.REQUEST;
+import static android.net.NetworkRequest.Type.RESERVATION;
import static android.net.NetworkRequest.Type.TRACK_DEFAULT;
import static android.net.NetworkRequest.Type.TRACK_SYSTEM_DEFAULT;
import static android.net.QosCallback.QosCallbackRegistrationException;
@@ -1873,7 +1874,7 @@
public NetworkCapabilities[] getDefaultNetworkCapabilitiesForUser(int userId) {
try {
return mService.getDefaultNetworkCapabilitiesForUser(
- userId, mContext.getOpPackageName(), getAttributionTag());
+ userId, mContext.getOpPackageName(), mContext.getAttributionTag());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1967,7 +1968,7 @@
@NonNull String packageName) {
try {
return mService.getRedactedLinkPropertiesForPackage(
- lp, uid, packageName, getAttributionTag());
+ lp, uid, packageName, mContext.getAttributionTag());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1993,7 +1994,7 @@
public NetworkCapabilities getNetworkCapabilities(@Nullable Network network) {
try {
return mService.getNetworkCapabilities(
- network, mContext.getOpPackageName(), getAttributionTag());
+ network, mContext.getOpPackageName(), mContext.getAttributionTag());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -2027,7 +2028,7 @@
int uid, @NonNull String packageName) {
try {
return mService.getRedactedNetworkCapabilitiesForPackage(nc, uid, packageName,
- getAttributionTag());
+ mContext.getAttributionTag());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -2752,21 +2753,13 @@
checkLegacyRoutingApiAccess();
try {
return mService.requestRouteToHostAddress(networkType, hostAddress.getAddress(),
- mContext.getOpPackageName(), getAttributionTag());
+ mContext.getOpPackageName(), mContext.getAttributionTag());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
- * @return the context's attribution tag
- */
- // TODO: Remove method and replace with direct call once R code is pushed to AOSP
- private @Nullable String getAttributionTag() {
- return mContext.getAttributionTag();
- }
-
- /**
* Returns the value of the setting for background data usage. If false,
* applications should not use the network if the application is not in the
* foreground. Developers should respect this setting, and check the value
@@ -4279,12 +4272,18 @@
private static final int METHOD_ONLOST = 6;
/**
- * Called if no network is found within the timeout time specified in
- * {@link #requestNetwork(NetworkRequest, NetworkCallback, int)} call or if the
- * requested network request cannot be fulfilled (whether or not a timeout was
- * specified). When this callback is invoked the associated
- * {@link NetworkRequest} will have already been removed and released, as if
- * {@link #unregisterNetworkCallback(NetworkCallback)} had been called.
+ * If the callback was registered with one of the {@code requestNetwork} methods, this will
+ * be called if no network is found within the timeout specified in {@link
+ * #requestNetwork(NetworkRequest, NetworkCallback, int)} call or if the requested network
+ * request cannot be fulfilled (whether or not a timeout was specified).
+ *
+ * If the callback was registered when reserving a network, this method indicates that the
+ * reservation is removed. It can be called when the reservation is requested, because the
+ * system could not satisfy the reservation, or after the reserved network connects.
+ *
+ * When this callback is invoked the associated {@link NetworkRequest} will have already
+ * been removed and released, as if {@link #unregisterNetworkCallback(NetworkCallback)} had
+ * been called.
*/
@FilteredCallback(methodId = METHOD_ONUNAVAILABLE, calledByCallbackId = CALLBACK_UNAVAIL)
public void onUnavailable() {}
@@ -4425,6 +4424,28 @@
}
private static final int METHOD_ONBLOCKEDSTATUSCHANGED_INT = 14;
+ /**
+ * Called when a network is reserved.
+ *
+ * The reservation includes the {@link NetworkCapabilities} that uniquely describe the
+ * network that was reserved. the caller communicates this information to hardware or
+ * software components on or off-device to instruct them to create a network matching this
+ * reservation.
+ *
+ * {@link #onReserved(NetworkCapabilities)} is called at most once and is guaranteed to be
+ * called before any other callback unless the reservation is unavailable.
+ *
+ * Once a reservation is made, the reserved {@link NetworkCapabilities} will not be updated,
+ * and the reservation remains in place until the reserved network connects or {@link
+ * #onUnavailable} is called.
+ *
+ * @param networkCapabilities The {@link NetworkCapabilities} of the reservation.
+ */
+ @FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
+ @FilteredCallback(methodId = METHOD_ONRESERVED, calledByCallbackId = CALLBACK_RESERVED)
+ public void onReserved(@NonNull NetworkCapabilities networkCapabilities) {}
+ private static final int METHOD_ONRESERVED = 15;
+
private NetworkRequest networkRequest;
private final int mFlags;
}
@@ -4476,6 +4497,8 @@
public static final int CALLBACK_BLK_CHANGED = 11;
/** @hide */
public static final int CALLBACK_LOCAL_NETWORK_INFO_CHANGED = 12;
+ /** @hide */
+ public static final int CALLBACK_RESERVED = 13;
// When adding new IDs, note CallbackQueue assumes callback IDs are at most 16 bits.
@@ -4495,6 +4518,7 @@
case CALLBACK_RESUMED: return "CALLBACK_RESUMED";
case CALLBACK_BLK_CHANGED: return "CALLBACK_BLK_CHANGED";
case CALLBACK_LOCAL_NETWORK_INFO_CHANGED: return "CALLBACK_LOCAL_NETWORK_INFO_CHANGED";
+ case CALLBACK_RESERVED: return "CALLBACK_RESERVED";
default:
return Integer.toString(whichCallback);
}
@@ -4525,6 +4549,7 @@
public static class NetworkCallbackMethodsHolder {
public static final NetworkCallbackMethod[] NETWORK_CB_METHODS =
new NetworkCallbackMethod[] {
+ method("onReserved", 1 << CALLBACK_RESERVED, NetworkCapabilities.class),
method("onPreCheck", 1 << CALLBACK_PRECHECK, Network.class),
// Note the final overload of onAvailable is not included, since it cannot
// match any overridden method.
@@ -4604,6 +4629,11 @@
}
switch (message.what) {
+ case CALLBACK_RESERVED: {
+ final NetworkCapabilities cap = getObject(message, NetworkCapabilities.class);
+ callback.onReserved(cap);
+ break;
+ }
case CALLBACK_PRECHECK: {
callback.onPreCheck(network);
break;
@@ -4705,12 +4735,12 @@
if (reqType == LISTEN) {
request = mService.listenForNetwork(
need, messenger, binder, callbackFlags, callingPackageName,
- getAttributionTag(), declaredMethodsFlag);
+ mContext.getAttributionTag(), declaredMethodsFlag);
} else {
request = mService.requestNetwork(
asUid, need, reqType.ordinal(), messenger, timeoutMs, binder,
- legacyType, callbackFlags, callingPackageName, getAttributionTag(),
- declaredMethodsFlag);
+ legacyType, callbackFlags, callingPackageName,
+ mContext.getAttributionTag(), declaredMethodsFlag);
}
if (request != null) {
sCallbacks.put(request, callback);
@@ -4985,6 +5015,41 @@
}
/**
+ * Reserve a network to satisfy a set of {@link NetworkCapabilities}.
+ *
+ * Some types of networks require the system to generate (i.e. reserve) some set of information
+ * before a network can be connected. For such networks, {@link #reserveNetwork} can be used
+ * which may lead to a call to {@link NetworkCallback#onReserved(NetworkCapabilities)}
+ * containing the {@link NetworkCapabilities} that were reserved.
+ *
+ * A reservation reserves at most one network. If the network connects, a reservation request
+ * behaves similar to a request filed using {@link #requestNetwork}. The provided {@link
+ * NetworkCallback} will only be called for the reserved network.
+ *
+ * If the system determines that the requested reservation can never be fulfilled, {@link
+ * NetworkCallback#onUnavailable} is called, the reservation is released by the system, and the
+ * provided callback can be reused. Otherwise, the reservation remains in place until the
+ * requested network connects. There is no guarantee that the reserved network will ever
+ * connect.
+ *
+ * @param request {@link NetworkRequest} describing this request.
+ * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+ * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
+ * the callback must not be shared - it uniquely specifies this request.
+ */
+ // TODO: add executor overloads for all network request methods. Any method that passed an
+ // Executor could process the messages on the singleton ConnectivityThread Handler.
+ @SuppressLint("ExecutorRegistration")
+ @FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
+ public void reserveNetwork(@NonNull NetworkRequest request,
+ @NonNull Handler handler,
+ @NonNull NetworkCallback networkCallback) {
+ final CallbackHandler cbHandler = new CallbackHandler(handler);
+ final NetworkCapabilities nc = request.networkCapabilities;
+ sendRequestForNetwork(nc, networkCallback, 0, RESERVATION, TYPE_NONE, cbHandler);
+ }
+
+ /**
* Request a network to satisfy a set of {@link NetworkCapabilities}, limited
* by a timeout.
*
@@ -5127,7 +5192,7 @@
try {
mService.pendingRequestForNetwork(
request.networkCapabilities, operation, mContext.getOpPackageName(),
- getAttributionTag());
+ mContext.getAttributionTag());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
} catch (ServiceSpecificException e) {
@@ -5276,7 +5341,7 @@
try {
mService.pendingListenForNetwork(
request.networkCapabilities, operation, mContext.getOpPackageName(),
- getAttributionTag());
+ mContext.getAttributionTag());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
} catch (ServiceSpecificException e) {
diff --git a/framework/src/android/net/ICaptivePortal.aidl b/framework/src/android/net/ICaptivePortal.aidl
index e35f8d4..5cbb428 100644
--- a/framework/src/android/net/ICaptivePortal.aidl
+++ b/framework/src/android/net/ICaptivePortal.aidl
@@ -16,6 +16,9 @@
package android.net;
+import android.net.IIntResultListener;
+import android.os.IBinder;
+
/**
* Interface to inform NetworkMonitor of decisions of app handling captive portal.
* @hide
@@ -23,4 +26,5 @@
oneway interface ICaptivePortal {
void appRequest(int request);
void appResponse(int response);
+ void setDelegateUid(int uid, IBinder binder, IIntResultListener listener);
}
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 574ab2f..cefa1ea 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -31,12 +31,14 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.Process;
import android.os.RemoteException;
import android.telephony.data.EpsBearerQosSessionAttributes;
import android.telephony.data.NrQosSessionAttributes;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.FrameworkConnectivityStatsLog;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -943,6 +945,19 @@
private void queueOrSendMessage(@NonNull RegistryAction action) {
synchronized (mPreConnectedQueue) {
+ if (mNetwork == null && !Process.isApplicationUid(Process.myUid())) {
+ // Theoretically, it should not be valid to queue messages here before
+ // registering the NetworkAgent. However, practically, with the way
+ // queueing works right now, it ends up working out just fine.
+ // Log a statistic so that we know if this is happening in the
+ // wild. The check for isApplicationUid is to prevent logging the
+ // metric from test code.
+
+ FrameworkConnectivityStatsLog.write(
+ FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_MESSAGE_QUEUED_BEFORE_CONNECT
+ );
+ }
if (mRegistry != null) {
try {
action.execute(mRegistry);
diff --git a/framework/src/android/net/NetworkAgentConfig.java b/framework/src/android/net/NetworkAgentConfig.java
index da12a0a..deaa734 100644
--- a/framework/src/android/net/NetworkAgentConfig.java
+++ b/framework/src/android/net/NetworkAgentConfig.java
@@ -272,6 +272,27 @@
return mVpnRequiresValidation;
}
+ /**
+ * Whether the native network creation should be skipped.
+ *
+ * If set, the native network and routes should be maintained by the caller.
+ *
+ * @hide
+ */
+ private boolean mSkipNativeNetworkCreation = false;
+
+
+ /**
+ * @return Whether the native network creation should be skipped.
+ * @hide
+ */
+ // TODO: Expose API when ready.
+ // @FlaggedApi(Flags.FLAG_TETHERING_NETWORK_AGENT)
+ // @SystemApi(client = MODULE_LIBRARIES) when ready.
+ public boolean shouldSkipNativeNetworkCreation() {
+ return mSkipNativeNetworkCreation;
+ }
+
/** @hide */
public NetworkAgentConfig() {
}
@@ -293,6 +314,7 @@
mLegacyExtraInfo = nac.mLegacyExtraInfo;
excludeLocalRouteVpn = nac.excludeLocalRouteVpn;
mVpnRequiresValidation = nac.mVpnRequiresValidation;
+ mSkipNativeNetworkCreation = nac.mSkipNativeNetworkCreation;
}
}
@@ -484,6 +506,26 @@
}
/**
+ * Sets the native network creation should be skipped.
+ *
+ * @return this builder, to facilitate chaining.
+ * @hide
+ */
+ @NonNull
+ // TODO: Expose API when ready.
+ // @FlaggedApi(Flags.FLAG_TETHERING_NETWORK_AGENT)
+ // @SystemApi(client = MODULE_LIBRARIES) when ready.
+ public Builder setSkipNativeNetworkCreation(boolean skipNativeNetworkCreation) {
+ if (!SdkLevel.isAtLeastV()) {
+ // Local agents are supported starting on U on TVs and on V on everything else.
+ // Thus, only support this flag on V+.
+ throw new UnsupportedOperationException("Method is not supported");
+ }
+ mConfig.mSkipNativeNetworkCreation = skipNativeNetworkCreation;
+ return this;
+ }
+
+ /**
* Returns the constructed {@link NetworkAgentConfig} object.
*/
@NonNull
@@ -510,7 +552,8 @@
&& Objects.equals(legacySubTypeName, that.legacySubTypeName)
&& Objects.equals(mLegacyExtraInfo, that.mLegacyExtraInfo)
&& excludeLocalRouteVpn == that.excludeLocalRouteVpn
- && mVpnRequiresValidation == that.mVpnRequiresValidation;
+ && mVpnRequiresValidation == that.mVpnRequiresValidation
+ && mSkipNativeNetworkCreation == that.mSkipNativeNetworkCreation;
}
@Override
@@ -518,7 +561,8 @@
return Objects.hash(allowBypass, explicitlySelected, acceptUnvalidated,
acceptPartialConnectivity, provisioningNotificationDisabled, subscriberId,
skip464xlat, legacyType, legacySubType, legacyTypeName, legacySubTypeName,
- mLegacyExtraInfo, excludeLocalRouteVpn, mVpnRequiresValidation);
+ mLegacyExtraInfo, excludeLocalRouteVpn, mVpnRequiresValidation,
+ mSkipNativeNetworkCreation);
}
@Override
@@ -539,6 +583,7 @@
+ ", legacyExtraInfo = '" + mLegacyExtraInfo + '\''
+ ", excludeLocalRouteVpn = '" + excludeLocalRouteVpn + '\''
+ ", vpnRequiresValidation = '" + mVpnRequiresValidation + '\''
+ + ", skipNativeNetworkCreation = '" + mSkipNativeNetworkCreation + '\''
+ "}";
}
@@ -563,33 +608,35 @@
out.writeString(mLegacyExtraInfo);
out.writeInt(excludeLocalRouteVpn ? 1 : 0);
out.writeInt(mVpnRequiresValidation ? 1 : 0);
+ out.writeInt(mSkipNativeNetworkCreation ? 1 : 0);
}
public static final @NonNull Creator<NetworkAgentConfig> CREATOR =
new Creator<NetworkAgentConfig>() {
- @Override
- public NetworkAgentConfig createFromParcel(Parcel in) {
- NetworkAgentConfig networkAgentConfig = new NetworkAgentConfig();
- networkAgentConfig.allowBypass = in.readInt() != 0;
- networkAgentConfig.explicitlySelected = in.readInt() != 0;
- networkAgentConfig.acceptUnvalidated = in.readInt() != 0;
- networkAgentConfig.acceptPartialConnectivity = in.readInt() != 0;
- networkAgentConfig.subscriberId = in.readString();
- networkAgentConfig.provisioningNotificationDisabled = in.readInt() != 0;
- networkAgentConfig.skip464xlat = in.readInt() != 0;
- networkAgentConfig.legacyType = in.readInt();
- networkAgentConfig.legacyTypeName = in.readString();
- networkAgentConfig.legacySubType = in.readInt();
- networkAgentConfig.legacySubTypeName = in.readString();
- networkAgentConfig.mLegacyExtraInfo = in.readString();
- networkAgentConfig.excludeLocalRouteVpn = in.readInt() != 0;
- networkAgentConfig.mVpnRequiresValidation = in.readInt() != 0;
- return networkAgentConfig;
- }
+ @Override
+ public NetworkAgentConfig createFromParcel(Parcel in) {
+ NetworkAgentConfig networkAgentConfig = new NetworkAgentConfig();
+ networkAgentConfig.allowBypass = in.readInt() != 0;
+ networkAgentConfig.explicitlySelected = in.readInt() != 0;
+ networkAgentConfig.acceptUnvalidated = in.readInt() != 0;
+ networkAgentConfig.acceptPartialConnectivity = in.readInt() != 0;
+ networkAgentConfig.subscriberId = in.readString();
+ networkAgentConfig.provisioningNotificationDisabled = in.readInt() != 0;
+ networkAgentConfig.skip464xlat = in.readInt() != 0;
+ networkAgentConfig.legacyType = in.readInt();
+ networkAgentConfig.legacyTypeName = in.readString();
+ networkAgentConfig.legacySubType = in.readInt();
+ networkAgentConfig.legacySubTypeName = in.readString();
+ networkAgentConfig.mLegacyExtraInfo = in.readString();
+ networkAgentConfig.excludeLocalRouteVpn = in.readInt() != 0;
+ networkAgentConfig.mVpnRequiresValidation = in.readInt() != 0;
+ networkAgentConfig.mSkipNativeNetworkCreation = in.readInt() != 0;
+ return networkAgentConfig;
+ }
- @Override
- public NetworkAgentConfig[] newArray(int size) {
- return new NetworkAgentConfig[size];
- }
- };
+ @Override
+ public NetworkAgentConfig[] newArray(int size) {
+ return new NetworkAgentConfig[size];
+ }
+ };
}
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 4a50397..c6b62ee 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -359,6 +359,7 @@
mSubIds = new ArraySet<>();
mUnderlyingNetworks = null;
mEnterpriseId = 0;
+ mReservationId = RES_ID_UNSET;
}
/**
@@ -393,6 +394,7 @@
// necessary.
mUnderlyingNetworks = nc.mUnderlyingNetworks;
mEnterpriseId = nc.mEnterpriseId;
+ mReservationId = nc.mReservationId;
}
/**
@@ -2233,7 +2235,8 @@
&& (onlyImmutable || satisfiedByUids(nc))
&& (onlyImmutable || satisfiedBySSID(nc))
&& (onlyImmutable || satisfiedByRequestor(nc))
- && (onlyImmutable || satisfiedBySubscriptionIds(nc)));
+ && (onlyImmutable || satisfiedBySubscriptionIds(nc)))
+ && satisfiedByReservationId(nc);
}
/**
@@ -2347,7 +2350,8 @@
&& equalsAdministratorUids(that)
&& equalsSubscriptionIds(that)
&& equalsUnderlyingNetworks(that)
- && equalsEnterpriseCapabilitiesId(that);
+ && equalsEnterpriseCapabilitiesId(that)
+ && equalsReservationId(that);
}
@Override
@@ -2373,7 +2377,9 @@
+ Arrays.hashCode(mAdministratorUids) * 67
+ Objects.hashCode(mSubIds) * 71
+ Objects.hashCode(mUnderlyingNetworks) * 73
- + mEnterpriseId * 79;
+ + mEnterpriseId * 79
+ + mReservationId * 83;
+
}
@Override
@@ -2411,6 +2417,7 @@
dest.writeIntArray(CollectionUtils.toIntArray(mSubIds));
dest.writeTypedList(mUnderlyingNetworks);
dest.writeInt(mEnterpriseId & ALL_VALID_ENTERPRISE_IDS);
+ dest.writeInt(mReservationId);
}
public static final @android.annotation.NonNull Creator<NetworkCapabilities> CREATOR =
@@ -2446,6 +2453,7 @@
}
netCap.setUnderlyingNetworks(in.createTypedArrayList(Network.CREATOR));
netCap.mEnterpriseId = in.readInt() & ALL_VALID_ENTERPRISE_IDS;
+ netCap.mReservationId = in.readInt();
return netCap;
}
@Override
@@ -2548,6 +2556,11 @@
NetworkCapabilities::enterpriseIdNameOf, "&");
}
+ if (mReservationId != RES_ID_UNSET) {
+ final boolean isReservationOffer = (mReservationId == RES_ID_MATCH_ALL_RESERVATIONS);
+ sb.append(" ReservationId: ").append(isReservationOffer ? "*" : mReservationId);
+ }
+
sb.append(" UnderlyingNetworks: ");
if (mUnderlyingNetworks != null) {
sb.append("[");
@@ -2876,6 +2889,65 @@
}
/**
+ * The reservation ID used by non-reservable Networks and "regular" NetworkOffers.
+ *
+ * Note that {@code NetworkRequest#FIRST_REQUEST_ID} is 1;
+ * @hide
+ */
+ public static final int RES_ID_UNSET = 0;
+
+ /**
+ * The reservation ID used by special NetworkOffers that handle RESERVATION requests.
+ *
+ * NetworkOffers with {@code RES_ID_MATCH_ALL_RESERVATIONS} *only* receive onNetworkNeeded()
+ * callbacks for {@code NetworkRequest.Type.RESERVATION}.
+ * @hide
+ */
+ public static final int RES_ID_MATCH_ALL_RESERVATIONS = -1;
+
+ /**
+ * Unique ID that identifies the network reservation.
+ */
+ private int mReservationId;
+
+ /**
+ * Returns the reservation ID
+ * @hide
+ */
+ public int getReservationId() {
+ return mReservationId;
+ }
+
+ /**
+ * Set the reservation ID
+ * @hide
+ */
+ public void setReservationId(int resId) {
+ mReservationId = resId;
+ }
+
+ private boolean equalsReservationId(@NonNull NetworkCapabilities nc) {
+ return mReservationId == nc.mReservationId;
+ }
+
+ private boolean satisfiedByReservationId(@NonNull NetworkCapabilities nc) {
+ if (mReservationId == RES_ID_UNSET) {
+ // To maintain regular NetworkRequest semantics, a request with a zero reservationId
+ // matches an offer or network with any reservationId except MATCH_ALL_RESERVATIONS.
+ return nc.mReservationId != RES_ID_MATCH_ALL_RESERVATIONS;
+ }
+ // A request with a non-zero reservationId matches only an offer or network with that exact
+ // reservationId (required to match the network that will eventually come up) or
+ // MATCH_ALL_RESERVATIONS (required to match the blanket reservation offer).
+ if (nc.mReservationId == RES_ID_MATCH_ALL_RESERVATIONS) {
+ return true;
+ }
+ return mReservationId == nc.mReservationId;
+ }
+
+
+
+ /**
* Returns a bitmask of all the applicable redactions (based on the permissions held by the
* receiving app) to be performed on this object.
*
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 89572b3..5a08d44 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -32,6 +32,7 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+import static android.net.NetworkCapabilities.RES_ID_UNSET;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import android.annotation.FlaggedApi;
@@ -193,6 +194,16 @@
* callbacks about the single, highest scoring current network
* (if any) that matches the specified NetworkCapabilities, or
*
+ * - RESERVATION requests behave identically to those of type REQUEST.
+ * For example, unlike LISTEN, they cause networks to remain
+ * connected, and they match exactly one network (the best one).
+ * A RESERVATION generates a unique reservationId in its
+ * NetworkCapabilities by copying the requestId which affects
+ * matching. A NetworkProvider can register a "blanket" NetworkOffer
+ * with reservationId = MATCH_ALL_RESERVATIONS to indicate that it
+ * is capable of generating NetworkOffers in response to RESERVATION
+ * requests.
+ *
* - TRACK_DEFAULT, which causes the framework to issue callbacks for
* the single, highest scoring current network (if any) that will
* be chosen for an app, but which cannot cause the framework to
@@ -229,6 +240,7 @@
BACKGROUND_REQUEST,
TRACK_SYSTEM_DEFAULT,
LISTEN_FOR_BEST,
+ RESERVATION,
};
/**
@@ -245,8 +257,17 @@
if (nc == null) {
throw new NullPointerException();
}
+ if (nc.getReservationId() != RES_ID_UNSET) {
+ throw new IllegalArgumentException("ReservationId must only be set by the system");
+ }
requestId = rId;
networkCapabilities = nc;
+ if (type == Type.RESERVATION) {
+ // Conceptually, the reservationId is not related to the requestId; however, the
+ // requestId fulfills the same uniqueness requirements that are needed for the
+ // reservationId, so it can be reused for this purpose.
+ networkCapabilities.setReservationId(rId);
+ }
this.legacyType = legacyType;
this.type = type;
}
@@ -261,6 +282,13 @@
this.type = that.type;
}
+ private NetworkRequest(Parcel in) {
+ networkCapabilities = NetworkCapabilities.CREATOR.createFromParcel(in);
+ legacyType = in.readInt();
+ requestId = in.readInt();
+ type = Type.valueOf(in.readString()); // IllegalArgumentException if invalid.
+ }
+
/**
* Builder used to create {@link NetworkRequest} objects. Specify the Network features
* needed in terms of {@link NetworkCapabilities} features
@@ -657,12 +685,7 @@
public static final @android.annotation.NonNull Creator<NetworkRequest> CREATOR =
new Creator<NetworkRequest>() {
public NetworkRequest createFromParcel(Parcel in) {
- NetworkCapabilities nc = NetworkCapabilities.CREATOR.createFromParcel(in);
- int legacyType = in.readInt();
- int requestId = in.readInt();
- Type type = Type.valueOf(in.readString()); // IllegalArgumentException if invalid.
- NetworkRequest result = new NetworkRequest(nc, legacyType, requestId, type);
- return result;
+ return new NetworkRequest(in);
}
public NetworkRequest[] newArray(int size) {
return new NetworkRequest[size];
@@ -703,7 +726,7 @@
* @hide
*/
public boolean isRequest() {
- return type == Type.REQUEST || type == Type.BACKGROUND_REQUEST;
+ return type == Type.REQUEST || type == Type.BACKGROUND_REQUEST || type == Type.RESERVATION;
}
/**
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index 51df8ab..0536263 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -135,6 +135,17 @@
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public static final long ENABLE_TRAFFICSTATS_RATE_LIMIT_CACHE = 74210811L;
+ /**
+ * Restrict local network access.
+ *
+ * Apps targeting a release after V will require permissions to access the local network.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public static final long RESTRICT_LOCAL_NETWORK = 365139289L;
+
private ConnectivityCompatChanges() {
}
}
diff --git a/nearby/tests/cts/fastpair/AndroidManifest.xml b/nearby/tests/cts/fastpair/AndroidManifest.xml
index 472f4f0..9e1ec70 100644
--- a/nearby/tests/cts/fastpair/AndroidManifest.xml
+++ b/nearby/tests/cts/fastpair/AndroidManifest.xml
@@ -21,6 +21,7 @@
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<application>
<uses-library android:name="android.test.runner"/>
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index 3291223..58d1808 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -26,7 +26,6 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
-import static org.junit.Assume.assumeTrue;
import android.app.UiAutomation;
import android.bluetooth.test_utils.EnableBluetoothRule;
@@ -79,10 +78,10 @@
@ClassRule public static final EnableBluetoothRule sEnableBluetooth = new EnableBluetoothRule();
- private static final byte[] SALT = new byte[]{1, 2};
- private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] SALT = new byte[] {1, 2};
+ private static final byte[] SECRET_ID = new byte[] {1, 2, 3, 4};
private static final byte[] META_DATA_ENCRYPTION_KEY = new byte[14];
- private static final byte[] AUTHENTICITY_KEY = new byte[]{0, 1, 1, 1};
+ private static final byte[] AUTHENTICITY_KEY = new byte[] {0, 1, 1, 1};
private static final String DEVICE_NAME = "test_device";
private static final int BLE_MEDIUM = 1;
@@ -91,43 +90,45 @@
private UiAutomation mUiAutomation =
InstrumentationRegistry.getInstrumentation().getUiAutomation();
- private ScanRequest mScanRequest = new ScanRequest.Builder()
- .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR)
- .setScanMode(ScanRequest.SCAN_MODE_LOW_LATENCY)
- .setBleEnabled(true)
- .build();
+ private ScanRequest mScanRequest =
+ new ScanRequest.Builder()
+ .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR)
+ .setScanMode(ScanRequest.SCAN_MODE_LOW_LATENCY)
+ .setBleEnabled(true)
+ .build();
private PresenceDevice.Builder mBuilder =
new PresenceDevice.Builder("deviceId", SALT, SECRET_ID, META_DATA_ENCRYPTION_KEY);
- private ScanCallback mScanCallback = new ScanCallback() {
- @Override
- public void onDiscovered(@NonNull NearbyDevice device) {
- }
+ private ScanCallback mScanCallback =
+ new ScanCallback() {
+ @Override
+ public void onDiscovered(@NonNull NearbyDevice device) {}
- @Override
- public void onUpdated(@NonNull NearbyDevice device) {
- }
+ @Override
+ public void onUpdated(@NonNull NearbyDevice device) {}
- @Override
- public void onLost(@NonNull NearbyDevice device) {
- }
+ @Override
+ public void onLost(@NonNull NearbyDevice device) {}
- @Override
- public void onError(int errorCode) {
- }
- };
+ @Override
+ public void onError(int errorCode) {}
+ };
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
@Before
public void setUp() {
- mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG,
- WRITE_ALLOWLISTED_DEVICE_CONFIG, BLUETOOTH_PRIVILEGED);
- String nameSpace = SdkLevel.isAtLeastU() ? DeviceConfig.NAMESPACE_NEARBY
- : DeviceConfig.NAMESPACE_TETHERING;
- DeviceConfig.setProperty(nameSpace,
- "nearby_enable_presence_broadcast_legacy",
- "true", false);
+ mUiAutomation.adoptShellPermissionIdentity(
+ READ_DEVICE_CONFIG,
+ WRITE_DEVICE_CONFIG,
+ WRITE_ALLOWLISTED_DEVICE_CONFIG,
+ BLUETOOTH_PRIVILEGED);
+ String nameSpace =
+ SdkLevel.isAtLeastU()
+ ? DeviceConfig.NAMESPACE_NEARBY
+ : DeviceConfig.NAMESPACE_TETHERING;
+ DeviceConfig.setProperty(
+ nameSpace, "nearby_enable_presence_broadcast_legacy", "true", false);
mContext = InstrumentationRegistry.getContext();
mNearbyManager = mContext.getSystemService(NearbyManager.class);
@@ -144,8 +145,9 @@
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void test_startScan_noPrivilegedPermission() {
mUiAutomation.dropShellPermissionIdentity();
- assertThrows(SecurityException.class, () -> mNearbyManager
- .startScan(mScanRequest, EXECUTOR, mScanCallback));
+ assertThrows(
+ SecurityException.class,
+ () -> mNearbyManager.startScan(mScanRequest, EXECUTOR, mScanCallback));
}
@Test
@@ -159,23 +161,25 @@
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testStartStopBroadcast() throws InterruptedException {
- PrivateCredential credential = new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY,
- META_DATA_ENCRYPTION_KEY, DEVICE_NAME)
- .setIdentityType(IDENTITY_TYPE_PRIVATE)
- .build();
+ PrivateCredential credential =
+ new PrivateCredential.Builder(
+ SECRET_ID, AUTHENTICITY_KEY, META_DATA_ENCRYPTION_KEY, DEVICE_NAME)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
BroadcastRequest broadcastRequest =
new PresenceBroadcastRequest.Builder(
- Collections.singletonList(BLE_MEDIUM), SALT, credential)
+ Collections.singletonList(BLE_MEDIUM), SALT, credential)
.addAction(123)
.build();
CountDownLatch latch = new CountDownLatch(1);
- BroadcastCallback callback = status -> {
- latch.countDown();
- assertThat(status).isEqualTo(BroadcastCallback.STATUS_OK);
- };
- mNearbyManager.startBroadcast(broadcastRequest, Executors.newSingleThreadExecutor(),
- callback);
+ BroadcastCallback callback =
+ status -> {
+ latch.countDown();
+ assertThat(status).isEqualTo(BroadcastCallback.STATUS_OK);
+ };
+ mNearbyManager.startBroadcast(
+ broadcastRequest, Executors.newSingleThreadExecutor(), callback);
latch.await(10, TimeUnit.SECONDS);
mNearbyManager.stopBroadcast(callback);
}
@@ -197,9 +201,8 @@
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public void testsetPoweredOffFindingEphemeralIds() {
- // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
- assumeTrue(SdkLevel.isAtLeastV());
// Only test supporting devices.
if (mNearbyManager.getPoweredOffFindingMode()
== NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
@@ -208,24 +211,22 @@
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public void testsetPoweredOffFindingEphemeralIds_noPrivilegedPermission() {
- // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
- assumeTrue(SdkLevel.isAtLeastV());
// Only test supporting devices.
if (mNearbyManager.getPoweredOffFindingMode()
== NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
mUiAutomation.dropShellPermissionIdentity();
- assertThrows(SecurityException.class,
+ assertThrows(
+ SecurityException.class,
() -> mNearbyManager.setPoweredOffFindingEphemeralIds(List.of(new byte[20])));
}
-
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public void testSetAndGetPoweredOffFindingMode_enabled() {
- // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
- assumeTrue(SdkLevel.isAtLeastV());
// Only test supporting devices.
if (mNearbyManager.getPoweredOffFindingMode()
== NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
@@ -234,30 +235,26 @@
// enableLocation() has dropped shell permission identity.
mUiAutomation.adoptShellPermissionIdentity(BLUETOOTH_PRIVILEGED);
- mNearbyManager.setPoweredOffFindingMode(
- NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
+ mNearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
assertThat(mNearbyManager.getPoweredOffFindingMode())
.isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public void testSetAndGetPoweredOffFindingMode_disabled() {
- // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
- assumeTrue(SdkLevel.isAtLeastV());
// Only test supporting devices.
if (mNearbyManager.getPoweredOffFindingMode()
== NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
- mNearbyManager.setPoweredOffFindingMode(
- NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
+ mNearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
assertThat(mNearbyManager.getPoweredOffFindingMode())
.isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public void testSetPoweredOffFindingMode_noPrivilegedPermission() {
- // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
- assumeTrue(SdkLevel.isAtLeastV());
// Only test supporting devices.
if (mNearbyManager.getPoweredOffFindingMode()
== NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
@@ -265,14 +262,16 @@
enableLocation();
mUiAutomation.dropShellPermissionIdentity();
- assertThrows(SecurityException.class, () -> mNearbyManager
- .setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED));
+ assertThrows(
+ SecurityException.class,
+ () ->
+ mNearbyManager.setPoweredOffFindingMode(
+ NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED));
}
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public void testGetPoweredOffFindingMode_noPrivilegedPermission() {
- // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
- assumeTrue(SdkLevel.isAtLeastV());
// Only test supporting devices.
if (mNearbyManager.getPoweredOffFindingMode()
== NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
index 32286e1..a36084b 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
@@ -18,7 +18,6 @@
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyInt;
@@ -29,12 +28,13 @@
import android.hardware.bluetooth.finder.Eid;
import android.hardware.bluetooth.finder.IBluetoothFinder;
import android.nearby.PoweredOffFindingEphemeralId;
+import android.os.Build;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
-import com.android.modules.utils.build.SdkLevel;
+import androidx.test.filters.SdkSuppress;
import org.junit.Before;
import org.junit.Test;
@@ -44,6 +44,7 @@
import java.util.List;
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public class BluetoothFinderManagerTest {
private BluetoothFinderManager mBluetoothFinderManager;
private boolean mGetServiceCalled = false;
@@ -71,8 +72,6 @@
@Before
public void setup() {
- // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
- assumeTrue(SdkLevel.isAtLeastV());
MockitoAnnotations.initMocks(this);
mBluetoothFinderManager = new BluetoothFinderManagerSpy();
}
@@ -80,16 +79,16 @@
@Test
public void testSendEids() throws Exception {
byte[] eidBytes1 = {
- (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
- (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
- (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
- (byte) 0xe1, (byte) 0xde
+ (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+ (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+ (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+ (byte) 0xe1, (byte) 0xde
};
byte[] eidBytes2 = {
- (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
- (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
- (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
- (byte) 0xf2, (byte) 0xef
+ (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+ (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+ (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+ (byte) 0xf2, (byte) 0xef
};
PoweredOffFindingEphemeralId ephemeralId1 = new PoweredOffFindingEphemeralId();
PoweredOffFindingEphemeralId ephemeralId2 = new PoweredOffFindingEphemeralId();
@@ -105,8 +104,7 @@
@Test
public void testSendEids_remoteException() throws Exception {
- doThrow(new RemoteException())
- .when(mIBluetoothFinderMock).sendEids(any());
+ doThrow(new RemoteException()).when(mIBluetoothFinderMock).sendEids(any());
mBluetoothFinderManager.sendEids(List.of());
// Verify that we get the service again following a RemoteException.
@@ -117,8 +115,7 @@
@Test
public void testSendEids_serviceSpecificException() throws Exception {
- doThrow(new ServiceSpecificException(1))
- .when(mIBluetoothFinderMock).sendEids(any());
+ doThrow(new ServiceSpecificException(1)).when(mIBluetoothFinderMock).sendEids(any());
mBluetoothFinderManager.sendEids(List.of());
}
@@ -134,7 +131,8 @@
@Test
public void testSetPoweredOffFinderMode_remoteException() throws Exception {
doThrow(new RemoteException())
- .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+ .when(mIBluetoothFinderMock)
+ .setPoweredOffFinderMode(anyBoolean());
mBluetoothFinderManager.setPoweredOffFinderMode(true);
// Verify that we get the service again following a RemoteException.
@@ -146,7 +144,8 @@
@Test
public void testSetPoweredOffFinderMode_serviceSpecificException() throws Exception {
doThrow(new ServiceSpecificException(1))
- .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+ .when(mIBluetoothFinderMock)
+ .setPoweredOffFinderMode(anyBoolean());
mBluetoothFinderManager.setPoweredOffFinderMode(true);
}
diff --git a/networksecurity/service/Android.bp b/networksecurity/service/Android.bp
index a41e6a0..d7aacdb 100644
--- a/networksecurity/service/Android.bp
+++ b/networksecurity/service/Android.bp
@@ -24,16 +24,19 @@
srcs: [
"src/**/*.java",
+ ":statslog-certificate-transparency-java-gen",
],
libs: [
"framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
"service-connectivity-pre-jarjar",
+ "framework-statsd.stubs.module_lib",
],
static_libs: [
"auto_value_annotations",
+ "android.security.flags-aconfig-java-export",
],
plugins: [
@@ -48,3 +51,10 @@
sdk_version: "system_server_current",
apex_available: ["com.android.tethering"],
}
+
+genrule {
+ name: "statslog-certificate-transparency-java-gen",
+ tools: ["stats-log-api-gen"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module certificate_transparency --javaPackage com.android.server.net.ct --javaClass CertificateTransparencyStatsLog",
+ out: ["com/android/server/net/ct/CertificateTransparencyStatsLog.java"],
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index d53f007..c9694d1 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -13,13 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.android.server.net.ct;
-import android.annotation.NonNull;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DEVICE_OFFLINE;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DOWNLOAD_CANNOT_RESUME;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_TOO_MANY_REDIRECTS;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_UNKNOWN;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__PENDING_WAITING_FOR_WIFI;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
import android.annotation.RequiresApi;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -31,16 +43,13 @@
import com.android.server.net.ct.DownloadHelper.DownloadStatus;
+import org.json.JSONException;
+import org.json.JSONObject;
+
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
-import java.security.KeyFactory;
-import java.security.PublicKey;
-import java.security.Signature;
-import java.security.spec.X509EncodedKeySpec;
-import java.util.Base64;
-import java.util.Optional;
/** Helper class to download certificate transparency log files. */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@@ -51,79 +60,81 @@
private final Context mContext;
private final DataStore mDataStore;
private final DownloadHelper mDownloadHelper;
+ private final SignatureVerifier mSignatureVerifier;
private final CertificateTransparencyInstaller mInstaller;
+ private final CertificateTransparencyLogger mLogger;
- @NonNull private Optional<PublicKey> mPublicKey = Optional.empty();
+ private boolean started = false;
- @VisibleForTesting
CertificateTransparencyDownloader(
Context context,
DataStore dataStore,
DownloadHelper downloadHelper,
- CertificateTransparencyInstaller installer) {
+ SignatureVerifier signatureVerifier,
+ CertificateTransparencyInstaller installer,
+ CertificateTransparencyLogger logger) {
mContext = context;
+ mSignatureVerifier = signatureVerifier;
mDataStore = dataStore;
mDownloadHelper = downloadHelper;
mInstaller = installer;
+ mLogger = logger;
}
- CertificateTransparencyDownloader(Context context, DataStore dataStore) {
- this(
- context,
- dataStore,
- new DownloadHelper(context),
- new CertificateTransparencyInstaller());
- }
-
- void initialize() {
+ void start() {
+ if (started) {
+ return;
+ }
mInstaller.addCompatibilityVersion(Config.COMPATIBILITY_VERSION);
-
- IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
- mContext.registerReceiver(this, intentFilter, Context.RECEIVER_EXPORTED);
+ mContext.registerReceiver(
+ this,
+ new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
+ Context.RECEIVER_EXPORTED);
+ started = true;
if (Config.DEBUG) {
- Log.d(TAG, "CertificateTransparencyDownloader initialized successfully");
+ Log.d(TAG, "CertificateTransparencyDownloader started.");
}
}
- void setPublicKey(String publicKey) throws GeneralSecurityException {
- try {
- mPublicKey =
- Optional.of(
- KeyFactory.getInstance("RSA")
- .generatePublic(
- new X509EncodedKeySpec(
- Base64.getDecoder().decode(publicKey))));
- } catch (IllegalArgumentException e) {
- Log.e(TAG, "Invalid public key Base64 encoding", e);
- mPublicKey = Optional.empty();
- }
- }
-
- @VisibleForTesting
- void resetPublicKey() {
- mPublicKey = Optional.empty();
- }
-
- void startMetadataDownload(String metadataUrl) {
- long downloadId = download(metadataUrl);
- if (downloadId == -1) {
- Log.e(TAG, "Metadata download request failed for " + metadataUrl);
+ void stop() {
+ if (!started) {
return;
}
- mDataStore.setPropertyLong(Config.METADATA_URL_KEY, downloadId);
- mDataStore.store();
+ mContext.unregisterReceiver(this);
+ mInstaller.removeCompatibilityVersion(Config.COMPATIBILITY_VERSION);
+ started = false;
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyDownloader stopped.");
+ }
}
- void startContentDownload(String contentUrl) {
- long downloadId = download(contentUrl);
- if (downloadId == -1) {
- Log.e(TAG, "Content download request failed for " + contentUrl);
- return;
+ long startPublicKeyDownload() {
+ long downloadId = download(mDataStore.getProperty(Config.PUBLIC_KEY_URL));
+ if (downloadId != -1) {
+ mDataStore.setPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, downloadId);
+ mDataStore.store();
}
- mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, downloadId);
- mDataStore.store();
+ return downloadId;
+ }
+
+ long startMetadataDownload() {
+ long downloadId = download(mDataStore.getProperty(Config.METADATA_URL));
+ if (downloadId != -1) {
+ mDataStore.setPropertyLong(Config.METADATA_DOWNLOAD_ID, downloadId);
+ mDataStore.store();
+ }
+ return downloadId;
+ }
+
+ long startContentDownload() {
+ long downloadId = download(mDataStore.getProperty(Config.CONTENT_URL));
+ if (downloadId != -1) {
+ mDataStore.setPropertyLong(Config.CONTENT_DOWNLOAD_ID, downloadId);
+ mDataStore.store();
+ }
+ return downloadId;
}
@Override
@@ -140,6 +151,11 @@
return;
}
+ if (isPublicKeyDownloadId(completedId)) {
+ handlePublicKeyDownloadCompleted(completedId);
+ return;
+ }
+
if (isMetadataDownloadId(completedId)) {
handleMetadataDownloadCompleted(completedId);
return;
@@ -150,7 +166,34 @@
return;
}
- Log.e(TAG, "Download id " + completedId + " is neither metadata nor content.");
+ Log.i(TAG, "Download id " + completedId + " is not recognized.");
+ }
+
+ private void handlePublicKeyDownloadCompleted(long downloadId) {
+ DownloadStatus status = mDownloadHelper.getDownloadStatus(downloadId);
+ if (!status.isSuccessful()) {
+ handleDownloadFailed(status);
+ return;
+ }
+
+ Uri publicKeyUri = getPublicKeyDownloadUri();
+ if (publicKeyUri == null) {
+ Log.e(TAG, "Invalid public key URI");
+ return;
+ }
+
+ try {
+ mSignatureVerifier.setPublicKeyFrom(publicKeyUri);
+ } catch (GeneralSecurityException | IOException | IllegalArgumentException e) {
+ Log.e(TAG, "Error setting the public Key", e);
+ return;
+ }
+
+ if (startMetadataDownload() == -1) {
+ Log.e(TAG, "Metadata download not started.");
+ } else if (Config.DEBUG) {
+ Log.d(TAG, "Metadata download started successfully.");
+ }
}
private void handleMetadataDownloadCompleted(long downloadId) {
@@ -159,7 +202,11 @@
handleDownloadFailed(status);
return;
}
- startContentDownload(mDataStore.getProperty(Config.CONTENT_URL_PENDING));
+ if (startContentDownload() == -1) {
+ Log.e(TAG, "Content download not started.");
+ } else if (Config.DEBUG) {
+ Log.d(TAG, "Content download started successfully.");
+ }
}
private void handleContentDownloadCompleted(long downloadId) {
@@ -177,21 +224,55 @@
}
boolean success = false;
+ boolean failureLogged = false;
+
try {
- success = verify(contentUri, metadataUri);
+ success = mSignatureVerifier.verify(contentUri, metadataUri);
+ } catch (MissingPublicKeyException e) {
+ if (updateFailureCount()) {
+ failureLogged = true;
+ mLogger.logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND,
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0)
+ );
+ }
+ } catch (InvalidKeyException e) {
+ if (updateFailureCount()) {
+ failureLogged = true;
+ mLogger.logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION,
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0)
+ );
+ }
} catch (IOException | GeneralSecurityException e) {
Log.e(TAG, "Could not verify new log list", e);
}
+
if (!success) {
Log.w(TAG, "Log list did not pass verification");
+
+ // Avoid logging failure twice
+ if (!failureLogged && updateFailureCount()) {
+ mLogger.logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION,
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0));
+ }
return;
}
- // TODO: validate file content.
+ String version = null;
+ try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
+ version =
+ new JSONObject(new String(inputStream.readAllBytes(), UTF_8))
+ .getString("version");
+ } catch (JSONException | IOException e) {
+ Log.e(TAG, "Could not extract version from log list", e);
+ return;
+ }
- String version = mDataStore.getProperty(Config.VERSION_PENDING);
- String contentUrl = mDataStore.getProperty(Config.CONTENT_URL_PENDING);
- String metadataUrl = mDataStore.getProperty(Config.METADATA_URL_PENDING);
try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
success = mInstaller.install(Config.COMPATIBILITY_VERSION, inputStream, version);
} catch (IOException e) {
@@ -202,30 +283,83 @@
if (success) {
// Update information about the stored version on successful install.
mDataStore.setProperty(Config.VERSION, version);
- mDataStore.setProperty(Config.CONTENT_URL, contentUrl);
- mDataStore.setProperty(Config.METADATA_URL, metadataUrl);
+
+ // Reset the number of consecutive log list failure updates back to zero.
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* value= */ 0);
mDataStore.store();
+ } else {
+ if (updateFailureCount()) {
+ mLogger.logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS,
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0));
+ }
}
}
private void handleDownloadFailed(DownloadStatus status) {
- Log.e(TAG, "Content download failed with " + status);
- // TODO(378626065): Report failure via statsd.
+ Log.e(TAG, "Download failed with " + status);
+
+ if (updateFailureCount()) {
+ int failureCount =
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
+
+ // HTTP Error
+ if (400 <= status.reason() && status.reason() <= 600) {
+ mLogger.logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR,
+ failureCount,
+ status.reason());
+ } else {
+ // TODO(b/384935059): handle blocked domain logging
+ mLogger.logCTLogListUpdateFailedEvent(
+ downloadStatusToFailureReason(status.reason()), failureCount);
+ }
+ }
}
- private boolean verify(Uri file, Uri signature) throws IOException, GeneralSecurityException {
- if (!mPublicKey.isPresent()) {
- throw new InvalidKeyException("Missing public key for signature verification");
+ /** Converts DownloadStatus reason into failure reason to log. */
+ private int downloadStatusToFailureReason(int downloadStatusReason) {
+ switch (downloadStatusReason) {
+ case DownloadManager.PAUSED_WAITING_TO_RETRY:
+ case DownloadManager.PAUSED_WAITING_FOR_NETWORK:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DEVICE_OFFLINE;
+ case DownloadManager.ERROR_UNHANDLED_HTTP_CODE:
+ case DownloadManager.ERROR_HTTP_DATA_ERROR:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_HTTP_ERROR;
+ case DownloadManager.ERROR_TOO_MANY_REDIRECTS:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_TOO_MANY_REDIRECTS;
+ case DownloadManager.ERROR_CANNOT_RESUME:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_DOWNLOAD_CANNOT_RESUME;
+ case DownloadManager.ERROR_INSUFFICIENT_SPACE:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE;
+ case DownloadManager.PAUSED_QUEUED_FOR_WIFI:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__PENDING_WAITING_FOR_WIFI;
+ default:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_UNKNOWN;
}
- Signature verifier = Signature.getInstance("SHA256withRSA");
- verifier.initVerify(mPublicKey.get());
- ContentResolver contentResolver = mContext.getContentResolver();
+ }
- try (InputStream fileStream = contentResolver.openInputStream(file);
- InputStream signatureStream = contentResolver.openInputStream(signature)) {
- verifier.update(fileStream.readAllBytes());
- return verifier.verify(signatureStream.readAllBytes());
+ /**
+ * Updates the data store with the current number of consecutive log list update failures.
+ *
+ * @return whether the failure count exceeds the threshold and should be logged.
+ */
+ private boolean updateFailureCount() {
+ int failure_count =
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
+ int new_failure_count = failure_count + 1;
+
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, new_failure_count);
+ mDataStore.store();
+
+ boolean shouldReport = new_failure_count >= Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD;
+ if (shouldReport) {
+ Log.d(TAG, "Log list update failure count exceeds threshold: " + new_failure_count);
}
+ return shouldReport;
}
private long download(String url) {
@@ -238,20 +372,59 @@
}
@VisibleForTesting
+ long getPublicKeyDownloadId() {
+ return mDataStore.getPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, -1);
+ }
+
+ @VisibleForTesting
+ long getMetadataDownloadId() {
+ return mDataStore.getPropertyLong(Config.METADATA_DOWNLOAD_ID, -1);
+ }
+
+ @VisibleForTesting
+ long getContentDownloadId() {
+ return mDataStore.getPropertyLong(Config.CONTENT_DOWNLOAD_ID, -1);
+ }
+
+ @VisibleForTesting
+ boolean hasPublicKeyDownloadId() {
+ return getPublicKeyDownloadId() != -1;
+ }
+
+ @VisibleForTesting
+ boolean hasMetadataDownloadId() {
+ return getMetadataDownloadId() != -1;
+ }
+
+ @VisibleForTesting
+ boolean hasContentDownloadId() {
+ return getContentDownloadId() != -1;
+ }
+
+ @VisibleForTesting
+ boolean isPublicKeyDownloadId(long downloadId) {
+ return getPublicKeyDownloadId() == downloadId;
+ }
+
+ @VisibleForTesting
boolean isMetadataDownloadId(long downloadId) {
- return mDataStore.getPropertyLong(Config.METADATA_URL_KEY, -1) == downloadId;
+ return getMetadataDownloadId() == downloadId;
}
@VisibleForTesting
boolean isContentDownloadId(long downloadId) {
- return mDataStore.getPropertyLong(Config.CONTENT_URL_KEY, -1) == downloadId;
+ return getContentDownloadId() == downloadId;
+ }
+
+ private Uri getPublicKeyDownloadUri() {
+ return mDownloadHelper.getUri(getPublicKeyDownloadId());
}
private Uri getMetadataDownloadUri() {
- return mDownloadHelper.getUri(mDataStore.getPropertyLong(Config.METADATA_URL_KEY, -1));
+ return mDownloadHelper.getUri(getMetadataDownloadId());
}
private Uri getContentDownloadUri() {
- return mDownloadHelper.getUri(mDataStore.getPropertyLong(Config.CONTENT_URL_KEY, -1));
+ return mDownloadHelper.getUri(getContentDownloadId());
}
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
deleted file mode 100644
index 93a7064..0000000
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.server.net.ct;
-
-import android.annotation.RequiresApi;
-import android.os.Build;
-import android.provider.DeviceConfig;
-import android.provider.DeviceConfig.Properties;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.security.GeneralSecurityException;
-import java.util.concurrent.Executors;
-
-/** Listener class for the Certificate Transparency Phenotype flags. */
-@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesChangedListener {
-
- private static final String TAG = "CertificateTransparencyFlagsListener";
-
- private final DataStore mDataStore;
- private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
-
- CertificateTransparencyFlagsListener(
- DataStore dataStore,
- CertificateTransparencyDownloader certificateTransparencyDownloader) {
- mDataStore = dataStore;
- mCertificateTransparencyDownloader = certificateTransparencyDownloader;
- }
-
- void initialize() {
- mDataStore.load();
- mCertificateTransparencyDownloader.initialize();
- DeviceConfig.addOnPropertiesChangedListener(
- Config.NAMESPACE_NETWORK_SECURITY, Executors.newSingleThreadExecutor(), this);
- if (Config.DEBUG) {
- Log.d(TAG, "CertificateTransparencyFlagsListener initialized successfully");
- }
- // TODO: handle property changes triggering on boot before registering this listener.
- }
-
- @Override
- public void onPropertiesChanged(Properties properties) {
- if (!Config.NAMESPACE_NETWORK_SECURITY.equals(properties.getNamespace())) {
- return;
- }
-
- String newPublicKey =
- DeviceConfig.getString(
- Config.NAMESPACE_NETWORK_SECURITY,
- Config.FLAG_PUBLIC_KEY,
- /* defaultValue= */ "");
- String newVersion =
- DeviceConfig.getString(
- Config.NAMESPACE_NETWORK_SECURITY,
- Config.FLAG_VERSION,
- /* defaultValue= */ "");
- String newContentUrl =
- DeviceConfig.getString(
- Config.NAMESPACE_NETWORK_SECURITY,
- Config.FLAG_CONTENT_URL,
- /* defaultValue= */ "");
- String newMetadataUrl =
- DeviceConfig.getString(
- Config.NAMESPACE_NETWORK_SECURITY,
- Config.FLAG_METADATA_URL,
- /* defaultValue= */ "");
- if (TextUtils.isEmpty(newPublicKey)
- || TextUtils.isEmpty(newVersion)
- || TextUtils.isEmpty(newContentUrl)
- || TextUtils.isEmpty(newMetadataUrl)) {
- return;
- }
-
- if (Config.DEBUG) {
- Log.d(TAG, "newPublicKey=" + newPublicKey);
- Log.d(TAG, "newVersion=" + newVersion);
- Log.d(TAG, "newContentUrl=" + newContentUrl);
- Log.d(TAG, "newMetadataUrl=" + newMetadataUrl);
- }
-
- String oldVersion = mDataStore.getProperty(Config.VERSION);
- String oldContentUrl = mDataStore.getProperty(Config.CONTENT_URL);
- String oldMetadataUrl = mDataStore.getProperty(Config.METADATA_URL);
-
- if (TextUtils.equals(newVersion, oldVersion)
- && TextUtils.equals(newContentUrl, oldContentUrl)
- && TextUtils.equals(newMetadataUrl, oldMetadataUrl)) {
- Log.i(TAG, "No flag changed, ignoring update");
- return;
- }
-
- try {
- mCertificateTransparencyDownloader.setPublicKey(newPublicKey);
- } catch (GeneralSecurityException e) {
- Log.e(TAG, "Error setting the public Key", e);
- return;
- }
-
- // TODO: handle the case where there is already a pending download.
-
- mDataStore.setProperty(Config.VERSION_PENDING, newVersion);
- mDataStore.setProperty(Config.CONTENT_URL_PENDING, newContentUrl);
- mDataStore.setProperty(Config.METADATA_URL_PENDING, newMetadataUrl);
- mDataStore.store();
-
- mCertificateTransparencyDownloader.startMetadataDownload(newMetadataUrl);
- }
-}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
index 4ca97eb..9970667 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
@@ -83,7 +83,7 @@
DirectoryUtils.makeDir(mRootDirectory);
if (!compatVersion.install(newContent, version)) {
- Log.e(TAG, "Failed to install logs for compatibility version " + compatibilityVersion);
+ Log.e(TAG, "Failed to install logs version " + version);
return false;
}
Log.i(TAG, "New logs installed at " + compatVersion.getLogsDir());
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
index 6fbf0ba..baca2e3 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
@@ -23,65 +23,92 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
+import android.os.ConfigUpdate;
import android.os.SystemClock;
-import android.provider.DeviceConfig;
import android.util.Log;
-import java.util.HashMap;
-
/** Implementation of the Certificate Transparency job */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
public class CertificateTransparencyJob extends BroadcastReceiver {
private static final String TAG = "CertificateTransparencyJob";
- private static final String ACTION_JOB_START = "com.android.server.net.ct.action.JOB_START";
-
private final Context mContext;
private final DataStore mDataStore;
private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
- // TODO(b/374692404): remove dependency to flags.
- private final CertificateTransparencyFlagsListener mFlagsListener;
private final AlarmManager mAlarmManager;
+ private final PendingIntent mPendingIntent;
+
+ private boolean mDependenciesReady = false;
/** Creates a new {@link CertificateTransparencyJob} object. */
public CertificateTransparencyJob(
Context context,
DataStore dataStore,
- CertificateTransparencyDownloader certificateTransparencyDownloader,
- CertificateTransparencyFlagsListener flagsListener) {
+ CertificateTransparencyDownloader certificateTransparencyDownloader) {
mContext = context;
- mFlagsListener = flagsListener;
mDataStore = dataStore;
mCertificateTransparencyDownloader = certificateTransparencyDownloader;
mAlarmManager = context.getSystemService(AlarmManager.class);
+ mPendingIntent =
+ PendingIntent.getBroadcast(
+ mContext,
+ /* requestCode= */ 0,
+ new Intent(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
+ PendingIntent.FLAG_IMMUTABLE);
}
- void initialize() {
- mDataStore.load();
- mCertificateTransparencyDownloader.initialize();
-
+ void schedule() {
mContext.registerReceiver(
- this, new IntentFilter(ACTION_JOB_START), Context.RECEIVER_EXPORTED);
+ this,
+ new IntentFilter(ConfigUpdate.ACTION_UPDATE_CT_LOGS),
+ Context.RECEIVER_EXPORTED);
mAlarmManager.setInexactRepeating(
AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime(), // schedule first job at earliest convenient time.
AlarmManager.INTERVAL_DAY,
- PendingIntent.getBroadcast(
- mContext, 0, new Intent(ACTION_JOB_START), PendingIntent.FLAG_IMMUTABLE));
+ mPendingIntent);
if (Config.DEBUG) {
- Log.d(TAG, "CertificateTransparencyJob scheduled successfully.");
+ Log.d(TAG, "CertificateTransparencyJob scheduled.");
+ }
+ }
+
+ void cancel() {
+ mContext.unregisterReceiver(this);
+ mAlarmManager.cancel(mPendingIntent);
+ mCertificateTransparencyDownloader.stop();
+ mDependenciesReady = false;
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyJob canceled.");
}
}
@Override
public void onReceive(Context context, Intent intent) {
- if (!ACTION_JOB_START.equals(intent.getAction())) {
+ if (!ConfigUpdate.ACTION_UPDATE_CT_LOGS.equals(intent.getAction())) {
Log.w(TAG, "Received unexpected broadcast with action " + intent);
return;
}
- mFlagsListener.onPropertiesChanged(
- new DeviceConfig.Properties(Config.NAMESPACE_NETWORK_SECURITY, new HashMap<>()));
+ if (Config.DEBUG) {
+ Log.d(TAG, "Starting CT daily job.");
+ }
+ if (!mDependenciesReady) {
+ mDataStore.load();
+ mCertificateTransparencyDownloader.start();
+ mDependenciesReady = true;
+ }
+
+ mDataStore.setProperty(Config.CONTENT_URL, Config.URL_LOG_LIST);
+ mDataStore.setProperty(Config.METADATA_URL, Config.URL_SIGNATURE);
+ mDataStore.setProperty(Config.PUBLIC_KEY_URL, Config.URL_PUBLIC_KEY);
+ mDataStore.store();
+
+ if (mCertificateTransparencyDownloader.startPublicKeyDownload() == -1) {
+ Log.e(TAG, "Public key download not started.");
+ } else if (Config.DEBUG) {
+ Log.d(TAG, "Public key download started successfully.");
+ }
}
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
new file mode 100644
index 0000000..70fb1ae
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net.ct;
+
+/** Interface with logging to statsd for Certificate Transparency. */
+public interface CertificateTransparencyLogger {
+
+ /**
+ * Logs a CTLogListUpdateFailed event to statsd, when no HTTP error status code is present.
+ *
+ * @param failureReason reason why the log list wasn't updated (e.g. DownloadManager failures)
+ * @param failureCount number of consecutive log list update failures
+ */
+ void logCTLogListUpdateFailedEvent(int failureReason, int failureCount);
+
+ /**
+ * Logs a CTLogListUpdateFailed event to statsd, when an HTTP error status code is provided.
+ *
+ * @param failureReason reason why the log list wasn't updated (e.g. DownloadManager failures)
+ * @param failureCount number of consecutive log list update failures
+ * @param httpErrorStatusCode if relevant, the HTTP error status code from DownloadManager
+ */
+ void logCTLogListUpdateFailedEvent(
+ int failureReason, int failureCount, int httpErrorStatusCode);
+
+}
\ No newline at end of file
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
new file mode 100644
index 0000000..f660752
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net.ct;
+
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED;
+
+/** Implementation for logging to statsd for Certificate Transparency. */
+class CertificateTransparencyLoggerImpl implements CertificateTransparencyLogger {
+
+ @Override
+ public void logCTLogListUpdateFailedEvent(int failureReason, int failureCount) {
+ logCTLogListUpdateFailedEvent(failureReason, failureCount, /* httpErrorStatusCode= */ 0);
+ }
+
+ @Override
+ public void logCTLogListUpdateFailedEvent(
+ int failureReason, int failureCount, int httpErrorStatusCode) {
+ CertificateTransparencyStatsLog.write(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED,
+ failureReason,
+ failureCount,
+ httpErrorStatusCode
+ );
+ }
+
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
index ac55e44..eb24567 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -13,46 +13,58 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.android.server.net.ct;
+import static android.security.Flags.certificateTransparencyConfiguration;
+
+import static com.android.net.ct.flags.Flags.certificateTransparencyService;
+
import android.annotation.RequiresApi;
import android.content.Context;
import android.net.ct.ICertificateTransparencyManager;
import android.os.Build;
import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
+import android.util.Log;
-import com.android.net.ct.flags.Flags;
import com.android.server.SystemService;
+import java.util.concurrent.Executors;
+
/** Implementation of the Certificate Transparency service. */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
+public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub
+ implements DeviceConfig.OnPropertiesChangedListener {
- private final DataStore mDataStore;
- private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
- private final CertificateTransparencyFlagsListener mFlagsListener;
+ private static final String TAG = "CertificateTransparencyService";
+
private final CertificateTransparencyJob mCertificateTransparencyJob;
+ private boolean started = false;
+
/**
* @return true if the CertificateTransparency service is enabled.
*/
public static boolean enabled(Context context) {
- return DeviceConfig.getBoolean(
- Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_SERVICE_ENABLED, false)
- && Flags.certificateTransparencyService();
+ return certificateTransparencyService() && certificateTransparencyConfiguration();
}
/** Creates a new {@link CertificateTransparencyService} object. */
public CertificateTransparencyService(Context context) {
- mDataStore = new DataStore(Config.PREFERENCES_FILE);
- mCertificateTransparencyDownloader =
- new CertificateTransparencyDownloader(context, mDataStore);
- mFlagsListener =
- new CertificateTransparencyFlagsListener(
- mDataStore, mCertificateTransparencyDownloader);
+ DataStore dataStore = new DataStore(Config.PREFERENCES_FILE);
+ DownloadHelper downloadHelper = new DownloadHelper(context);
+ SignatureVerifier signatureVerifier = new SignatureVerifier(context);
+ CertificateTransparencyDownloader downloader =
+ new CertificateTransparencyDownloader(
+ context,
+ dataStore,
+ downloadHelper,
+ signatureVerifier,
+ new CertificateTransparencyInstaller(),
+ new CertificateTransparencyLoggerImpl());
mCertificateTransparencyJob =
- new CertificateTransparencyJob(
- context, mDataStore, mCertificateTransparencyDownloader, mFlagsListener);
+ new CertificateTransparencyJob(context, dataStore, downloader);
}
/**
@@ -61,16 +73,52 @@
* @see com.android.server.SystemService#onBootPhase
*/
public void onBootPhase(int phase) {
-
switch (phase) {
case SystemService.PHASE_BOOT_COMPLETED:
- if (Flags.certificateTransparencyJob()) {
- mCertificateTransparencyJob.initialize();
- } else {
- mFlagsListener.initialize();
- }
+ DeviceConfig.addOnPropertiesChangedListener(
+ Config.NAMESPACE_NETWORK_SECURITY,
+ Executors.newSingleThreadExecutor(),
+ this);
+ onPropertiesChanged(
+ new Properties.Builder(Config.NAMESPACE_NETWORK_SECURITY).build());
break;
default:
}
}
+
+ @Override
+ public void onPropertiesChanged(Properties properties) {
+ if (!Config.NAMESPACE_NETWORK_SECURITY.equals(properties.getNamespace())) {
+ return;
+ }
+
+ if (DeviceConfig.getBoolean(
+ Config.NAMESPACE_NETWORK_SECURITY,
+ Config.FLAG_SERVICE_ENABLED,
+ /* defaultValue= */ true)) {
+ startService();
+ } else {
+ stopService();
+ }
+ }
+
+ private void startService() {
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyService start");
+ }
+ if (!started) {
+ mCertificateTransparencyJob.schedule();
+ started = true;
+ }
+ }
+
+ private void stopService() {
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyService stop");
+ }
+ if (started) {
+ mCertificateTransparencyJob.cancel();
+ started = false;
+ }
+ }
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
index 242f13a..aafee60 100644
--- a/networksecurity/service/src/com/android/server/net/ct/Config.java
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -47,12 +47,21 @@
static final String FLAG_PUBLIC_KEY = FLAGS_PREFIX + "public_key";
// properties
- static final String VERSION_PENDING = "version_pending";
static final String VERSION = "version";
- static final String CONTENT_URL_PENDING = "content_url_pending";
static final String CONTENT_URL = "content_url";
- static final String CONTENT_URL_KEY = "content_url_key";
- static final String METADATA_URL_PENDING = "metadata_url_pending";
+ static final String CONTENT_DOWNLOAD_ID = "content_download_id";
static final String METADATA_URL = "metadata_url";
- static final String METADATA_URL_KEY = "metadata_url_key";
+ static final String METADATA_DOWNLOAD_ID = "metadata_download_id";
+ static final String PUBLIC_KEY_URL = "public_key_url";
+ static final String PUBLIC_KEY_DOWNLOAD_ID = "public_key_download_id";
+ static final String LOG_LIST_UPDATE_FAILURE_COUNT = "log_list_update_failure_count";
+
+ // URLs
+ static final String URL_PREFIX = "https://www.gstatic.com/android/certificate_transparency/";
+ static final String URL_LOG_LIST = URL_PREFIX + "log_list.json";
+ static final String URL_SIGNATURE = URL_PREFIX + "log_list.sig";
+ static final String URL_PUBLIC_KEY = URL_PREFIX + "log_list.pub";
+
+ // Threshold amounts
+ static final int LOG_LIST_UPDATE_FAILURE_THRESHOLD = 10;
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/DataStore.java b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
index cd6aebf..3779269 100644
--- a/networksecurity/service/src/com/android/server/net/ct/DataStore.java
+++ b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
@@ -64,4 +64,12 @@
Object setPropertyLong(String key, long value) {
return setProperty(key, Long.toString(value));
}
+
+ int getPropertyInt(String key, int defaultValue) {
+ return Optional.ofNullable(getProperty(key)).map(Integer::parseInt).orElse(defaultValue);
+ }
+
+ Object setPropertyInt(String key, int value) {
+ return setProperty(key, Integer.toString(value));
+ }
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/MissingPublicKeyException.java b/networksecurity/service/src/com/android/server/net/ct/MissingPublicKeyException.java
new file mode 100644
index 0000000..80607f6
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/MissingPublicKeyException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net.ct;
+
+/**
+ * An exception thrown when the public key is missing for CT signature verification.
+ */
+public class MissingPublicKeyException extends Exception {
+
+ public MissingPublicKeyException(String message) {
+ super(message);
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
new file mode 100644
index 0000000..96488fc
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net.ct;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+import java.util.Optional;
+
+/** Verifier of the log list signature. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+public class SignatureVerifier {
+
+ private final Context mContext;
+
+ @NonNull private Optional<PublicKey> mPublicKey = Optional.empty();
+
+ public SignatureVerifier(Context context) {
+ mContext = context;
+ }
+
+ @VisibleForTesting
+ Optional<PublicKey> getPublicKey() {
+ return mPublicKey;
+ }
+
+ void resetPublicKey() {
+ mPublicKey = Optional.empty();
+ }
+
+ void setPublicKeyFrom(Uri file) throws GeneralSecurityException, IOException {
+ try (InputStream fileStream = mContext.getContentResolver().openInputStream(file)) {
+ setPublicKey(new String(fileStream.readAllBytes()));
+ }
+ }
+
+ void setPublicKey(String publicKey) throws GeneralSecurityException {
+ setPublicKey(
+ KeyFactory.getInstance("RSA")
+ .generatePublic(
+ new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))));
+ }
+
+ @VisibleForTesting
+ void setPublicKey(PublicKey publicKey) {
+ mPublicKey = Optional.of(publicKey);
+ }
+
+ boolean verify(Uri file, Uri signature)
+ throws GeneralSecurityException, IOException, MissingPublicKeyException {
+ if (!mPublicKey.isPresent()) {
+ throw new MissingPublicKeyException("Missing public key for signature verification");
+ }
+ Signature verifier = Signature.getInstance("SHA256withRSA");
+ verifier.initVerify(mPublicKey.get());
+ ContentResolver contentResolver = mContext.getContentResolver();
+
+ try (InputStream fileStream = contentResolver.openInputStream(file);
+ InputStream signatureStream = contentResolver.openInputStream(signature)) {
+ verifier.update(fileStream.readAllBytes());
+ return verifier.verify(signatureStream.readAllBytes());
+ }
+ }
+}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index fb55295..25f0dc1 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -15,24 +15,37 @@
*/
package com.android.server.net.ct;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.app.DownloadManager.Request;
import android.content.Context;
import android.content.Intent;
+import android.database.Cursor;
+import android.database.MatrixCursor;
import android.net.Uri;
import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.server.net.ct.DownloadHelper.DownloadStatus;
-
+import org.json.JSONException;
+import org.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -59,20 +72,23 @@
@RunWith(JUnit4.class)
public class CertificateTransparencyDownloaderTest {
- @Mock private DownloadHelper mDownloadHelper;
+ @Mock private DownloadManager mDownloadManager;
@Mock private CertificateTransparencyInstaller mCertificateTransparencyInstaller;
+ @Mock private CertificateTransparencyLogger mLogger;
private PrivateKey mPrivateKey;
private PublicKey mPublicKey;
private Context mContext;
private File mTempFile;
private DataStore mDataStore;
+ private SignatureVerifier mSignatureVerifier;
private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
+ private long mNextDownloadId = 666;
+
@Before
public void setUp() throws IOException, GeneralSecurityException {
MockitoAnnotations.initMocks(this);
-
KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
KeyPair keyPair = instance.generateKeyPair();
mPrivateKey = keyPair.getPrivate();
@@ -81,195 +97,610 @@
mContext = InstrumentationRegistry.getInstrumentation().getContext();
mTempFile = File.createTempFile("datastore-test", ".properties");
mDataStore = new DataStore(mTempFile);
- mDataStore.load();
-
+ mSignatureVerifier = new SignatureVerifier(mContext);
mCertificateTransparencyDownloader =
new CertificateTransparencyDownloader(
- mContext, mDataStore, mDownloadHelper, mCertificateTransparencyInstaller);
+ mContext,
+ mDataStore,
+ new DownloadHelper(mDownloadManager),
+ mSignatureVerifier,
+ mCertificateTransparencyInstaller,
+ mLogger);
+
+ prepareDataStore();
+ prepareDownloadManager();
}
@After
public void tearDown() {
mTempFile.delete();
- mCertificateTransparencyDownloader.resetPublicKey();
+ mSignatureVerifier.resetPublicKey();
+ }
+
+ @Test
+ public void testDownloader_startPublicKeyDownload() {
+ assertThat(mCertificateTransparencyDownloader.hasPublicKeyDownloadId()).isFalse();
+ long downloadId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+
+ assertThat(mCertificateTransparencyDownloader.hasPublicKeyDownloadId()).isTrue();
+ assertThat(mCertificateTransparencyDownloader.isPublicKeyDownloadId(downloadId)).isTrue();
}
@Test
public void testDownloader_startMetadataDownload() {
- String metadataUrl = "http://test-metadata.org";
- long downloadId = 666;
- when(mDownloadHelper.startDownload(metadataUrl)).thenReturn(downloadId);
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ long downloadId = mCertificateTransparencyDownloader.startMetadataDownload();
- assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isFalse();
- mCertificateTransparencyDownloader.startMetadataDownload(metadataUrl);
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isTrue();
assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isTrue();
}
@Test
public void testDownloader_startContentDownload() {
- String contentUrl = "http://test-content.org";
- long downloadId = 666;
- when(mDownloadHelper.startDownload(contentUrl)).thenReturn(downloadId);
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
+ long downloadId = mCertificateTransparencyDownloader.startContentDownload();
- assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isFalse();
- mCertificateTransparencyDownloader.startContentDownload(contentUrl);
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isTrue();
assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isTrue();
}
@Test
- public void testDownloader_metadataDownloadSuccess_startContentDownload() {
- long metadataId = 123;
- mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
- when(mDownloadHelper.getDownloadStatus(metadataId))
- .thenReturn(makeSuccessfulDownloadStatus(metadataId));
- long contentId = 666;
- String contentUrl = "http://test-content.org";
- mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUrl);
- when(mDownloadHelper.startDownload(contentUrl)).thenReturn(contentId);
+ public void testDownloader_publicKeyDownloadSuccess_updatePublicKey_startMetadataDownload()
+ throws Exception {
+ long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+ setSuccessfulDownload(publicKeyId, writePublicKeyToFile(mPublicKey));
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(publicKeyId));
+
+ assertThat(mSignatureVerifier.getPublicKey()).hasValue(mPublicKey);
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isTrue();
+ }
+
+ @Test
+ public void
+ testDownloader_publicKeyDownloadSuccess_updatePublicKeyFail_doNotStartMetadataDownload()
+ throws Exception {
+ long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+ setSuccessfulDownload(
+ publicKeyId, writeToFile("i_am_not_a_base64_encoded_public_key".getBytes()));
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(publicKeyId));
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ }
+
+ @Test
+ public void testDownloader_publicKeyDownloadFail_doNotUpdatePublicKey() throws Exception {
+ long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+ setFailedDownload(
+ publicKeyId, // Failure cases where we give up on the download.
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ DownloadManager.ERROR_HTTP_DATA_ERROR);
+ Intent downloadCompleteIntent = makeDownloadCompleteIntent(publicKeyId);
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+
+ assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
+ assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+ }
+
+ @Test
+ public void testDownloader_publicKeyDownloadFail_failureThresholdExceeded_logsFailure()
+ throws Exception {
+ long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+ setFailedDownload(
+ publicKeyId, // Failure cases where we give up on the download.
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ DownloadManager.ERROR_HTTP_DATA_ERROR);
+ Intent downloadCompleteIntent = makeDownloadCompleteIntent(publicKeyId);
+
+ mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+
+ assertThat(mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
+ );
+ }
+
+ @Test
+ public void testDownloader_publicKeyDownloadFail_failureThresholdNotMet_doesNotLog()
+ throws Exception {
+ long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+ // Set the failure count to well below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
+ setFailedDownload(
+ publicKeyId, // Failure cases where we give up on the download.
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ DownloadManager.ERROR_HTTP_DATA_ERROR);
+ Intent downloadCompleteIntent = makeDownloadCompleteIntent(publicKeyId);
+
+ mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+
+ assertThat(mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(1);
+ verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
+ }
+
+ @Test
+ public void testDownloader_metadataDownloadSuccess_startContentDownload() {
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setSuccessfulDownload(metadataId, new File("log_list.sig"));
+
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
mCertificateTransparencyDownloader.onReceive(
mContext, makeDownloadCompleteIntent(metadataId));
- assertThat(mCertificateTransparencyDownloader.isContentDownloadId(contentId)).isTrue();
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isTrue();
}
@Test
public void testDownloader_metadataDownloadFail_doNotStartContentDownload() {
- long metadataId = 123;
- mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
- String contentUrl = "http://test-content.org";
- mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUrl);
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setFailedDownload(
+ metadataId,
+ // Failure cases where we give up on the download.
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ DownloadManager.ERROR_HTTP_DATA_ERROR);
Intent downloadCompleteIntent = makeDownloadCompleteIntent(metadataId);
- // In all these failure cases we give up on the download.
- when(mDownloadHelper.getDownloadStatus(metadataId))
- .thenReturn(
- makeHttpErrorDownloadStatus(metadataId),
- makeStorageErrorDownloadStatus(metadataId));
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
- verify(mDownloadHelper, never()).startDownload(contentUrl);
+ assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
+ }
+
+ @Test
+ public void testDownloader_metadataDownloadFail_failureThresholdExceeded_logsFailure()
+ throws Exception {
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+ setFailedDownload(
+ metadataId,
+ // Failure cases where we give up on the download.
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ DownloadManager.ERROR_HTTP_DATA_ERROR);
+ Intent downloadCompleteIntent = makeDownloadCompleteIntent(metadataId);
+
+ mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+
+ assertThat(mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
+ );
+ }
+
+ @Test
+ public void testDownloader_metadataDownloadFail_failureThresholdNotMet_doesNotLog()
+ throws Exception {
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ // Set the failure count to well below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
+ setFailedDownload(
+ metadataId,
+ // Failure cases where we give up on the download.
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ DownloadManager.ERROR_HTTP_DATA_ERROR);
+ Intent downloadCompleteIntent = makeDownloadCompleteIntent(metadataId);
+
+ mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+
+ assertThat(mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(1);
+ verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
}
@Test
public void testDownloader_contentDownloadSuccess_installSuccess_updateDataStore()
throws Exception {
- String version = "456";
- long contentId = 666;
- File logListFile = File.createTempFile("log_list", "json");
- Uri contentUri = Uri.fromFile(logListFile);
- long metadataId = 123;
+ String newVersion = "456";
+ File logListFile = makeLogListFile(newVersion);
File metadataFile = sign(logListFile);
- Uri metadataUri = Uri.fromFile(metadataFile);
- mCertificateTransparencyDownloader.setPublicKey(
- Base64.getEncoder().encodeToString(mPublicKey.getEncoded()));
- setUpContentDownloadCompleteSuccessful(
- version, metadataId, metadataUri, contentId, contentUri);
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setSuccessfulDownload(metadataId, metadataFile);
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ setSuccessfulDownload(contentId, logListFile);
when(mCertificateTransparencyInstaller.install(
- eq(Config.COMPATIBILITY_VERSION), any(), eq(version)))
+ eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
.thenReturn(true);
- assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
- assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
- assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
+ assertNoVersionIsInstalled();
mCertificateTransparencyDownloader.onReceive(
mContext, makeDownloadCompleteIntent(contentId));
- verify(mCertificateTransparencyInstaller, times(1))
- .install(eq(Config.COMPATIBILITY_VERSION), any(), eq(version));
- assertThat(mDataStore.getProperty(Config.VERSION)).isEqualTo(version);
- assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isEqualTo(contentUri.toString());
- assertThat(mDataStore.getProperty(Config.METADATA_URL)).isEqualTo(metadataUri.toString());
+ assertInstallSuccessful(newVersion);
}
@Test
public void testDownloader_contentDownloadFail_doNotInstall() throws Exception {
- mDataStore.setProperty(Config.VERSION_PENDING, "123");
- long contentId = 666;
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ setFailedDownload(
+ contentId,
+ // Failure cases where we give up on the download.
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ DownloadManager.ERROR_HTTP_DATA_ERROR);
Intent downloadCompleteIntent = makeDownloadCompleteIntent(contentId);
- // In all these failure cases we give up on the download.
- when(mDownloadHelper.getDownloadStatus(contentId))
- .thenReturn(
- makeHttpErrorDownloadStatus(contentId),
- makeStorageErrorDownloadStatus(contentId));
mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
verify(mCertificateTransparencyInstaller, never()).install(any(), any(), any());
- assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
- assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
- assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
+ assertNoVersionIsInstalled();
+ }
+
+ @Test
+ public void testDownloader_contentDownloadFail_failureThresholdExceeded_logsFailure()
+ throws Exception {
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+ setFailedDownload(
+ contentId,
+ // Failure cases where we give up on the download.
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ DownloadManager.ERROR_HTTP_DATA_ERROR);
+ Intent downloadCompleteIntent = makeDownloadCompleteIntent(contentId);
+
+ mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+
+ assertThat(mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_NO_DISK_SPACE,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
+ );
+ }
+
+ @Test
+ public void testDownloader_contentDownloadFail_failureThresholdNotMet_doesNotLog()
+ throws Exception {
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ // Set the failure count to well below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
+ setFailedDownload(
+ contentId,
+ // Failure cases where we give up on the download.
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ DownloadManager.ERROR_HTTP_DATA_ERROR);
+ Intent downloadCompleteIntent = makeDownloadCompleteIntent(contentId);
+
+ mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+
+ assertThat(mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(1);
+ verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
}
@Test
public void testDownloader_contentDownloadSuccess_installFail_doNotUpdateDataStore()
throws Exception {
- String version = "456";
- long contentId = 666;
- File logListFile = File.createTempFile("log_list", "json");
- Uri contentUri = Uri.fromFile(logListFile);
- long metadataId = 123;
+ File logListFile = makeLogListFile("456");
File metadataFile = sign(logListFile);
- Uri metadataUri = Uri.fromFile(metadataFile);
- setUpContentDownloadCompleteSuccessful(
- version, metadataId, metadataUri, contentId, contentUri);
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setSuccessfulDownload(metadataId, metadataFile);
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ setSuccessfulDownload(contentId, logListFile);
when(mCertificateTransparencyInstaller.install(
- eq(Config.COMPATIBILITY_VERSION), any(), eq(version)))
+ eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
+ .thenReturn(false);
+
+ assertNoVersionIsInstalled();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ assertNoVersionIsInstalled();
+ }
+
+ @Test
+ public void
+ testDownloader_contentDownloadSuccess_noSignatureFound_failureThresholdExceeded_logsSingleFailure()
+ throws Exception {
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setSuccessfulDownload(metadataId, metadataFile);
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ setSuccessfulDownload(contentId, logListFile);
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+
+ // Set the public key to be missing
+ mSignatureVerifier.resetPublicKey();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ assertThat(mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
+ );
+ verify(mLogger, never()).logCTLogListUpdateFailedEvent(
+ eq(CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION),
+ anyInt()
+ );
+ }
+
+ @Test
+ public void
+ testDownloader_contentDownloadSuccess_wrongSignatureAlgo_failureThresholdExceeded_logsSingleFailure()
+ throws Exception {
+ // Arrange
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+
+ // Set the key to be deliberately wrong by using diff algorithm
+ KeyPairGenerator instance = KeyPairGenerator.getInstance("EC");
+ mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
+
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setSuccessfulDownload(metadataId, metadataFile);
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ setSuccessfulDownload(contentId, logListFile);
+
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+
+ // Act
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ // Assert
+ assertThat(mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, never()).logCTLogListUpdateFailedEvent(
+ eq(CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND),
+ anyInt()
+ );
+ verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
+ );
+ }
+
+ @Test
+ public void
+ testDownloader_contentDownloadSuccess_signatureNotVerified_failureThresholdExceeded_logsSingleFailure()
+ throws Exception {
+ // Arrange
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+
+ // Set the key to be deliberately wrong by using diff key pair
+ KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
+ mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
+
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setSuccessfulDownload(metadataId, metadataFile);
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ setSuccessfulDownload(contentId, logListFile);
+
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+
+ // Act
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ // Assert
+ assertThat(mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, never()).logCTLogListUpdateFailedEvent(
+ eq(CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND),
+ anyInt()
+ );
+ verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
+ );
+ }
+
+ @Test
+ public void
+ testDownloader_contentDownloadSuccess_wrongSignature_failureThresholdNotMet_doesNotLog()
+ throws Exception {
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+ // Set the public key wrong, so signature verification fails
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setSuccessfulDownload(metadataId, metadataFile);
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ setSuccessfulDownload(contentId, logListFile);
+ // Set the failure count to well below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ assertThat(mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(1);
+ verify(mLogger, never()).logCTLogListUpdateFailedEvent(
+ eq(CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND),
+ anyInt()
+ );
+ verify(mLogger, never()).logCTLogListUpdateFailedEvent(
+ eq(CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION),
+ anyInt()
+ );
+ }
+
+ @Test
+ public void
+ testDownloader_contentDownloadSuccess_installFail_failureThresholdExceeded_logsFailure()
+ throws Exception {
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setSuccessfulDownload(metadataId, metadataFile);
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ setSuccessfulDownload(contentId, logListFile);
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+ when(mCertificateTransparencyInstaller.install(
+ eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
.thenReturn(false);
mCertificateTransparencyDownloader.onReceive(
mContext, makeDownloadCompleteIntent(contentId));
- assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
- assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
- assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
+ assertThat(mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, times(1)).logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS,
+ Config.LOG_LIST_UPDATE_FAILURE_THRESHOLD
+ );
}
@Test
- public void testDownloader_contentDownloadSuccess_verificationFail_doNotInstall()
- throws IOException {
- String version = "456";
- long contentId = 666;
- Uri contentUri = Uri.fromFile(File.createTempFile("log_list", "json"));
- long metadataId = 123;
- Uri metadataUri = Uri.fromFile(File.createTempFile("log_list-wrong_metadata", "sig"));
- setUpContentDownloadCompleteSuccessful(
- version, metadataId, metadataUri, contentId, contentUri);
+ public void
+ testDownloader_contentDownloadSuccess_installFail_failureThresholdNotMet_doesNotLog()
+ throws Exception {
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setSuccessfulDownload(metadataId, metadataFile);
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ setSuccessfulDownload(contentId, logListFile);
+ // Set the failure count to well below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
+ when(mCertificateTransparencyInstaller.install(
+ eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
+ .thenReturn(false);
mCertificateTransparencyDownloader.onReceive(
mContext, makeDownloadCompleteIntent(contentId));
+ assertThat(mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(1);
+ verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
+ }
+
+ @Test
+ public void testDownloader_contentDownloadSuccess_verificationFail_doNotInstall()
+ throws Exception {
+ File logListFile = makeLogListFile("456");
+ File metadataFile = File.createTempFile("log_list-wrong_metadata", "sig");
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setSuccessfulDownload(metadataId, metadataFile);
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ setSuccessfulDownload(contentId, logListFile);
+
+ assertNoVersionIsInstalled();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
verify(mCertificateTransparencyInstaller, never())
- .install(eq(Config.COMPATIBILITY_VERSION), any(), eq(version));
- assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
- assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
- assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
+ .install(eq(Config.COMPATIBILITY_VERSION), any(), anyString());
+ assertNoVersionIsInstalled();
}
@Test
public void testDownloader_contentDownloadSuccess_missingVerificationPublicKey_doNotInstall()
throws Exception {
- String version = "456";
- long contentId = 666;
- File logListFile = File.createTempFile("log_list", "json");
- Uri contentUri = Uri.fromFile(logListFile);
- long metadataId = 123;
+ File logListFile = makeLogListFile("456");
File metadataFile = sign(logListFile);
- Uri metadataUri = Uri.fromFile(metadataFile);
- setUpContentDownloadCompleteSuccessful(
- version, metadataId, metadataUri, contentId, contentUri);
+ mSignatureVerifier.resetPublicKey();
+ long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+ setSuccessfulDownload(metadataId, metadataFile);
+ long contentId = mCertificateTransparencyDownloader.startContentDownload();
+ setSuccessfulDownload(contentId, logListFile);
+ assertNoVersionIsInstalled();
mCertificateTransparencyDownloader.onReceive(
mContext, makeDownloadCompleteIntent(contentId));
verify(mCertificateTransparencyInstaller, never())
- .install(eq(Config.COMPATIBILITY_VERSION), any(), eq(version));
+ .install(eq(Config.COMPATIBILITY_VERSION), any(), anyString());
+ assertNoVersionIsInstalled();
+ }
+
+ @Test
+ public void testDownloader_endToEndSuccess_installNewVersion() throws Exception {
+ String newVersion = "456";
+ File logListFile = makeLogListFile(newVersion);
+ File metadataFile = sign(logListFile);
+ File publicKeyFile = writePublicKeyToFile(mPublicKey);
+
+ assertNoVersionIsInstalled();
+
+ // 1. Start download of public key.
+ long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+
+ // 2. On successful public key download, set the key and start the metatadata download.
+ setSuccessfulDownload(publicKeyId, publicKeyFile);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(publicKeyId));
+
+ // 3. On successful metadata download, start the content download.
+ long metadataId = mCertificateTransparencyDownloader.getMetadataDownloadId();
+ setSuccessfulDownload(metadataId, metadataFile);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(metadataId));
+
+ // 4. On successful content download, verify the signature and install the new version.
+ long contentId = mCertificateTransparencyDownloader.getContentDownloadId();
+ setSuccessfulDownload(contentId, logListFile);
+ when(mCertificateTransparencyInstaller.install(
+ eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
+ .thenReturn(true);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ assertInstallSuccessful(newVersion);
+ }
+
+ private void assertNoVersionIsInstalled() {
assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
- assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
- assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
+ }
+
+ private void assertInstallSuccessful(String version) {
+ assertThat(mDataStore.getProperty(Config.VERSION)).isEqualTo(version);
}
private Intent makeDownloadCompleteIntent(long downloadId) {
@@ -277,43 +708,76 @@
.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
}
- private void setUpContentDownloadCompleteSuccessful(
- String version, long metadataId, Uri metadataUri, long contentId, Uri contentUri)
- throws IOException {
- mDataStore.setProperty(Config.VERSION_PENDING, version);
-
- mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
- mDataStore.setProperty(Config.METADATA_URL_PENDING, metadataUri.toString());
- when(mDownloadHelper.getUri(metadataId)).thenReturn(metadataUri);
-
- mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, contentId);
- mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUri.toString());
- when(mDownloadHelper.getDownloadStatus(contentId))
- .thenReturn(makeSuccessfulDownloadStatus(contentId));
- when(mDownloadHelper.getUri(contentId)).thenReturn(contentUri);
+ private void prepareDataStore() {
+ mDataStore.load();
+ mDataStore.setProperty(Config.CONTENT_URL, Config.URL_LOG_LIST);
+ mDataStore.setProperty(Config.METADATA_URL, Config.URL_SIGNATURE);
+ mDataStore.setProperty(Config.PUBLIC_KEY_URL, Config.URL_PUBLIC_KEY);
}
- private DownloadStatus makeSuccessfulDownloadStatus(long downloadId) {
- return DownloadStatus.builder()
- .setDownloadId(downloadId)
- .setStatus(DownloadManager.STATUS_SUCCESSFUL)
- .build();
+ private void prepareDownloadManager() {
+ when(mDownloadManager.enqueue(any(Request.class)))
+ .thenAnswer(invocation -> mNextDownloadId++);
}
- private DownloadStatus makeStorageErrorDownloadStatus(long downloadId) {
- return DownloadStatus.builder()
- .setDownloadId(downloadId)
- .setStatus(DownloadManager.STATUS_FAILED)
- .setReason(DownloadManager.ERROR_INSUFFICIENT_SPACE)
- .build();
+ private Cursor makeSuccessfulDownloadCursor() {
+ MatrixCursor cursor =
+ new MatrixCursor(
+ new String[] {
+ DownloadManager.COLUMN_STATUS, DownloadManager.COLUMN_REASON
+ });
+ cursor.addRow(new Object[] {DownloadManager.STATUS_SUCCESSFUL, -1});
+ return cursor;
}
- private DownloadStatus makeHttpErrorDownloadStatus(long downloadId) {
- return DownloadStatus.builder()
- .setDownloadId(downloadId)
- .setStatus(DownloadManager.STATUS_FAILED)
- .setReason(DownloadManager.ERROR_HTTP_DATA_ERROR)
- .build();
+ private void setSuccessfulDownload(long downloadId, File file) {
+ when(mDownloadManager.query(any(Query.class))).thenReturn(makeSuccessfulDownloadCursor());
+ when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(Uri.fromFile(file));
+ }
+
+ private Cursor makeFailedDownloadCursor(int error) {
+ MatrixCursor cursor =
+ new MatrixCursor(
+ new String[] {
+ DownloadManager.COLUMN_STATUS, DownloadManager.COLUMN_REASON
+ });
+ cursor.addRow(new Object[] {DownloadManager.STATUS_FAILED, error});
+ return cursor;
+ }
+
+ private void setFailedDownload(long downloadId, int... downloadManagerErrors) {
+ Cursor first = makeFailedDownloadCursor(downloadManagerErrors[0]);
+ Cursor[] others = new Cursor[downloadManagerErrors.length - 1];
+ for (int i = 1; i < downloadManagerErrors.length; i++) {
+ others[i - 1] = makeFailedDownloadCursor(downloadManagerErrors[i]);
+ }
+ when(mDownloadManager.query(any())).thenReturn(first, others);
+ when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(null);
+ }
+
+ private File writePublicKeyToFile(PublicKey publicKey)
+ throws IOException, GeneralSecurityException {
+ return writeToFile(Base64.getEncoder().encode(publicKey.getEncoded()));
+ }
+
+ private File writeToFile(byte[] bytes) throws IOException, GeneralSecurityException {
+ File file = File.createTempFile("temp_file", "tmp");
+
+ try (OutputStream outputStream = new FileOutputStream(file)) {
+ outputStream.write(bytes);
+ }
+
+ return file;
+ }
+
+ private File makeLogListFile(String version) throws IOException, JSONException {
+ File logListFile = File.createTempFile("log_list", "json");
+
+ try (OutputStream outputStream = new FileOutputStream(logListFile)) {
+ outputStream.write(new JSONObject().put("version", version).toString().getBytes(UTF_8));
+ }
+
+ return logListFile;
}
private File sign(File file) throws IOException, GeneralSecurityException {
diff --git a/service-b/Android.bp b/service-b/Android.bp
new file mode 100644
index 0000000..47439ee
--- /dev/null
+++ b/service-b/Android.bp
@@ -0,0 +1,40 @@
+//
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_team: "trendy_team_enigma",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// TODO: b/380331248 This lib is a non-jarjared version of "service-connectivity-b-platform"
+// It will only be included in the Tethering module when the build system flag
+// RELEASE_MOVE_VCN_TO_MAINLINE is enabled. Including "service-connectivity-b-platform"
+// in Tethering will break art branch check because that lib lives in framework/base.
+// Once VCN is moved to Connectivity/, "service-connectivity-b-platform" can be cleaned up.
+java_library {
+ name: "service-connectivity-b-pre-jarjar",
+ defaults: ["service-connectivity-b-pre-jarjar-defaults"],
+ libs: ["service-connectivity-pre-jarjar"],
+
+ sdk_version: "system_server_current",
+
+ // TODO(b/210962470): Bump this to B
+ min_sdk_version: "30",
+
+ apex_available: [
+ "com.android.tethering",
+ ],
+}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 787e94e..d2e2a80 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -101,7 +101,7 @@
min_sdk_version: "21",
lint: {
error_checks: ["NewApi"],
-
+ baseline_filename: "lint-baseline-service-connectivity-mdns-standalone-build-test.xml",
},
srcs: [
"src/com/android/server/connectivity/mdns/**/*.java",
diff --git a/service-t/lint-baseline-service-connectivity-mdns-standalone-build-test.xml b/service-t/lint-baseline-service-connectivity-mdns-standalone-build-test.xml
new file mode 100644
index 0000000..232d31c
--- /dev/null
+++ b/service-t/lint-baseline-service-connectivity-mdns-standalone-build-test.xml
@@ -0,0 +1,972 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha08" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha08">
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadEngine.java"
+ line="37"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="FlaggedApi"
+ message="@FlaggedApi should specify an actual flag constant; raw strings are discouraged (and more importantly, **not enforced**)"
+ errorLine1="@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework-t/src/android/net/nsd/OffloadServiceInfo.java"
+ line="43"
+ column="13"/>
+ </issue>
+
+</issues>
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 0adb290..555549c 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -23,6 +23,7 @@
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.net.nsd.AdvertisingRequest.FLAG_SKIP_PROBING;
import static android.net.nsd.NsdManager.MDNS_DISCOVERY_MANAGER_EVENT;
import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT;
import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED;
@@ -981,7 +982,7 @@
NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */);
break;
}
- boolean isUpdateOnly = (advertisingRequest.getAdvertisingConfig()
+ boolean isUpdateOnly = (advertisingRequest.getFlags()
& AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0;
// If it is an update request, then reuse the old transactionId
if (isUpdateOnly) {
@@ -1046,9 +1047,12 @@
serviceInfo.setSubtypes(subtypes);
maybeStartMonitoringSockets();
+ final boolean skipProbing = (advertisingRequest.getFlags()
+ & FLAG_SKIP_PROBING) > 0;
final MdnsAdvertisingOptions mdnsAdvertisingOptions =
MdnsAdvertisingOptions.newBuilder()
.setIsOnlyUpdate(isUpdateOnly)
+ .setSkipProbing(skipProbing)
.setTtl(advertisingRequest.getTtl())
.build();
mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
@@ -1943,6 +1947,8 @@
.setCachedServicesRetentionTime(mDeps.getDeviceConfigPropertyInt(
MdnsFeatureFlags.NSD_CACHED_SERVICES_RETENTION_TIME,
MdnsFeatureFlags.DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS))
+ .setIsShortHostnamesEnabled(mDeps.isTetheringFeatureNotChickenedOut(
+ mContext, MdnsFeatureFlags.NSD_USE_SHORT_HOSTNAMES))
.setOverrideProvider(new MdnsFeatureFlags.FlagOverrideProvider() {
@Override
public boolean isForceEnabledForTest(@NonNull String flag) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
index f55db93..81ba530 100644
--- a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
+++ b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -139,11 +139,8 @@
// Base service type
questions.add(new MdnsPointerRecord(serviceTypeLabels, expectUnicastResponse));
for (String subtype : subtypes) {
- final String[] labels = new String[serviceTypeLabels.length + 2];
- labels[0] = MdnsConstants.SUBTYPE_PREFIX + subtype;
- labels[1] = MdnsConstants.SUBTYPE_LABEL;
- System.arraycopy(serviceTypeLabels, 0, labels, 2, serviceTypeLabels.length);
-
+ final String[] labels = MdnsUtils.constructFullSubtype(serviceTypeLabels,
+ MdnsConstants.SUBTYPE_PREFIX + subtype);
questions.add(new MdnsPointerRecord(labels, expectUnicastResponse));
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index 9c52eca..54f7ca3 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -46,6 +46,7 @@
import com.android.server.connectivity.ConnectivityResources;
import com.android.server.connectivity.mdns.util.MdnsUtils;
+import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -117,7 +118,7 @@
* Generates a unique hostname to be used by the device.
*/
@NonNull
- public String[] generateHostname() {
+ public String[] generateHostname(boolean useShortFormat) {
// Generate a very-probably-unique hostname. This allows minimizing possible conflicts
// to the point that probing for it is no longer necessary (as per RFC6762 8.1 last
// paragraph), and does not leak more information than what could already be obtained by
@@ -127,10 +128,24 @@
// Having a different hostname per interface is an acceptable option as per RFC6762 14.
// This hostname will change every time the interface is reconnected, so this does not
// allow tracking the device.
- // TODO: consider deriving a hostname from other sources, such as the IPv6 addresses
- // (reusing the same privacy-protecting mechanics).
- return new String[] {
- "Android_" + UUID.randomUUID().toString().replace("-", ""), LOCAL_TLD };
+ if (useShortFormat) {
+ // A short hostname helps reduce the size of APF mDNS filtering programs, and
+ // is necessary for compatibility with some Matter 1.0 devices which assumed
+ // 16 characters is the maximum length.
+ // Generate a hostname matching Android_[0-9A-Z]{8}, which has 36^8 possibilities.
+ // Even with 100 devices advertising the probability of collision is around 2E-9,
+ // which is negligible.
+ final SecureRandom sr = new SecureRandom();
+ final String allowedChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ final StringBuilder sb = new StringBuilder(8);
+ for (int i = 0; i < 8; i++) {
+ sb.append(allowedChars.charAt(sr.nextInt(allowedChars.length())));
+ }
+ return new String[]{ "Android_" + sb.toString(), LOCAL_TLD };
+ } else {
+ return new String[]{
+ "Android_" + UUID.randomUUID().toString().replace("-", ""), LOCAL_TLD};
+ }
}
}
@@ -825,7 +840,7 @@
mCb = cb;
mSocketProvider = socketProvider;
mDeps = deps;
- mDeviceHostName = deps.generateHostname();
+ mDeviceHostName = deps.generateHostname(mDnsFeatureFlags.isShortHostnamesEnabled());
mSharedLog = sharedLog;
mMdnsFeatureFlags = mDnsFeatureFlags;
final ConnectivityResources res = new ConnectivityResources(context);
@@ -943,7 +958,7 @@
mRegistrations.remove(id);
// Regenerates host name when registrations removed.
if (mRegistrations.size() == 0) {
- mDeviceHostName = mDeps.generateHostname();
+ mDeviceHostName = mDeps.generateHostname(mMdnsFeatureFlags.isShortHostnamesEnabled());
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
index a81d1e4..5133d4f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
@@ -34,13 +34,15 @@
private final boolean mIsOnlyUpdate;
@Nullable
private final Duration mTtl;
+ private final boolean mSkipProbing;
/**
* Parcelable constructs for a {@link MdnsAdvertisingOptions}.
*/
- MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl) {
+ MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl, boolean skipProbing) {
this.mIsOnlyUpdate = isOnlyUpdate;
this.mTtl = ttl;
+ this.mSkipProbing = skipProbing;
}
/**
@@ -68,6 +70,13 @@
}
/**
+ * @return {@code true} if the probing step should be skipped.
+ */
+ public boolean skipProbing() {
+ return mSkipProbing;
+ }
+
+ /**
* Returns the TTL for all records in a service.
*/
@Nullable
@@ -104,6 +113,7 @@
*/
public static final class Builder {
private boolean mIsOnlyUpdate = false;
+ private boolean mSkipProbing = false;
@Nullable
private Duration mTtl;
@@ -127,10 +137,18 @@
}
/**
+ * Sets whether to skip the probing step.
+ */
+ public Builder setSkipProbing(boolean skipProbing) {
+ this.mSkipProbing = skipProbing;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsAdvertisingOptions} with the arguments supplied to this builder.
*/
public MdnsAdvertisingOptions build() {
- return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl);
+ return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl, mSkipProbing);
}
}
}
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 4e27fef..2f3bdc5 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -81,6 +81,12 @@
public static final String NSD_CACHED_SERVICES_REMOVAL = "nsd_cached_services_removal";
/**
+ * A feature flag to control whether to use shorter (16 characters + .local) hostnames, instead
+ * of Android_[32 characters] hostnames.
+ */
+ public static final String NSD_USE_SHORT_HOSTNAMES = "nsd_use_short_hostnames";
+
+ /**
* A feature flag to control the retention time for cached services.
*
* <p> Making the retention time configurable allows for testing and future adjustments.
@@ -122,6 +128,9 @@
// Retention Time for cached services
public final long mCachedServicesRetentionTime;
+ // Flag to use shorter (16 characters + .local) hostnames
+ public final boolean mIsShortHostnamesEnabled;
+
@Nullable
private final FlagOverrideProvider mOverrideProvider;
@@ -217,6 +226,10 @@
NSD_CACHED_SERVICES_RETENTION_TIME, (int) mCachedServicesRetentionTime);
}
+ public boolean isShortHostnamesEnabled() {
+ return mIsShortHostnamesEnabled || isForceEnabledForTest(NSD_USE_SHORT_HOSTNAMES);
+ }
+
/**
* The constructor for {@link MdnsFeatureFlags}.
*/
@@ -231,6 +244,7 @@
boolean avoidAdvertisingEmptyTxtRecords,
boolean isCachedServicesRemovalEnabled,
long cachedServicesRetentionTime,
+ boolean isShortHostnamesEnabled,
@Nullable FlagOverrideProvider overrideProvider) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
@@ -243,6 +257,7 @@
mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
mIsCachedServicesRemovalEnabled = isCachedServicesRemovalEnabled;
mCachedServicesRetentionTime = cachedServicesRetentionTime;
+ mIsShortHostnamesEnabled = isShortHostnamesEnabled;
mOverrideProvider = overrideProvider;
}
@@ -266,6 +281,7 @@
private boolean mAvoidAdvertisingEmptyTxtRecords;
private boolean mIsCachedServicesRemovalEnabled;
private long mCachedServicesRetentionTime;
+ private boolean mIsShortHostnamesEnabled;
private FlagOverrideProvider mOverrideProvider;
/**
@@ -283,6 +299,7 @@
mAvoidAdvertisingEmptyTxtRecords = true; // Default enabled.
mIsCachedServicesRemovalEnabled = false;
mCachedServicesRetentionTime = DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS;
+ mIsShortHostnamesEnabled = true; // Default enabled.
mOverrideProvider = null;
}
@@ -409,6 +426,16 @@
}
/**
+ * Set whether the short hostnames feature is enabled.
+ *
+ * @see #NSD_USE_SHORT_HOSTNAMES
+ */
+ public Builder setIsShortHostnamesEnabled(boolean isShortHostnamesEnabled) {
+ mIsShortHostnamesEnabled = isShortHostnamesEnabled;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
*/
public MdnsFeatureFlags build() {
@@ -423,6 +450,7 @@
mAvoidAdvertisingEmptyTxtRecords,
mIsCachedServicesRemovalEnabled,
mCachedServicesRetentionTime,
+ mIsShortHostnamesEnabled,
mOverrideProvider);
}
}
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 58defa9..b9b09ed 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -122,28 +122,32 @@
}
@Override
public void onFinished(MdnsProber.ProbingInfo info) {
- final MdnsAnnouncer.AnnouncementInfo announcementInfo;
- mSharedLog.i("Probing finished for service " + info.getServiceId());
- mCbHandler.post(() -> mCb.onServiceProbingSucceeded(
- MdnsInterfaceAdvertiser.this, info.getServiceId()));
- try {
- announcementInfo = mRecordRepository.onProbingSucceeded(info);
- } catch (IOException e) {
- mSharedLog.e("Error building announcements", e);
- return;
- }
+ handleProbingFinished(info);
+ }
+ }
- mAnnouncer.startSending(info.getServiceId(), announcementInfo,
- 0L /* initialDelayMs */);
+ private void handleProbingFinished(MdnsProber.ProbingInfo info) {
+ final MdnsAnnouncer.AnnouncementInfo announcementInfo;
+ mSharedLog.i("Probing finished for service " + info.getServiceId());
+ mCbHandler.post(() -> mCb.onServiceProbingSucceeded(
+ MdnsInterfaceAdvertiser.this, info.getServiceId()));
+ try {
+ announcementInfo = mRecordRepository.onProbingSucceeded(info);
+ } catch (IOException e) {
+ mSharedLog.e("Error building announcements", e);
+ return;
+ }
- // Re-announce the services which have the same custom hostname.
- final String hostname = mRecordRepository.getHostnameForServiceId(info.getServiceId());
- if (hostname != null) {
- final List<MdnsAnnouncer.AnnouncementInfo> announcementInfos =
- new ArrayList<>(mRecordRepository.restartAnnouncingForHostname(hostname));
- announcementInfos.removeIf((i) -> i.getServiceId() == info.getServiceId());
- reannounceServices(announcementInfos);
- }
+ mAnnouncer.startSending(info.getServiceId(), announcementInfo,
+ 0L /* initialDelayMs */);
+
+ // Re-announce the services which have the same custom hostname.
+ final String hostname = mRecordRepository.getHostnameForServiceId(info.getServiceId());
+ if (hostname != null) {
+ final List<MdnsAnnouncer.AnnouncementInfo> announcementInfos =
+ new ArrayList<>(mRecordRepository.restartAnnouncingForHostname(hostname));
+ announcementInfos.removeIf((i) -> i.getServiceId() == info.getServiceId());
+ reannounceServices(announcementInfos);
}
}
@@ -280,7 +284,12 @@
+ " getting re-added, cancelling exit announcements");
mAnnouncer.stop(replacedExitingService);
}
- mProber.startProbing(mRecordRepository.setServiceProbing(id));
+ final MdnsProber.ProbingInfo probingInfo = mRecordRepository.setServiceProbing(id);
+ if (advertisingOptions.skipProbing()) {
+ handleProbingFinished(probingInfo);
+ } else {
+ mProber.startProbing(probingInfo);
+ }
}
/**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
index 356b738..7495aec 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
@@ -16,9 +16,14 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
+import com.android.internal.annotations.VisibleForTesting;
+
/**
* The query scheduler class for calculating next query tasks parameters.
* <p>
@@ -26,6 +31,25 @@
*/
public class MdnsQueryScheduler {
+ @VisibleForTesting
+ // RFC 6762 5.2: The interval between the first two queries MUST be at least one second.
+ static final int INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS = 1000;
+ private static final int INITIAL_TIME_BETWEEN_BURSTS_MS =
+ (int) MdnsConfigs.initialTimeBetweenBurstsMs();
+ private static final int MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS =
+ (int) MdnsConfigs.timeBetweenBurstsMs();
+ private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst();
+ private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS =
+ (int) MdnsConfigs.timeBetweenQueriesInBurstMs();
+ private static final int QUERIES_PER_BURST_PASSIVE_MODE =
+ (int) MdnsConfigs.queriesPerBurstPassive();
+ @VisibleForTesting
+ // Basically this tries to send one query per typical DTIM interval 100ms, to maximize the
+ // chances that a query will be received if devices are using a DTIM multiplier (in which case
+ // they only listen once every [multiplier] DTIM intervals).
+ static final int TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS = 100;
+ static final int MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS = 60000;
+
/**
* The argument for tracking the query tasks status.
*/
@@ -72,19 +96,21 @@
if (mLastScheduledQueryTaskArgs == null) {
return null;
}
- if (!mLastScheduledQueryTaskArgs.config.shouldUseQueryBackoff(numOfQueriesBeforeBackoff)) {
+ final QueryTaskConfig lastConfig = mLastScheduledQueryTaskArgs.config;
+ if (!shouldUseQueryBackoff(lastConfig.queryIndex, lastConfig.queryMode,
+ numOfQueriesBeforeBackoff)) {
return null;
}
final long timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
- mLastScheduledQueryTaskArgs.config, now, minRemainingTtl, lastSentTime,
+ lastConfig.queryIndex, lastConfig.queryMode, now, minRemainingTtl, lastSentTime,
numOfQueriesBeforeBackoff, false /* forceEnableBackoff */);
if (timeToRun <= mLastScheduledQueryTaskArgs.timeToRun) {
return null;
}
- mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(mLastScheduledQueryTaskArgs.config,
+ mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(lastConfig,
timeToRun,
minRemainingTtl + now,
sessionId);
@@ -104,17 +130,19 @@
int queryMode,
int numOfQueriesBeforeBackoff,
boolean forceEnableBackoff) {
- final QueryTaskConfig nextRunConfig = currentConfig.getConfigForNextRun(queryMode);
+ final int newQueryIndex = currentConfig.getConfigForNextRun(queryMode).queryIndex;
long timeToRun;
if (mLastScheduledQueryTaskArgs == null && !forceEnableBackoff) {
- timeToRun = now + nextRunConfig.getDelayBeforeTaskWithoutBackoff();
+ timeToRun = now + getDelayBeforeTaskWithoutBackoff(
+ newQueryIndex, queryMode);
} else {
- timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
- nextRunConfig, now, minRemainingTtl, lastSentTime, numOfQueriesBeforeBackoff,
- forceEnableBackoff);
+ timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs, newQueryIndex,
+ queryMode, now, minRemainingTtl, lastSentTime,
+ numOfQueriesBeforeBackoff, forceEnableBackoff);
}
- mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(nextRunConfig, timeToRun,
- minRemainingTtl + now,
+ mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(
+ currentConfig.getConfigForNextRun(queryMode),
+ timeToRun, minRemainingTtl + now,
sessionId);
return mLastScheduledQueryTaskArgs;
}
@@ -131,11 +159,11 @@
}
private static long calculateTimeToRun(@Nullable ScheduledQueryTaskArgs taskArgs,
- QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime,
+ int queryIndex, int queryMode, long now, long minRemainingTtl, long lastSentTime,
int numOfQueriesBeforeBackoff, boolean forceEnableBackoff) {
- final long baseDelayInMs = queryTaskConfig.getDelayBeforeTaskWithoutBackoff();
+ final long baseDelayInMs = getDelayBeforeTaskWithoutBackoff(queryIndex, queryMode);
if (!(forceEnableBackoff
- || queryTaskConfig.shouldUseQueryBackoff(numOfQueriesBeforeBackoff))) {
+ || shouldUseQueryBackoff(queryIndex, queryMode, numOfQueriesBeforeBackoff))) {
return lastSentTime + baseDelayInMs;
}
if (minRemainingTtl <= 0) {
@@ -152,4 +180,93 @@
}
return Math.max(now + (long) (0.8 * minRemainingTtl), lastSentTime + baseDelayInMs);
}
+
+ private static int getBurstIndex(int queryIndex, int queryMode) {
+ if (queryMode == PASSIVE_QUERY_MODE && queryIndex >= QUERIES_PER_BURST) {
+ // In passive mode, after the first burst of QUERIES_PER_BURST queries, subsequent
+ // bursts have QUERIES_PER_BURST_PASSIVE_MODE queries.
+ final int queryIndexAfterFirstBurst = queryIndex - QUERIES_PER_BURST;
+ return 1 + (queryIndexAfterFirstBurst / QUERIES_PER_BURST_PASSIVE_MODE);
+ } else {
+ return queryIndex / QUERIES_PER_BURST;
+ }
+ }
+
+ private static int getQueryIndexInBurst(int queryIndex, int queryMode) {
+ if (queryMode == PASSIVE_QUERY_MODE && queryIndex >= QUERIES_PER_BURST) {
+ final int queryIndexAfterFirstBurst = queryIndex - QUERIES_PER_BURST;
+ return queryIndexAfterFirstBurst % QUERIES_PER_BURST_PASSIVE_MODE;
+ } else {
+ return queryIndex % QUERIES_PER_BURST;
+ }
+ }
+
+ private static boolean isFirstBurst(int queryIndex, int queryMode) {
+ return getBurstIndex(queryIndex, queryMode) == 0;
+ }
+
+ static boolean isFirstQueryInBurst(int queryIndex, int queryMode) {
+ return getQueryIndexInBurst(queryIndex, queryMode) == 0;
+ }
+
+ private static long getDelayBeforeTaskWithoutBackoff(int queryIndex, int queryMode) {
+ final int burstIndex = getBurstIndex(queryIndex, queryMode);
+ final int queryIndexInBurst = getQueryIndexInBurst(queryIndex, queryMode);
+ if (queryIndexInBurst == 0) {
+ return getTimeToBurstMs(burstIndex, queryMode);
+ } else if (queryIndexInBurst == 1 && queryMode == AGGRESSIVE_QUERY_MODE) {
+ // In aggressive mode, the first 2 queries are sent without delay.
+ return 0;
+ }
+ return queryMode == AGGRESSIVE_QUERY_MODE
+ ? TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS
+ : TIME_BETWEEN_QUERIES_IN_BURST_MS;
+ }
+
+ /**
+ * Shifts a value left by the specified number of bits, coercing to at most maxValue.
+ *
+ * <p>This allows calculating min(value*2^shift, maxValue) without overflow.
+ */
+ private static int boundedLeftShift(int value, int shift, int maxValue) {
+ // There must be at least one leading zero for positive values, so the maximum left shift
+ // without overflow is the number of leading zeros minus one.
+ final int maxShift = Integer.numberOfLeadingZeros(value) - 1;
+ if (shift > maxShift) {
+ // The shift would overflow positive integers, so is greater than maxValue.
+ return maxValue;
+ }
+ return Math.min(value << shift, maxValue);
+ }
+
+ private static int getTimeToBurstMs(int burstIndex, int queryMode) {
+ if (burstIndex == 0) {
+ // No delay before the first burst
+ return 0;
+ }
+ switch (queryMode) {
+ case PASSIVE_QUERY_MODE:
+ return MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
+ case AGGRESSIVE_QUERY_MODE:
+ return boundedLeftShift(INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS,
+ burstIndex - 1,
+ MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS);
+ default: // ACTIVE_QUERY_MODE
+ return boundedLeftShift(INITIAL_TIME_BETWEEN_BURSTS_MS,
+ burstIndex - 1,
+ MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS);
+ }
+ }
+
+ /**
+ * Determine if the query backoff should be used.
+ */
+ public static boolean shouldUseQueryBackoff(int queryIndex, int queryMode,
+ int numOfQueriesBeforeBackoff) {
+ // Don't enable backoff mode during the burst or in the first burst
+ if (!isFirstQueryInBurst(queryIndex, queryMode) || isFirstBurst(queryIndex, queryMode)) {
+ return false;
+ }
+ return queryIndex > numOfQueriesBeforeBackoff;
+ }
}
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 c3cb776..bfef5d9 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -1482,22 +1482,14 @@
private static String[] splitFullyQualifiedName(
@NonNull NsdServiceInfo info, @NonNull String[] serviceType) {
- final String[] split = new String[serviceType.length + 1];
- split[0] = info.getServiceName();
- System.arraycopy(serviceType, 0, split, 1, serviceType.length);
-
- return split;
+ return CollectionUtils.prependArray(String.class, serviceType, info.getServiceName());
}
private static String[] splitServiceType(@NonNull NsdServiceInfo info) {
// String.split(pattern, 0) removes trailing empty strings, which would appear when
// splitting "domain.name." (with a dot a the end), so this is what is needed here.
final String[] split = info.getServiceType().split("\\.", 0);
- final String[] type = new String[split.length + 1];
- System.arraycopy(split, 0, type, 0, split.length);
- type[split.length] = LOCAL_TLD;
-
- return type;
+ return CollectionUtils.appendArray(String.class, split, LOCAL_TLD);
}
/** Returns whether there will be an SRV record when registering the {@code info}. */
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 a43486e..8c86fb8 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -730,7 +730,7 @@
serviceType,
subtypes,
taskArgs.config.expectUnicastResponse,
- taskArgs.config.transactionId,
+ taskArgs.config.getTransactionId(),
socketKey,
onlyUseIpv6OnIpv6OnlyNetworks,
sendDiscoveryQueries,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
index b640c32..1212e29 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
@@ -26,6 +26,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresApi;
+import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -164,6 +165,7 @@
this(context, looper, new Dependencies(), sharedLog, socketRequestMonitor);
}
+ @SuppressLint("NewApi")
MdnsSocketProvider(@NonNull Context context, @NonNull Looper looper,
@NonNull Dependencies deps, @NonNull SharedLog sharedLog,
@NonNull SocketRequestMonitor socketRequestMonitor) {
@@ -335,6 +337,7 @@
}
/*** Start monitoring sockets by listening callbacks for sockets creation or removal */
+ @SuppressLint("NewApi")
public void startMonitoringSockets() {
ensureRunningOnHandlerThread(mHandler);
mRequestStop = false; // Reset stop request flag.
@@ -365,6 +368,7 @@
}
}
+ @SuppressLint("NewApi")
private void maybeStopMonitoringSockets() {
if (!mMonitoringSockets) return; // Already unregistered.
if (!mRequestStop) return; // No stop request.
diff --git a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
index dd4073f..2ac5b74 100644
--- a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
+++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
@@ -17,7 +17,6 @@
package com.android.server.connectivity.mdns;
import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
-import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
import com.android.internal.annotations.VisibleForTesting;
@@ -26,136 +25,22 @@
* Call to getConfigForNextRun returns a config that can be used to build the next query task.
*/
public class QueryTaskConfig {
-
- private static final int INITIAL_TIME_BETWEEN_BURSTS_MS =
- (int) MdnsConfigs.initialTimeBetweenBurstsMs();
- private static final int MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS =
- (int) MdnsConfigs.timeBetweenBurstsMs();
- private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst();
- private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS =
- (int) MdnsConfigs.timeBetweenQueriesInBurstMs();
- private static final int QUERIES_PER_BURST_PASSIVE_MODE =
- (int) MdnsConfigs.queriesPerBurstPassive();
private static final int UNSIGNED_SHORT_MAX_VALUE = 65536;
- @VisibleForTesting
- // RFC 6762 5.2: The interval between the first two queries MUST be at least one second.
- static final int INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS = 1000;
- @VisibleForTesting
- // Basically this tries to send one query per typical DTIM interval 100ms, to maximize the
- // chances that a query will be received if devices are using a DTIM multiplier (in which case
- // they only listen once every [multiplier] DTIM intervals).
- static final int TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS = 100;
- static final int MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS = 60000;
private final boolean alwaysAskForUnicastResponse =
MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
@VisibleForTesting
- final int transactionId;
- @VisibleForTesting
final boolean expectUnicastResponse;
- private final int queryIndex;
- private final int queryMode;
+ final int queryIndex;
+ final int queryMode;
- QueryTaskConfig(int queryMode, int queryIndex, int transactionId,
- boolean expectUnicastResponse) {
+ QueryTaskConfig(int queryMode, int queryIndex) {
this.queryMode = queryMode;
- this.transactionId = transactionId;
this.queryIndex = queryIndex;
- this.expectUnicastResponse = expectUnicastResponse;
+ this.expectUnicastResponse = getExpectUnicastResponse();
}
QueryTaskConfig(int queryMode) {
- this(queryMode, 0, 1, true);
- }
-
- private static int getBurstIndex(int queryIndex, int queryMode) {
- if (queryMode == PASSIVE_QUERY_MODE && queryIndex >= QUERIES_PER_BURST) {
- // In passive mode, after the first burst of QUERIES_PER_BURST queries, subsequent
- // bursts have QUERIES_PER_BURST_PASSIVE_MODE queries.
- final int queryIndexAfterFirstBurst = queryIndex - QUERIES_PER_BURST;
- return 1 + (queryIndexAfterFirstBurst / QUERIES_PER_BURST_PASSIVE_MODE);
- } else {
- return queryIndex / QUERIES_PER_BURST;
- }
- }
-
- private static int getQueryIndexInBurst(int queryIndex, int queryMode) {
- if (queryMode == PASSIVE_QUERY_MODE && queryIndex >= QUERIES_PER_BURST) {
- final int queryIndexAfterFirstBurst = queryIndex - QUERIES_PER_BURST;
- return queryIndexAfterFirstBurst % QUERIES_PER_BURST_PASSIVE_MODE;
- } else {
- return queryIndex % QUERIES_PER_BURST;
- }
- }
-
- private static boolean isFirstBurst(int queryIndex, int queryMode) {
- return getBurstIndex(queryIndex, queryMode) == 0;
- }
-
- private static boolean isFirstQueryInBurst(int queryIndex, int queryMode) {
- return getQueryIndexInBurst(queryIndex, queryMode) == 0;
- }
-
- // TODO: move delay calculations to MdnsQueryScheduler
- long getDelayBeforeTaskWithoutBackoff() {
- return getDelayBeforeTaskWithoutBackoff(queryIndex, queryMode);
- }
-
- private static long getDelayBeforeTaskWithoutBackoff(int queryIndex, int queryMode) {
- final int burstIndex = getBurstIndex(queryIndex, queryMode);
- final int queryIndexInBurst = getQueryIndexInBurst(queryIndex, queryMode);
- if (queryIndexInBurst == 0) {
- return getTimeToBurstMs(burstIndex, queryMode);
- } else if (queryIndexInBurst == 1 && queryMode == AGGRESSIVE_QUERY_MODE) {
- // In aggressive mode, the first 2 queries are sent without delay.
- return 0;
- }
- return queryMode == AGGRESSIVE_QUERY_MODE
- ? TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS
- : TIME_BETWEEN_QUERIES_IN_BURST_MS;
- }
-
- private boolean getExpectUnicastResponse(int queryIndex, int queryMode) {
- if (queryMode == AGGRESSIVE_QUERY_MODE) {
- if (isFirstQueryInBurst(queryIndex, queryMode)) {
- return true;
- }
- }
- return alwaysAskForUnicastResponse;
- }
-
- /**
- * Shifts a value left by the specified number of bits, coercing to at most maxValue.
- *
- * <p>This allows calculating min(value*2^shift, maxValue) without overflow.
- */
- private static int boundedLeftShift(int value, int shift, int maxValue) {
- // There must be at least one leading zero for positive values, so the maximum left shift
- // without overflow is the number of leading zeros minus one.
- final int maxShift = Integer.numberOfLeadingZeros(value) - 1;
- if (shift > maxShift) {
- // The shift would overflow positive integers, so is greater than maxValue.
- return maxValue;
- }
- return Math.min(value << shift, maxValue);
- }
-
- private static int getTimeToBurstMs(int burstIndex, int queryMode) {
- if (burstIndex == 0) {
- // No delay before the first burst
- return 0;
- }
- switch (queryMode) {
- case PASSIVE_QUERY_MODE:
- return MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
- case AGGRESSIVE_QUERY_MODE:
- return boundedLeftShift(INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS,
- burstIndex - 1,
- MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS);
- default: // ACTIVE_QUERY_MODE
- return boundedLeftShift(INITIAL_TIME_BETWEEN_BURSTS_MS,
- burstIndex - 1,
- MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS);
- }
+ this(queryMode, 0);
}
/**
@@ -163,23 +48,19 @@
*/
public QueryTaskConfig getConfigForNextRun(int queryMode) {
final int newQueryIndex = queryIndex + 1;
- int newTransactionId = transactionId + 1;
- if (newTransactionId > UNSIGNED_SHORT_MAX_VALUE) {
- newTransactionId = 1;
- }
-
- return new QueryTaskConfig(queryMode, newQueryIndex, newTransactionId,
- getExpectUnicastResponse(newQueryIndex, queryMode));
+ return new QueryTaskConfig(queryMode, newQueryIndex);
}
- /**
- * Determine if the query backoff should be used.
- */
- public boolean shouldUseQueryBackoff(int numOfQueriesBeforeBackoff) {
- // Don't enable backoff mode during the burst or in the first burst
- if (!isFirstQueryInBurst(queryIndex, queryMode) || isFirstBurst(queryIndex, queryMode)) {
- return false;
+ public int getTransactionId() {
+ return (queryIndex % (UNSIGNED_SHORT_MAX_VALUE - 1)) + 1;
+ }
+
+ private boolean getExpectUnicastResponse() {
+ if (queryMode == AGGRESSIVE_QUERY_MODE) {
+ if (MdnsQueryScheduler.isFirstQueryInBurst(queryIndex, queryMode)) {
+ return true;
+ }
}
- return queryIndex > numOfQueriesBeforeBackoff;
+ return queryIndex == 0 || alwaysAskForUnicastResponse;
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index 41b15dd..282ca9a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -28,6 +28,7 @@
import android.util.ArraySet;
import android.util.Pair;
+import com.android.net.module.util.CollectionUtils;
import com.android.server.connectivity.mdns.MdnsConstants;
import com.android.server.connectivity.mdns.MdnsInetAddressRecord;
import com.android.server.connectivity.mdns.MdnsPacket;
@@ -273,11 +274,8 @@
* of ["_printer", "_sub", "_http", "_tcp"].
*/
public static String[] constructFullSubtype(String[] serviceType, String subtype) {
- String[] fullSubtype = new String[serviceType.length + 2];
- fullSubtype[0] = subtype;
- fullSubtype[1] = MdnsConstants.SUBTYPE_LABEL;
- System.arraycopy(serviceType, 0, fullSubtype, 2, serviceType.length);
- return fullSubtype;
+ return CollectionUtils.prependArray(String.class, serviceType, subtype,
+ MdnsConstants.SUBTYPE_LABEL);
}
/** A wrapper class of {@link SystemClock} to be mocked in unit tests. */
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index b8689d6..21b9b1d 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -317,6 +317,6 @@
@Override
public List<String> getInterfaceList() {
PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG);
- return mTracker.getInterfaceList();
+ return mTracker.getEthernetInterfaceList();
}
}
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 67d0891..4a9410e 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -21,6 +21,7 @@
import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+import static com.android.net.module.util.netlink.NetlinkConstants.IFF_UP;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -40,7 +41,6 @@
import android.os.Handler;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
-import android.os.ServiceSpecificException;
import android.system.OsConstants;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -56,13 +56,17 @@
import com.android.net.module.util.ip.NetlinkMonitor;
import com.android.net.module.util.netlink.NetlinkConstants;
import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.net.module.util.netlink.RtNetlinkLinkMessage;
import com.android.net.module.util.netlink.StructIfinfoMsg;
import com.android.server.connectivity.ConnectivityResources;
import java.io.FileDescriptor;
import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
import java.util.ArrayList;
+import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
@@ -105,7 +109,7 @@
/**
* Track test interfaces if true, don't track otherwise.
- * Volatile is needed as getInterfaceList() does not run on the handler thread.
+ * Volatile is needed as getEthernetInterfaceList() does not run on the handler thread.
*/
private volatile boolean mIncludeTestInterfaces = false;
@@ -139,8 +143,8 @@
private int mTetheringInterfaceMode = INTERFACE_MODE_CLIENT;
// Tracks whether clients were notified that the tethered interface is available
private boolean mTetheredInterfaceWasAvailable = false;
-
- private int mEthernetState = ETHERNET_STATE_ENABLED;
+ // Tracks the current state of ethernet as configured by EthernetManager#setEthernetEnabled.
+ private boolean mIsEthernetEnabled = true;
private class TetheredInterfaceRequestList extends
RemoteCallbackList<ITetheredInterfaceCallback> {
@@ -215,9 +219,6 @@
// Note: processNetlinkMessage is called on the handler thread.
@Override
protected void processNetlinkMessage(NetlinkMessage nlMsg, long whenMs) {
- // ignore all updates when ethernet is disabled.
- if (mEthernetState == ETHERNET_STATE_DISABLED) return;
-
if (nlMsg instanceof RtNetlinkLinkMessage) {
processRtNetlinkLinkMessage((RtNetlinkLinkMessage) nlMsg);
} else {
@@ -401,26 +402,27 @@
return mFactory.getAvailableInterfaces(includeRestricted);
}
- List<String> getInterfaceList() {
+ List<String> getEthernetInterfaceList() {
final List<String> interfaceList = new ArrayList<String>();
- final String[] ifaces;
+ final Enumeration<NetworkInterface> ifaces;
try {
- ifaces = mNetd.interfaceGetList();
- } catch (RemoteException e) {
- Log.e(TAG, "Could not get list of interfaces " + e);
+ ifaces = NetworkInterface.getNetworkInterfaces();
+ } catch (SocketException e) {
+ Log.e(TAG, "Failed to get ethernet interfaces: ", e);
return interfaceList;
}
// There is a possible race with setIncludeTestInterfaces() which can affect
// isValidEthernetInterface (it returns true for test interfaces if setIncludeTestInterfaces
// is set to true).
- // setIncludeTestInterfaces() is only used in tests, and since getInterfaceList() does not
- // run on the handler thread, the behavior around setIncludeTestInterfaces() is
+ // setIncludeTestInterfaces() is only used in tests, and since getEthernetInterfaceList()
+ // does not run on the handler thread, the behavior around setIncludeTestInterfaces() is
// indeterminate either way. This can easily be circumvented by waiting on a callback from
// a test interface after calling setIncludeTestInterfaces() before calling this function.
// In production code, this has no effect.
- for (String iface : ifaces) {
- if (isValidEthernetInterface(iface)) interfaceList.add(iface);
+ while (ifaces.hasMoreElements()) {
+ NetworkInterface iface = ifaces.nextElement();
+ if (isValidEthernetInterface(iface.getName())) interfaceList.add(iface.getName());
}
return interfaceList;
}
@@ -447,7 +449,7 @@
unicastInterfaceStateChange(listener, mTetheringInterface);
}
- unicastEthernetStateChange(listener, mEthernetState);
+ unicastEthernetStateChange(listener, mIsEthernetEnabled);
});
}
@@ -592,15 +594,11 @@
InterfaceConfigurationParcel config = null;
// Bring up the interface so we get link status indications.
try {
- PermissionUtils.enforceNetworkStackPermission(mContext);
// Read the flags before attempting to bring up the interface. If the interface is
// already running an UP event is created after adding the interface.
config = NetdUtils.getInterfaceConfigParcel(mNetd, iface);
- if (NetdUtils.hasFlag(config, INetd.IF_STATE_DOWN)) {
- // As a side-effect, NetdUtils#setInterfaceUp() also clears the interface's IPv4
- // address and readds it which *could* lead to unexpected behavior in the future.
- NetdUtils.setInterfaceUp(mNetd, iface);
- }
+ // Only bring the interface up when ethernet is enabled, otherwise set interface down.
+ setInterfaceUpState(iface, mIsEthernetEnabled);
} catch (IllegalStateException e) {
// Either the system is crashing or the interface has disappeared. Just ignore the
// error; we haven't modified any state because we only do that if our calls succeed.
@@ -646,6 +644,10 @@
}
private void setInterfaceAdministrativeState(String iface, boolean up, EthernetCallback cb) {
+ if (!mIsEthernetEnabled) {
+ cb.onError("Cannot enable/disable interface when ethernet is disabled");
+ return;
+ }
if (getInterfaceState(iface) == EthernetManager.STATE_ABSENT) {
cb.onError("Failed to enable/disable absent interface: " + iface);
return;
@@ -656,15 +658,7 @@
return;
}
- if (up) {
- // WARNING! setInterfaceUp() clears the IPv4 address and readds it. Calling
- // enableInterface() on an active interface can lead to a provisioning failure which
- // will cause IpClient to be restarted.
- // TODO: use netlink directly rather than calling into netd.
- NetdUtils.setInterfaceUp(mNetd, iface);
- } else {
- NetdUtils.setInterfaceDown(mNetd, iface);
- }
+ setInterfaceUpState(iface, up);
cb.onResult(iface);
}
@@ -703,10 +697,6 @@
}
private void maybeTrackInterface(String iface) {
- if (!isValidEthernetInterface(iface)) {
- return;
- }
-
// If we don't already track this interface, and if this interface matches
// our regex, start tracking it.
if (mFactory.hasInterface(iface) || iface.equals(mTetheringInterface)) {
@@ -726,13 +716,9 @@
}
private void trackAvailableInterfaces() {
- try {
- final String[] ifaces = mNetd.interfaceGetList();
- for (String iface : ifaces) {
- maybeTrackInterface(iface);
- }
- } catch (RemoteException | ServiceSpecificException e) {
- Log.e(TAG, "Could not get list of interfaces " + e);
+ final List<String> ifaces = getEthernetInterfaceList();
+ for (String iface : ifaces) {
+ maybeTrackInterface(iface);
}
}
@@ -960,43 +946,47 @@
@VisibleForTesting(visibility = PACKAGE)
protected void setEthernetEnabled(boolean enabled) {
mHandler.post(() -> {
- int newState = enabled ? ETHERNET_STATE_ENABLED : ETHERNET_STATE_DISABLED;
- if (mEthernetState == newState) return;
+ if (mIsEthernetEnabled == enabled) return;
- mEthernetState = newState;
+ mIsEthernetEnabled = enabled;
- if (enabled) {
- trackAvailableInterfaces();
- } else {
- // TODO: maybe also disable server mode interface as well.
- untrackFactoryInterfaces();
+ // Interface in server mode should also be included.
+ ArrayList<String> interfaces =
+ new ArrayList<>(
+ List.of(mFactory.getAvailableInterfaces(/* includeRestricted */ true)));
+
+ if (mTetheringInterfaceMode == INTERFACE_MODE_SERVER) {
+ interfaces.add(mTetheringInterface);
}
- broadcastEthernetStateChange(mEthernetState);
+
+ for (String iface : interfaces) {
+ setInterfaceUpState(iface, enabled);
+ }
+ broadcastEthernetStateChange(mIsEthernetEnabled);
});
}
- private void untrackFactoryInterfaces() {
- for (String iface : mFactory.getAvailableInterfaces(true /* includeRestricted */)) {
- stopTrackingInterface(iface);
- }
+ private int isEthernetEnabledAsInt(boolean state) {
+ return state ? ETHERNET_STATE_ENABLED : ETHERNET_STATE_DISABLED;
}
private void unicastEthernetStateChange(@NonNull IEthernetServiceListener listener,
- int state) {
+ boolean enabled) {
ensureRunningOnEthernetServiceThread();
try {
- listener.onEthernetStateChanged(state);
+ listener.onEthernetStateChanged(isEthernetEnabledAsInt(enabled));
} catch (RemoteException e) {
// Do nothing here.
}
}
- private void broadcastEthernetStateChange(int state) {
+ private void broadcastEthernetStateChange(boolean enabled) {
ensureRunningOnEthernetServiceThread();
final int n = mListeners.beginBroadcast();
for (int i = 0; i < n; i++) {
try {
- mListeners.getBroadcastItem(i).onEthernetStateChanged(state);
+ mListeners.getBroadcastItem(i)
+ .onEthernetStateChanged(isEthernetEnabledAsInt(enabled));
} catch (RemoteException e) {
// Do nothing here.
}
@@ -1004,11 +994,17 @@
mListeners.finishBroadcast();
}
+ private void setInterfaceUpState(@NonNull String interfaceName, boolean up) {
+ if (!NetlinkUtils.setInterfaceFlags(interfaceName, up ? IFF_UP : ~IFF_UP)) {
+ Log.e(TAG, "Failed to set interface " + interfaceName + (up ? " up" : " down"));
+ }
+ }
+
void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
postAndWaitForRunnable(() -> {
pw.println(getClass().getSimpleName());
pw.println("Ethernet State: "
- + (mEthernetState == ETHERNET_STATE_ENABLED ? "enabled" : "disabled"));
+ + (mIsEthernetEnabled ? "enabled" : "disabled"));
pw.println("Ethernet interface name filter: " + mIfaceMatch);
pw.println("Interface used for tethering: " + mTetheringInterface);
pw.println("Tethering interface mode: " + mTetheringInterfaceMode);
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index fb712a1..a8e3203 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -493,7 +493,8 @@
@Nullable
private final TrafficStatsRateLimitCache mTrafficStatsUidCache;
// A feature flag to control whether the client-side rate limit cache should be enabled.
- static final String TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG =
+ @VisibleForTesting
+ public static final String TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG =
"trafficstats_client_rate_limit_cache_enabled_flag";
static final String TRAFFICSTATS_SERVICE_RATE_LIMIT_CACHE_ENABLED_FLAG =
"trafficstats_rate_limit_cache_enabled_flag";
diff --git a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
index 667aad1..4f99d1b 100644
--- a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
+++ b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
@@ -29,7 +29,10 @@
/**
* A thread-safe cache for storing and retrieving {@link NetworkStats.Entry} objects,
* with an adjustable expiry duration to manage data freshness.
+ *
+ * @deprecated Use {@link LruCacheWithExpiry} instead.
*/
+// TODO: Remove this when service side rate limit cache solution is removed.
class TrafficStatsRateLimitCache extends
LruCacheWithExpiry<TrafficStatsRateLimitCache.TrafficStatsCacheKey, NetworkStats.Entry> {
@@ -41,7 +44,7 @@
* @param maxSize Maximum number of entries.
*/
TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs, int maxSize) {
- super(clock, expiryDurationMs, maxSize, it -> !it.isEmpty());
+ super(()-> clock.millis(), expiryDurationMs, maxSize, it -> !it.isEmpty());
}
public static class TrafficStatsCacheKey {
diff --git a/service/Android.bp b/service/Android.bp
index 567c079..2659ebf 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -90,7 +90,6 @@
static_libs: [
"libnet_utils_device_common_bpfjni",
"libnet_utils_device_common_bpfutils",
- "libnet_utils_device_common_timerfdjni",
],
shared_libs: [
"liblog",
@@ -126,6 +125,7 @@
"libmodules-utils-build",
"libnetjniutils",
"libnet_utils_device_common_bpfjni",
+ "libnet_utils_device_common_timerfdjni",
"netd_aidl_interface-lateststable-ndk",
],
shared_libs: [
@@ -207,6 +207,7 @@
},
visibility: [
"//packages/modules/Connectivity/service-t",
+ "//packages/modules/Connectivity/service-b",
"//packages/modules/Connectivity/networksecurity:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/thread/service:__subpackages__",
@@ -252,7 +253,11 @@
"service-networksecurity-pre-jarjar",
service_remoteauth_pre_jarjar_lib,
"service-thread-pre-jarjar",
- ],
+ ] + select(release_flag("RELEASE_MOVE_VCN_TO_MAINLINE"), {
+ true: ["service-connectivity-b-pre-jarjar"],
+ default: [],
+ }),
+
// The below libraries are not actually needed to build since no source is compiled
// (only combining prebuilt static_libs), but they are necessary so that R8 has the right
// references to optimize the code. Without these, there will be missing class warnings and
@@ -338,6 +343,7 @@
name: "service-connectivity-jarjar-gen",
tool_files: [
":service-connectivity-pre-jarjar{.jar}",
+ ":service-connectivity-b-pre-jarjar{.jar}",
":service-connectivity-tiramisu-pre-jarjar{.jar}",
"jarjar-excludes.txt",
],
@@ -347,6 +353,7 @@
out: ["service_connectivity_jarjar_rules.txt"],
cmd: "$(location jarjar-rules-generator) " +
"$(location :service-connectivity-pre-jarjar{.jar}) " +
+ "$(location :service-connectivity-b-pre-jarjar{.jar}) " +
"$(location :service-connectivity-tiramisu-pre-jarjar{.jar}) " +
"--prefix android.net.connectivity " +
"--excludes $(location jarjar-excludes.txt) " +
diff --git a/service/ServiceConnectivityResources/res/values-sw/strings.xml b/service/ServiceConnectivityResources/res/values-sw/strings.xml
index 29ec013..9ff9ada 100644
--- a/service/ServiceConnectivityResources/res/values-sw/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-sw/strings.xml
@@ -25,7 +25,7 @@
<string name="mobile_network_available_no_internet" msgid="1000871587359324217">"Hakuna intaneti"</string>
<string name="mobile_network_available_no_internet_detailed" msgid="5438738723127062816">"Huenda data ya <xliff:g id="NETWORK_CARRIER">%1$s</xliff:g> imeisha. Gusa ili upate chaguo."</string>
<string name="mobile_network_available_no_internet_detailed_unknown_carrier" msgid="5375681117265354337">"Huenda data yako imeisha. Gusa ili upate chaguo."</string>
- <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> haina uwezo wa kufikia intaneti"</string>
+ <string name="wifi_no_internet" msgid="1326348603404555475">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> haina intaneti"</string>
<string name="wifi_no_internet_detailed" msgid="1746921096565304090">"Gusa ili upate chaguo"</string>
<string name="mobile_no_internet" msgid="4087718456753201450">"Mtandao wa simu hauna uwezo wa kufikia intaneti"</string>
<string name="other_networks_no_internet" msgid="5693932964749676542">"Mtandao hauna uwezo wa kufikia intaneti"</string>
diff --git a/service/jarjar-excludes.txt b/service/jarjar-excludes.txt
index 7bd3862..9076b53 100644
--- a/service/jarjar-excludes.txt
+++ b/service/jarjar-excludes.txt
@@ -1,3 +1,4 @@
# Classes loaded by SystemServer via their hardcoded name, so they can't be jarjared
com\.android\.server\.ConnectivityServiceInitializer(\$.+)?
+com\.android\.server\.ConnectivityServiceInitializerB(\$.+)?
com\.android\.server\.NetworkStatsServiceInitializer(\$.+)?
diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp
index bb70d4f..8e01260 100644
--- a/service/jni/onload.cpp
+++ b/service/jni/onload.cpp
@@ -26,6 +26,8 @@
int register_android_server_net_NetworkStatsFactory(JNIEnv* env);
int register_android_server_net_NetworkStatsService(JNIEnv* env);
int register_com_android_server_ServiceManagerWrapper(JNIEnv* env);
+int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
+ char const *class_name);
extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
JNIEnv *env;
@@ -56,6 +58,12 @@
}
}
+ if (register_com_android_net_module_util_TimerFdUtils(
+ env, "android/net/connectivity/com/android/net/module/util/"
+ "TimerFdUtils") < 0) {
+ return JNI_ERR;
+ }
+
return JNI_VERSION_1_6;
}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 0d0f6fc..18801f0 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -48,6 +48,7 @@
import static android.net.ConnectivityManager.CALLBACK_LOSING;
import static android.net.ConnectivityManager.CALLBACK_LOST;
import static android.net.ConnectivityManager.CALLBACK_PRECHECK;
+import static android.net.ConnectivityManager.CALLBACK_RESERVED;
import static android.net.ConnectivityManager.CALLBACK_RESUMED;
import static android.net.ConnectivityManager.CALLBACK_SUSPENDED;
import static android.net.ConnectivityManager.CALLBACK_UNAVAIL;
@@ -108,6 +109,8 @@
import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
+import static android.net.NetworkCapabilities.RES_ID_UNSET;
+import static android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
@@ -121,7 +124,9 @@
import static android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION;
import static android.os.Process.INVALID_UID;
import static android.os.Process.VPN_UID;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+import static android.system.OsConstants.ENOENT;
+import static android.system.OsConstants.ENOTCONN;
+import static android.system.OsConstants.EOPNOTSUPP;
import static android.system.OsConstants.ETH_P_ALL;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
@@ -149,6 +154,7 @@
import static com.android.server.connectivity.ConnectivityFlags.CELLULAR_DATA_INACTIVITY_TIMEOUT;
import static com.android.server.connectivity.ConnectivityFlags.DELAY_DESTROY_SOCKETS;
import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
+import static com.android.server.connectivity.ConnectivityFlags.NAMESPACE_TETHERING_BOOT;
import static com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS;
import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
import static com.android.server.connectivity.ConnectivityFlags.WIFI_DATA_INACTIVITY_TIMEOUT;
@@ -195,6 +201,7 @@
import android.net.IConnectivityDiagnosticsCallback;
import android.net.IConnectivityManager;
import android.net.IDnsResolver;
+import android.net.IIntResultListener;
import android.net.INetd;
import android.net.INetworkActivityListener;
import android.net.INetworkAgent;
@@ -1615,13 +1622,13 @@
/** Returns the data inactivity timeout to be used for cellular networks */
public int getDefaultCellularDataInactivityTimeout() {
- return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING,
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING_BOOT,
CELLULAR_DATA_INACTIVITY_TIMEOUT, 10);
}
/** Returns the data inactivity timeout to be used for WiFi networks */
public int getDefaultWifiDataInactivityTimeout() {
- return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING,
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING_BOOT,
WIFI_DATA_INACTIVITY_TIMEOUT, 15);
}
@@ -6340,8 +6347,20 @@
}
}
- private class CaptivePortalImpl extends ICaptivePortal.Stub {
+ public class CaptivePortalImpl extends ICaptivePortal.Stub implements IBinder.DeathRecipient {
private final Network mNetwork;
+ // Binder object to track the lifetime of the setDelegateUid caller for cleanup purposes.
+ //
+ // Note that in theory it can happen that there are multiple callers for a given
+ // object. For example, the app that receives the CaptivePortal object from the Intent
+ // fired by startCaptivePortalAppInternal could send the object to another process, or
+ // clone it. Only the first of these objects that calls setDelegateUid will properly
+ // register a death recipient. Calls from the other objects will work, but only the
+ // first object's death will cause the death recipient to fire.
+ // TODO: track all callers by callerBinder instead of CaptivePortalImpl, store callerBinder
+ // in a Set. When the death recipient fires, we can remove the callingBinder from the set,
+ // and when the set is empty, we can clear the delegated UID.
+ private IBinder mDelegateUidCaller;
private CaptivePortalImpl(Network network) {
mNetwork = network;
@@ -6381,6 +6400,55 @@
}
}
+ private int handleSetDelegateUid(int uid, @NonNull final IBinder callerBinder) {
+ if (mDelegateUidCaller == null) {
+ mDelegateUidCaller = callerBinder;
+ try {
+ // While technically unnecessary, it is safe to register a DeathRecipient for
+ // a cleanup operation (where uid = INVALID_UID).
+ mDelegateUidCaller.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ // remote has died, return early.
+ return ENOTCONN;
+ }
+ }
+
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(mNetwork);
+ if (nai == null) return ENOENT; // network does not exist anymore.
+ if (nai.isDestroyed()) return ENOENT; // network has already been destroyed.
+
+ // TODO: consider allowing the uid to bypass VPN on all networks before V.
+ if (!mDeps.isAtLeastV()) return EOPNOTSUPP;
+
+ // Check whether there has already been a delegate UID configured, if so, perform
+ // cleanup and disallow bypassing VPN for that UID if no other caller is delegating
+ // this UID.
+ // TODO: consider using exceptions instead of errnos.
+ final int errno = nai.removeCaptivePortalDelegateUid(this);
+ if (errno != 0) return errno;
+
+ // If uid == INVALID_UID, we are done.
+ if (uid == INVALID_UID) return 0;
+ return nai.setCaptivePortalDelegateUid(this, uid);
+ }
+
+ @Override
+ public void setDelegateUid(int uid, @NonNull final IBinder callerBinder,
+ @NonNull final IIntResultListener listener) {
+ Objects.requireNonNull(callerBinder);
+ Objects.requireNonNull(listener);
+ enforceAnyPermissionOf(mContext, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+
+ mHandler.post(() -> {
+ final int errno = handleSetDelegateUid(uid, callerBinder);
+ try {
+ listener.onResult(errno);
+ } catch (RemoteException e) {
+ // remote has died, nothing to do.
+ }
+ });
+ }
+
@Nullable
private NetworkMonitorManager getNetworkMonitorManager(final Network network) {
// getNetworkAgentInfoForNetwork is thread-safe
@@ -6390,6 +6458,13 @@
// nai.networkMonitor() is thread-safe
return nai.networkMonitor();
}
+
+ @Override
+ public void binderDied() {
+ // Cleanup invalid UID and restore the VPN bypass rule. Because mDelegateUidCaller is
+ // never reset, it cannot be null in this context.
+ mHandler.post(() -> handleSetDelegateUid(INVALID_UID, mDelegateUidCaller));
+ }
}
public boolean avoidBadWifi() {
@@ -6691,7 +6766,7 @@
final NetworkOfferInfo offer =
findNetworkOfferInfoByCallback((INetworkOfferCallback) msg.obj);
if (null != offer) {
- handleUnregisterNetworkOffer(offer);
+ handleUnregisterNetworkOffer(offer, true /* releaseReservations */);
}
break;
}
@@ -7608,17 +7683,23 @@
}
}
- private void ensureAllNetworkRequestsHaveType(List<NetworkRequest> requests) {
+ private void ensureAllNetworkRequestsHaveSupportedType(List<NetworkRequest> requests) {
+ final boolean isMultilayerRequest = requests.size() > 1;
for (int i = 0; i < requests.size(); i++) {
- ensureNetworkRequestHasType(requests.get(i));
+ ensureNetworkRequestHasSupportedType(requests.get(i), isMultilayerRequest);
}
}
- private void ensureNetworkRequestHasType(NetworkRequest request) {
+ private void ensureNetworkRequestHasSupportedType(NetworkRequest request,
+ boolean isMultilayerRequest) {
if (request.type == NetworkRequest.Type.NONE) {
throw new IllegalArgumentException(
"All NetworkRequests in ConnectivityService must have a type");
}
+ if (isMultilayerRequest && request.type == NetworkRequest.Type.RESERVATION) {
+ throw new IllegalArgumentException(
+ "Reservation requests are not supported in multilayer request");
+ }
}
/**
@@ -7729,6 +7810,28 @@
}
/**
+ * NetworkCapabilities that were created as part of a NetworkOffer in response to a
+ * RESERVATION request. mReservedCapabilities is null if no current offer matches the
+ * RESERVATION request or if the request is not a RESERVATION. Matching is based on
+ * reservationId.
+ */
+ @Nullable
+ private NetworkCapabilities mReservedCapabilities;
+ @Nullable
+ NetworkCapabilities getReservedCapabilities() {
+ return mReservedCapabilities;
+ }
+
+ void setReservedCapabilities(@NonNull NetworkCapabilities caps) {
+ // This function can only be called once. NetworkCapabilities are never reset as the
+ // reservation is released when the offer disappears.
+ if (mReservedCapabilities != null) {
+ logwtf("ReservedCapabilities can only be set once");
+ }
+ mReservedCapabilities = caps;
+ }
+
+ /**
* Get the list of UIDs this nri applies to.
*/
@NonNull
@@ -7748,7 +7851,7 @@
NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r,
@NonNull final NetworkRequest requestForCallback, @Nullable final PendingIntent pi,
@Nullable String callingAttributionTag, final int preferenceOrder) {
- ensureAllNetworkRequestsHaveType(r);
+ ensureAllNetworkRequestsHaveSupportedType(r);
mRequests = initializeRequests(r);
mNetworkRequestForCallback = requestForCallback;
mPendingIntent = pi;
@@ -7782,7 +7885,7 @@
@NetworkCallback.Flag int callbackFlags,
@Nullable String callingAttributionTag, int declaredMethodsFlags) {
super();
- ensureAllNetworkRequestsHaveType(r);
+ ensureAllNetworkRequestsHaveSupportedType(r);
mRequests = initializeRequests(r);
mNetworkRequestForCallback = requestForCallback;
mMessenger = m;
@@ -7802,7 +7905,7 @@
NetworkRequestInfo(@NonNull final NetworkRequestInfo nri,
@NonNull final List<NetworkRequest> r) {
super();
- ensureAllNetworkRequestsHaveType(r);
+ ensureAllNetworkRequestsHaveSupportedType(r);
mRequests = initializeRequests(r);
mNetworkRequestForCallback = nri.getNetworkRequestForCallback();
final NetworkAgentInfo satisfier = nri.getSatisfier();
@@ -8088,6 +8191,14 @@
return PREFERENCE_ORDER_NONE;
}
+ public int getReservationId() {
+ // RESERVATIONs cannot be used in multilayer requests.
+ if (isMultilayerRequest()) return RES_ID_UNSET;
+ final NetworkRequest req = mRequests.get(0);
+ // Non-reservation types return RES_ID_UNSET.
+ return req.networkCapabilities.getReservationId();
+ }
+
@Override
public void binderDied() {
// As an immutable collection, mRequests cannot change by the time the
@@ -8139,6 +8250,7 @@
flags = maybeAppendDeclaredMethod(flags, CALLBACK_BLK_CHANGED, "BLK", sb);
flags = maybeAppendDeclaredMethod(flags, CALLBACK_LOCAL_NETWORK_INFO_CHANGED,
"LOCALINF", sb);
+ flags = maybeAppendDeclaredMethod(flags, CALLBACK_RESERVED, "RES", sb);
if (flags != 0) {
sb.append("|0x").append(Integer.toHexString(flags));
}
@@ -8343,6 +8455,7 @@
enforceNetworkStackOrSettingsPermission();
// Fall-through since other checks are the same with normal requests.
case REQUEST:
+ case RESERVATION:
networkCapabilities = new NetworkCapabilities(networkCapabilities);
enforceNetworkRequestPermissions(networkCapabilities, callingPackageName,
callingAttributionTag, callingUid);
@@ -8781,7 +8894,7 @@
@Override
public void releaseNetworkRequest(NetworkRequest networkRequest) {
- ensureNetworkRequestHasType(networkRequest);
+ ensureNetworkRequestHasSupportedType(networkRequest, false /* isMultilayerRequest */);
mHandler.sendMessage(mHandler.obtainMessage(
EVENT_RELEASE_NETWORK_REQUEST, mDeps.getCallingUid(), 0, networkRequest));
}
@@ -8824,6 +8937,11 @@
Objects.requireNonNull(score);
Objects.requireNonNull(caps);
Objects.requireNonNull(callback);
+ if (caps.hasTransport(TRANSPORT_TEST)) {
+ enforceAnyPermissionOf(mContext, Manifest.permission.MANAGE_TEST_NETWORKS);
+ } else {
+ enforceNetworkFactoryPermission();
+ }
final boolean yieldToBadWiFi = caps.hasTransport(TRANSPORT_CELLULAR) && !avoidBadWifi();
final NetworkOffer offer = new NetworkOffer(
FullScore.makeProspectiveScore(score, caps, yieldToBadWiFi),
@@ -8862,7 +8980,7 @@
}
}
for (final NetworkOfferInfo noi : toRemove) {
- handleUnregisterNetworkOffer(noi);
+ handleUnregisterNetworkOffer(noi, true /* releaseReservations */);
}
if (DBG) log("unregisterNetworkProvider for " + npi.name);
}
@@ -9295,7 +9413,7 @@
@Override
public void binderDied() {
- mHandler.post(() -> handleUnregisterNetworkOffer(this));
+ mHandler.post(() -> handleUnregisterNetworkOffer(this, true /* releaseReservations */));
}
}
@@ -9306,6 +9424,18 @@
return false;
}
+ @Nullable
+ private NetworkRequestInfo maybeGetNriForReservedOffer(NetworkOfferInfo noi) {
+ final int reservationId = noi.offer.caps.getReservationId();
+ if (reservationId == RES_ID_UNSET) return null; // not a reserved offer.
+
+ for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+ if (reservationId == nri.getReservationId()) return nri;
+ }
+ // The reservation was withdrawn or the reserving process died.
+ return null;
+ }
+
/**
* Register or update a network offer.
* @param newOffer The new offer. If the callback member is the same as an existing
@@ -9322,19 +9452,62 @@
return;
}
final NetworkOfferInfo existingOffer = findNetworkOfferInfoByCallback(newOffer.callback);
+
+ // If a reserved offer is updated, ensure the capabilities are not changed. This ensures
+ // that the reserved offer's capabilities match the ones passed by the onReserved callback,
+ // which is sent only once.
+ //
+ // TODO: consider letting the provider change the capabilities of an offer as long as they
+ // continue to satisfy the capabilities that were passed to onReserved. This is not needed
+ // today, but it shouldn't violate the API contract:
+ // - NetworkOffer capabilities are not promises
+ // - The app making a reservation must never assume that the capabilities of the reserved
+ // network are equal to the ones that were passed to onReserved. There will almost always be
+ // other capabilities, for example, those that change at runtime such as VALIDATED or
+ // NOT_SUSPENDED.
+ if (null != existingOffer
+ && existingOffer.offer.caps.getReservationId() != RES_ID_UNSET
+ && existingOffer.offer.caps.getReservationId() != RES_ID_MATCH_ALL_RESERVATIONS
+ && !newOffer.caps.equals(existingOffer.offer.caps)) {
+ // Reserved offers are not allowed to update their NetworkCapabilities.
+ // Doing so will immediately remove the offer from CS and send onUnavailable to the app.
+ handleUnregisterNetworkOffer(existingOffer, true /* releaseReservations */);
+ existingOffer.offer.notifyUnneeded();
+ logwtf("Reserved offers must never update their reserved NetworkCapabilities");
+ return;
+ }
+
+ final NetworkOfferInfo noi = new NetworkOfferInfo(newOffer);
if (null != existingOffer) {
- handleUnregisterNetworkOffer(existingOffer);
+ // Do not send onUnavailable for a reserved offer when updating it.
+ handleUnregisterNetworkOffer(existingOffer, false /* releaseReservations */);
newOffer.migrateFrom(existingOffer.offer);
if (DBG) {
// handleUnregisterNetworkOffer has already logged the old offer
log("update offer from providerId " + newOffer.providerId + " new : " + newOffer);
}
} else {
+ final NetworkRequestInfo reservationNri = maybeGetNriForReservedOffer(noi);
+ if (reservationNri != null) {
+ // A NetworkRequest is only allowed to trigger a single reserved offer (and
+ // onReserved() callback). All subsequent offers are ignored. This either indicates
+ // a bug in the provider (e.g., responding twice to the same reservation, or
+ // updating the capabilities of a reserved offer), or multiple providers responding
+ // to the same offer (which could happen, but is not useful to the requesting app).
+ if (reservationNri.getReservedCapabilities() != null) {
+ loge("A reservation can only trigger a single offer; new offer is ignored.");
+ return;
+ }
+ // Always update the reserved offer before calling callCallbackForRequest.
+ reservationNri.setReservedCapabilities(noi.offer.caps);
+ callCallbackForRequest(
+ reservationNri, null /*networkAgent*/, CALLBACK_RESERVED, 0 /*arg1*/);
+ }
if (DBG) {
log("register offer from providerId " + newOffer.providerId + " : " + newOffer);
}
}
- final NetworkOfferInfo noi = new NetworkOfferInfo(newOffer);
+
try {
noi.offer.callback.asBinder().linkToDeath(noi, 0 /* flags */);
} catch (RemoteException e) {
@@ -9345,7 +9518,8 @@
issueNetworkNeeds(noi);
}
- private void handleUnregisterNetworkOffer(@NonNull final NetworkOfferInfo noi) {
+ private void handleUnregisterNetworkOffer(@NonNull final NetworkOfferInfo noi,
+ boolean releaseReservations) {
ensureRunningOnConnectivityServiceThread();
if (DBG) {
log("unregister offer from providerId " + noi.offer.providerId + " : " + noi.offer);
@@ -9355,6 +9529,18 @@
// function may be called twice in a row, but the array will no longer contain
// the offer.
if (!mNetworkOffers.remove(noi)) return;
+
+ // If the offer was brought up as a result of a reservation, inform the RESERVATION request
+ // that it has disappeared. There is no need to reset nri.mReservedCapabilities to null, as
+ // CALLBACK_UNAVAIL will cause the request to be torn down. In addition, leaving
+ // nri.mReservedOffer set prevents an additional onReserved() callback in
+ // handleRegisterNetworkOffer() in the case of a migration (which would be ignored as it
+ // follows an onUnavailable).
+ final NetworkRequestInfo nri = maybeGetNriForReservedOffer(noi);
+ if (releaseReservations && nri != null) {
+ handleRemoveNetworkRequest(nri);
+ callCallbackForRequest(nri, null /* networkAgent */, CALLBACK_UNAVAIL, 0 /* arg1 */);
+ }
noi.offer.callback.asBinder().unlinkToDeath(noi, 0 /* flags */);
}
@@ -10573,9 +10759,9 @@
return bundle;
}
- // networkAgent is only allowed to be null if notificationType is
- // CALLBACK_UNAVAIL. This is because UNAVAIL is about no network being
- // available, while all other cases are about some particular network.
+ // networkAgent is only allowed to be null if notificationType is CALLBACK_UNAVAIL or
+ // CALLBACK_RESERVED. This is because, per definition, no network is available for UNAVAIL, and
+ // RESERVED callbacks happen when a NetworkOffer is created in response to a reservation.
private void callCallbackForRequest(@NonNull final NetworkRequestInfo nri,
@Nullable final NetworkAgentInfo networkAgent, final int notificationType,
final int arg1) {
@@ -10587,6 +10773,10 @@
}
// Even if a callback ends up not being sent, it may affect other callbacks in the queue, so
// queue callbacks before checking the declared methods flags.
+ // UNAVAIL and RESERVED callbacks are safe not to be queued, because RESERVED must always be
+ // the first callback. In addition, RESERVED cannot be sent more than once and is only
+ // cancelled by UNVAIL.
+ // TODO: evaluate whether it makes sense to queue RESERVED callbacks.
if (networkAgent != null && nri.maybeQueueCallback(networkAgent, notificationType)) {
return;
}
@@ -10594,14 +10784,24 @@
// No need to send the notification as the recipient method is not overridden
return;
}
- final Network bundleNetwork = notificationType == CALLBACK_UNAVAIL
- ? null
- : networkAgent.network;
+ // networkAgent is only null for UNAVAIL and RESERVED.
+ final Network bundleNetwork = (networkAgent != null) ? networkAgent.network : null;
final Bundle bundle = makeCommonBundleForCallback(nri, bundleNetwork);
final boolean includeLocationSensitiveInfo =
(nri.mCallbackFlags & NetworkCallback.FLAG_INCLUDE_LOCATION_INFO) != 0;
final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback();
switch (notificationType) {
+ case CALLBACK_RESERVED: {
+ final NetworkCapabilities nc =
+ createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+ networkCapabilitiesRestrictedForCallerPermissions(
+ nri.getReservedCapabilities(), nri.mPid, nri.mUid),
+ includeLocationSensitiveInfo, nri.mPid, nri.mUid,
+ nrForCallback.getRequestorPackageName(),
+ nri.mCallingAttributionTag);
+ putParcelable(bundle, nc);
+ break;
+ }
case CALLBACK_AVAILABLE: {
final NetworkCapabilities nc =
createWithLocationInfoSanitizedIfNecessaryWhenParceled(
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index 93335f1..136ea81 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -26,6 +26,11 @@
*/
public final class ConnectivityFlags {
/**
+ * Boot namespace for this module. Values from this should only be read at boot.
+ */
+ public static final String NAMESPACE_TETHERING_BOOT = "tethering_boot";
+
+ /**
* Minimum module version at which to avoid rematching all requests when a network request is
* registered, and rematch only the registered requests instead.
*/
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 94b655f..2686e4a 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -25,6 +25,12 @@
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkCapabilities.transportNamesOf;
+import static android.system.OsConstants.EIO;
+import static android.system.OsConstants.EEXIST;
+import static android.system.OsConstants.ENOENT;
+
+import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED;
+import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_DISALLOW_BYPASS_VPN_FOR_DELEGATE_UID_ENOENT;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -57,9 +63,11 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
+import android.os.ServiceSpecificException;
import android.os.SystemClock;
import android.telephony.data.EpsBearerQosSessionAttributes;
import android.telephony.data.NrQosSessionAttributes;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
@@ -68,8 +76,10 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.WakeupMessage;
+import com.android.net.module.util.FrameworkConnectivityStatsLog;
import com.android.net.module.util.HandlerUtils;
import com.android.server.ConnectivityService;
+import com.android.server.ConnectivityService.CaptivePortalImpl;
import java.io.PrintWriter;
import java.net.Inet4Address;
@@ -574,6 +584,10 @@
// For fast lookups. Indexes into mInactivityTimers by request ID.
private final SparseArray<InactivityTimer> mInactivityTimerForRequest = new SparseArray<>();
+ // Map of delegated UIDs used to bypass VPN and its captive portal app caller.
+ private final ArrayMap<CaptivePortalImpl, Integer> mCaptivePortalDelegateUids =
+ new ArrayMap<>();
+
// Inactivity expiry timer. Armed whenever mInactivityTimers is non-empty, regardless of
// whether the network is inactive or not. Always set to the expiry of the mInactivityTimers
// that expires last. When the timer fires, all inactivity state is cleared, and if the network
@@ -626,6 +640,7 @@
private final Context mContext;
private final Handler mHandler;
private final QosCallbackTracker mQosCallbackTracker;
+ private final INetd mNetd;
private final long mCreationTime;
@@ -655,6 +670,7 @@
mConnServiceDeps = deps;
setScore(score); // uses members connService, networkCapabilities and networkAgentConfig
clatd = new Nat464Xlat(this, netd, dnsResolver, deps);
+ mNetd = netd;
mContext = context;
mHandler = handler;
this.factorySerialNumber = factorySerialNumber;
@@ -1549,6 +1565,58 @@
}
}
+ private int allowBypassVpnOnNetwork(boolean allow, int uid, int netId) {
+ try {
+ mNetd.networkAllowBypassVpnOnNetwork(allow, uid, netId);
+ return 0;
+ } catch (RemoteException e) {
+ // Netd has crashed, and this process is about to crash as well.
+ return EIO;
+ } catch (ServiceSpecificException e) {
+ return e.errorCode;
+ }
+ }
+
+ /**
+ * Set the delegate UID of the app that is allowed to perform network traffic for captive
+ * portal login, and configure the netd bypass rule with this delegated UID.
+ *
+ * @param caller the captive portal app to that delegated UID
+ * @param uid the delegated UID of the captive portal app.
+ * @return Return 0 if set the UID and VPN bypass rule successfully or bypass rule corresponding
+ * to this UID already exists otherwise return errno.
+ */
+ public int setCaptivePortalDelegateUid(@NonNull final CaptivePortalImpl caller, int uid) {
+ final int errorCode = allowBypassVpnOnNetwork(true /* allow */, uid, network.netId);
+ if (errorCode == 0 || errorCode == EEXIST) {
+ mCaptivePortalDelegateUids.put(caller, uid);
+ }
+ return errorCode == EEXIST ? 0 : errorCode;
+ }
+
+ /**
+ * Remove the delegate UID of the app that is allowed to perform network traffic for captive
+ * portal login, and remove the netd bypass rule if no other caller is delegating this UID.
+ *
+ * @param caller the captive portal app to that delegated UID.
+ * @return Return 0 if remove the UID and VPN bypass rule successfully or bypass rule
+ * corresponding to this UID doesn't exist otherwise return errno.
+ */
+ public int removeCaptivePortalDelegateUid(@NonNull final CaptivePortalImpl caller) {
+ final Integer maybeDelegateUid = mCaptivePortalDelegateUids.remove(caller);
+ if (maybeDelegateUid == null) return 0;
+ if (mCaptivePortalDelegateUids.values().contains(maybeDelegateUid)) return 0;
+ final int errorCode =
+ allowBypassVpnOnNetwork(false /* allow */, maybeDelegateUid, network.netId);
+ if (errorCode == ENOENT) {
+ FrameworkConnectivityStatsLog.write(
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_DISALLOW_BYPASS_VPN_FOR_DELEGATE_UID_ENOENT
+ );
+ }
+ return errorCode == ENOENT ? 0 : errorCode;
+ }
+
private static boolean areAllowedUidsAcceptableFromNetworkAgent(
@NonNull final NetworkCapabilities nc, final boolean hasAutomotiveFeature,
@NonNull final ConnectivityService.Dependencies deps,
diff --git a/service/src/com/android/server/connectivity/NetworkOffer.java b/service/src/com/android/server/connectivity/NetworkOffer.java
index eea382e..d294046 100644
--- a/service/src/com/android/server/connectivity/NetworkOffer.java
+++ b/service/src/com/android/server/connectivity/NetworkOffer.java
@@ -42,6 +42,7 @@
* @hide
*/
public class NetworkOffer implements NetworkRanker.Scoreable {
+ private static final String TAG = NetworkOffer.class.getSimpleName();
@NonNull public final FullScore score;
@NonNull public final NetworkCapabilities caps;
@NonNull public final INetworkOfferCallback callback;
@@ -126,6 +127,23 @@
}
/**
+ * Sends onNetworkUnneeded for any remaining NetworkRequests.
+ *
+ * Used after a NetworkOffer migration failed to let the provider know that its networks should
+ * be torn down (as the offer is no longer registered).
+ */
+ public void notifyUnneeded() {
+ try {
+ for (NetworkRequest request : mCurrentlyNeeded) {
+ callback.onNetworkUnneeded(request);
+ }
+ } catch (RemoteException e) {
+ // The remote is dead; nothing to do.
+ }
+ mCurrentlyNeeded.clear();
+ }
+
+ /**
* Migrate from, and take over, a previous offer.
*
* When an updated offer is sent from a provider, call this method on the new offer, passing
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index f484027..b4a3b8a 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -32,6 +32,8 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
+// This library shouldn't be used anymore (no class should be added), and per-user libraries like
+// net-utils-service-connectivity or net-utils-framework-wifi should be used instead.
java_library {
name: "net-utils-device-common",
srcs: [
@@ -725,3 +727,34 @@
],
apex_available: ["com.android.wifi"],
}
+
+genrule {
+ name: "statslog-framework-connectivity-java-gen",
+ tools: ["stats-log-api-gen"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module connectivity --javaPackage com.android.net.module.util --javaClass FrameworkConnectivityStatsLog",
+ out: ["com/android/net/module/util/FrameworkConnectivityStatsLog.java"],
+}
+
+java_library {
+ name: "net-utils-service-vcn",
+ sdk_version: "module_current",
+ min_sdk_version: "30",
+ srcs: [
+ "device/com/android/net/module/util/HandlerUtils.java",
+ ],
+ libs: [
+ "framework-annotations-lib",
+ ],
+ visibility: [
+ // TODO: b/374174952 Remove it when VCN modularization is released
+ "//frameworks/base/packages/Vcn/service-b",
+
+ "//packages/modules/Connectivity/service-b",
+ ],
+ apex_available: [
+ // TODO: b/374174952 Remove it when VCN modularization is released
+ "//apex_available:platform",
+
+ "com.android.tethering",
+ ],
+}
diff --git a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
index d99eedc..8b2fe58 100644
--- a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
@@ -149,21 +149,21 @@
}
/** Setup interface for tethering. */
- public static void tetherInterface(final INetd netd, final String iface, final IpPrefix dest)
- throws RemoteException, ServiceSpecificException {
- tetherInterface(netd, iface, dest, 20 /* maxAttempts */, 50 /* pollingIntervalMs */);
+ public static void tetherInterface(final INetd netd, int netId, final String iface,
+ final IpPrefix dest) throws RemoteException, ServiceSpecificException {
+ tetherInterface(netd, netId, iface, dest, 20 /* maxAttempts */, 50 /* pollingIntervalMs */);
}
/** Setup interface with configurable retries for tethering. */
- public static void tetherInterface(final INetd netd, final String iface, final IpPrefix dest,
- int maxAttempts, int pollingIntervalMs)
+ public static void tetherInterface(final INetd netd, int netId, final String iface,
+ final IpPrefix dest, int maxAttempts, int pollingIntervalMs)
throws RemoteException, ServiceSpecificException {
netd.tetherInterfaceAdd(iface);
- networkAddInterface(netd, iface, maxAttempts, pollingIntervalMs);
+ networkAddInterface(netd, netId, iface, maxAttempts, pollingIntervalMs);
// Activate a route to dest and IPv6 link local.
- modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+ modifyRoute(netd, ModifyOperation.ADD, netId,
new RouteInfo(dest, null, iface, RTN_UNICAST));
- modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+ modifyRoute(netd, ModifyOperation.ADD, netId,
new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
}
@@ -174,12 +174,12 @@
* in use in netd because the ConnectivityService thread hasn't processed the disconnect yet.
* See b/158269544 for detail.
*/
- private static void networkAddInterface(final INetd netd, final String iface,
+ private static void networkAddInterface(final INetd netd, int netId, final String iface,
int maxAttempts, int pollingIntervalMs)
throws ServiceSpecificException, RemoteException {
for (int i = 1; i <= maxAttempts; i++) {
try {
- netd.networkAddInterface(INetd.LOCAL_NET_ID, iface);
+ netd.networkAddInterface(netId, iface);
return;
} catch (ServiceSpecificException e) {
if (e.errorCode == EBUSY && i < maxAttempts) {
@@ -194,37 +194,38 @@
}
/** Reset interface for tethering. */
- public static void untetherInterface(final INetd netd, String iface)
+ public static void untetherInterface(final INetd netd, int netId, String iface)
throws RemoteException, ServiceSpecificException {
try {
netd.tetherInterfaceRemove(iface);
} finally {
- netd.networkRemoveInterface(INetd.LOCAL_NET_ID, iface);
+ netd.networkRemoveInterface(netId, iface);
}
}
- /** Add |routes| to local network. */
- public static void addRoutesToLocalNetwork(final INetd netd, final String iface,
+ /** Add |routes| to the given network. */
+ public static void addRoutesToNetwork(final INetd netd, int netId, final String iface,
final List<RouteInfo> routes) {
for (RouteInfo route : routes) {
if (!route.isDefaultRoute()) {
- modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID, route);
+ modifyRoute(netd, ModifyOperation.ADD, netId, route);
}
}
// IPv6 link local should be activated always.
- modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+ modifyRoute(netd, ModifyOperation.ADD, netId,
new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
}
- /** Remove routes from local network. */
- public static int removeRoutesFromLocalNetwork(final INetd netd, final List<RouteInfo> routes) {
+ /** Remove routes from the given network. */
+ public static int removeRoutesFromNetwork(final INetd netd, int netId,
+ final List<RouteInfo> routes) {
int failures = 0;
for (RouteInfo route : routes) {
try {
- modifyRoute(netd, ModifyOperation.REMOVE, INetd.LOCAL_NET_ID, route);
+ modifyRoute(netd, ModifyOperation.REMOVE, netId, route);
} catch (IllegalStateException e) {
failures++;
}
diff --git a/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
index 5069672..c2fbb56 100644
--- a/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
+++ b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
@@ -16,7 +16,6 @@
package com.android.net.module.util;
-import static android.net.INetd.LOCAL_NET_ID;
import static android.system.OsConstants.EBUSY;
import static com.android.testutils.MiscAsserts.assertThrows;
@@ -63,6 +62,7 @@
private static final String IFACE = "TEST_IFACE";
private static final IpPrefix TEST_IPPREFIX = new IpPrefix("192.168.42.1/24");
+ private static final int TEST_NET_ID = 123;
@Before
public void setUp() throws Exception {
@@ -134,7 +134,7 @@
}
throw new ServiceSpecificException(EBUSY);
- }).when(mNetd).networkAddInterface(LOCAL_NET_ID, IFACE);
+ }).when(mNetd).networkAddInterface(TEST_NET_ID, IFACE);
}
class Counter {
@@ -163,7 +163,7 @@
setNetworkAddInterfaceOutcome(new ServiceSpecificException(expectedCode), expectedTries);
try {
- NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX, 20, 0);
+ NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX, 20, 0);
fail("Expect throw ServiceSpecificException");
} catch (ServiceSpecificException e) {
assertEquals(e.errorCode, expectedCode);
@@ -177,7 +177,7 @@
setNetworkAddInterfaceOutcome(new RemoteException(), expectedTries);
try {
- NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX, 20, 0);
+ NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX, 20, 0);
fail("Expect throw RemoteException");
} catch (RemoteException e) { }
@@ -187,18 +187,19 @@
private void verifyNetworkAddInterfaceFails(int expectedTries) throws Exception {
verify(mNetd).tetherInterfaceAdd(IFACE);
- verify(mNetd, times(expectedTries)).networkAddInterface(LOCAL_NET_ID, IFACE);
+ verify(mNetd, times(expectedTries)).networkAddInterface(TEST_NET_ID, IFACE);
verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any());
+
verifyNoMoreInteractions(mNetd);
}
private void verifyTetherInterfaceSucceeds(int expectedTries) throws Exception {
setNetworkAddInterfaceOutcome(null, expectedTries);
- NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX);
+ NetdUtils.tetherInterface(mNetd, TEST_NET_ID, IFACE, TEST_IPPREFIX);
verify(mNetd).tetherInterfaceAdd(IFACE);
- verify(mNetd, times(expectedTries)).networkAddInterface(LOCAL_NET_ID, IFACE);
- verify(mNetd, times(2)).networkAddRoute(eq(LOCAL_NET_ID), eq(IFACE), any(), any());
+ verify(mNetd, times(expectedTries)).networkAddInterface(TEST_NET_ID, IFACE);
+ verify(mNetd, times(2)).networkAddRoute(eq(TEST_NET_ID), eq(IFACE), any(), any());
verifyNoMoreInteractions(mNetd);
reset(mNetd);
}
diff --git a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
new file mode 100644
index 0000000..f0de142
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.os.Process;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Contains mostly timerfd functionality.
+ */
+public class TimerFdUtils {
+ static {
+ final String jniLibName = JniUtil.getJniLibraryName(TimerFdUtils.class.getPackage());
+ if (jniLibName.equals("android_net_connectivity_com_android_net_module_util_jni")) {
+ // This library is part of service-connectivity.jar when in the system server,
+ // so libservice-connectivity.so is the library to load.
+ System.loadLibrary("service-connectivity");
+ } else {
+ System.loadLibrary(jniLibName);
+ }
+ }
+
+ private static final String TAG = TimerFdUtils.class.getSimpleName();
+
+ /**
+ * Create a timerfd.
+ *
+ * @throws IOException if the timerfd creation is failed.
+ */
+ private static native int createTimerFd() throws IOException;
+
+ /**
+ * Set given time to the timerfd.
+ *
+ * @param timeMs target time
+ * @throws IOException if setting expiration time is failed.
+ */
+ private static native void setTime(int fd, long timeMs) throws IOException;
+
+ /**
+ * Create a timerfd
+ */
+ static int createTimerFileDescriptor() {
+ try {
+ return createTimerFd();
+ } catch (IOException e) {
+ Log.e(TAG, "createTimerFd failed", e);
+ return -1;
+ }
+ }
+
+ /**
+ * Set expiration time to timerfd
+ */
+ static boolean setExpirationTime(int fd, long expirationTimeMs) {
+ try {
+ setTime(fd, expirationTimeMs);
+ } catch (IOException e) {
+ Log.e(TAG, "setExpirationTime failed", e);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java b/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java
new file mode 100644
index 0000000..dbbccc5
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.ParcelFileDescriptor;
+import android.util.CloseGuard;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.IOException;
+
+/**
+ * Represents a Timer file descriptor object used for scheduling tasks with precise delays.
+ * Compared to {@link Handler#postDelayed}, this class offers enhanced accuracy for delayed
+ * callbacks by accounting for periods when the device is in deep sleep.
+ *
+ * <p> This class is designed for use exclusively from the handler thread.
+ *
+ * **Usage Examples:**
+ *
+ * ** Scheduling recurring tasks with the same TimerFileDescriptor **
+ *
+ * ```java
+ * // Create a TimerFileDescriptor
+ * final TimerFileDescriptor timerFd = new TimerFileDescriptor(handler);
+ *
+ * // Schedule a new task with a delay.
+ * timerFd.setDelayedTask(() -> taskToExecute(), delayTime);
+ *
+ * // Once the delay has elapsed, and the task is running, schedule another task.
+ * timerFd.setDelayedTask(() -> anotherTaskToExecute(), anotherDelayTime);
+ *
+ * // Remember to close the TimerFileDescriptor after all tasks have finished running.
+ * timerFd.close();
+ * ```
+ */
+public class TimerFileDescriptor {
+ private static final String TAG = TimerFileDescriptor.class.getSimpleName();
+ // EVENT_ERROR may be generated even if not specified, as per its javadoc.
+ private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
+ private final CloseGuard mGuard = new CloseGuard();
+ @NonNull
+ private final Handler mHandler;
+ @NonNull
+ private final MessageQueue mQueue;
+ @NonNull
+ private final ParcelFileDescriptor mParcelFileDescriptor;
+ private final int mFdInt;
+ @Nullable
+ private ITask mTask;
+
+ /**
+ * An interface for defining tasks that can be executed using a {@link Handler}.
+ */
+ public interface ITask {
+ /**
+ * Executes the task using the provided {@link Handler}.
+ *
+ * @param handler The {@link Handler} to use for executing the task.
+ */
+ void post(Handler handler);
+ }
+
+ /**
+ * A task that sends a {@link Message} using a {@link Handler}.
+ */
+ public static class MessageTask implements ITask {
+ private final Message mMessage;
+
+ public MessageTask(Message message) {
+ mMessage = message;
+ }
+
+ /**
+ * Sends the {@link Message} using the provided {@link Handler}.
+ *
+ * @param handler The {@link Handler} to use for sending the message.
+ */
+ @Override
+ public void post(Handler handler) {
+ handler.sendMessage(mMessage);
+ }
+ }
+
+ /**
+ * A task that posts a {@link Runnable} to a {@link Handler}.
+ */
+ public static class RunnableTask implements ITask {
+ private final Runnable mRunnable;
+
+ public RunnableTask(Runnable runnable) {
+ mRunnable = runnable;
+ }
+
+ /**
+ * Posts the {@link Runnable} to the provided {@link Handler}.
+ *
+ * @param handler The {@link Handler} to use for posting the runnable.
+ */
+ @Override
+ public void post(Handler handler) {
+ handler.post(mRunnable);
+ }
+ }
+
+ /**
+ * TimerFileDescriptor constructor
+ *
+ * Note: The constructor is currently safe to call on another thread because it only sets final
+ * members and registers the event to be called on the handler.
+ */
+ public TimerFileDescriptor(@NonNull Handler handler) {
+ mFdInt = TimerFdUtils.createTimerFileDescriptor();
+ mParcelFileDescriptor = ParcelFileDescriptor.adoptFd(mFdInt);
+ mHandler = handler;
+ mQueue = handler.getLooper().getQueue();
+ registerFdEventListener();
+
+ mGuard.open("close");
+ }
+
+ /**
+ * Set a task to be executed after a specified delay.
+ *
+ * <p> A task can only be scheduled once at a time. Cancel previous scheduled task before the
+ * new task is scheduled.
+ *
+ * @param task the task to be executed
+ * @param delayMs the delay time in milliseconds
+ * @throws IllegalArgumentException if try to replace the current scheduled task
+ * @throws IllegalArgumentException if the delay time is less than 0
+ */
+ public void setDelayedTask(@NonNull ITask task, long delayMs) {
+ ensureRunningOnCorrectThread();
+ if (mTask != null) {
+ throw new IllegalArgumentException("task is already scheduled");
+ }
+ if (delayMs <= 0L) {
+ task.post(mHandler);
+ return;
+ }
+
+ if (TimerFdUtils.setExpirationTime(mFdInt, delayMs)) {
+ mTask = task;
+ }
+ }
+
+ /**
+ * Cancel the scheduled task.
+ */
+ public void cancelTask() {
+ ensureRunningOnCorrectThread();
+ if (mTask == null) return;
+
+ TimerFdUtils.setExpirationTime(mFdInt, 0 /* delayMs */);
+ mTask = null;
+ }
+
+ /**
+ * Check if there is a scheduled task.
+ */
+ public boolean hasDelayedTask() {
+ ensureRunningOnCorrectThread();
+ return mTask != null;
+ }
+
+ /**
+ * Close the TimerFileDescriptor. This implementation closes the underlying
+ * OS resources allocated to represent this stream.
+ */
+ public void close() {
+ ensureRunningOnCorrectThread();
+ unregisterAndDestroyFd();
+ }
+
+ private void registerFdEventListener() {
+ mQueue.addOnFileDescriptorEventListener(
+ mParcelFileDescriptor.getFileDescriptor(),
+ FD_EVENTS,
+ (fd, events) -> {
+ if (!isRunning()) {
+ return 0;
+ }
+ if ((events & EVENT_INPUT) != 0) {
+ handleExpiration();
+ }
+ return FD_EVENTS;
+ });
+ }
+
+ private boolean isRunning() {
+ return mParcelFileDescriptor.getFileDescriptor().valid();
+ }
+
+ private void handleExpiration() {
+ // Execute the task
+ if (mTask != null) {
+ mTask.post(mHandler);
+ mTask = null;
+ }
+ }
+
+ private void unregisterAndDestroyFd() {
+ if (mGuard != null) {
+ mGuard.close();
+ }
+
+ mQueue.removeOnFileDescriptorEventListener(mParcelFileDescriptor.getFileDescriptor());
+ try {
+ mParcelFileDescriptor.close();
+ } catch (IOException exception) {
+ Log.e(TAG, "close ParcelFileDescriptor failed. ", exception);
+ }
+ }
+
+ private void ensureRunningOnCorrectThread() {
+ if (mHandler.getLooper() != Looper.myLooper()) {
+ throw new IllegalStateException(
+ "Not running on Handler thread: " + Thread.currentThread().getName());
+ }
+ }
+
+ @SuppressWarnings("Finalize")
+ @Override
+ protected void finalize() throws Throwable {
+ if (mGuard != null) {
+ mGuard.warnIfOpen();
+ }
+ super.finalize();
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
index 781a04e..dfb2053 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
@@ -150,6 +150,8 @@
return (NetlinkMessage) RtNetlinkNeighborMessage.parse(nlmsghdr, byteBuffer);
case NetlinkConstants.RTM_NEWNDUSEROPT:
return (NetlinkMessage) NduseroptMessage.parse(nlmsghdr, byteBuffer);
+ case NetlinkConstants.RTM_NEWPREFIX:
+ return (NetlinkMessage) RtNetlinkPrefixMessage.parse(nlmsghdr, byteBuffer);
default: return null;
}
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index 541a375..e2544d3 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -55,6 +55,7 @@
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
@@ -469,4 +470,31 @@
// Nothing we can do here
}
}
+
+ /**
+ * Sends a netlink request to set flags for given interface
+ *
+ * @param interfaceName The name of the network interface to query.
+ * @param flags power-of-two integer flags to set or unset. A flag to set should be passed as
+ * is as a power-of-two value, and a flag to remove should be passed inversed as -1 with
+ * a single bit down. For example: IFF_UP, ~IFF_BROADCAST...
+ * @return true if the request finished successfully, otherwise false.
+ */
+ public static boolean setInterfaceFlags(@NonNull String interfaceName, int... flags) {
+ final RtNetlinkLinkMessage ntMsg =
+ RtNetlinkLinkMessage.createSetFlagsMessage(interfaceName, /*seqNo*/ 0, flags);
+ if (ntMsg == null) {
+ Log.e(TAG, "Failed to create message to set interface flags for interface "
+ + interfaceName + ", input flags are: " + Arrays.toString(flags));
+ return false;
+ }
+ final byte[] msg = ntMsg.pack(ByteOrder.nativeOrder());
+ try {
+ NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
+ return true;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to set flags for: " + interfaceName, e);
+ return false;
+ }
+ }
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
index 037d95f..1afe3b8 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
@@ -312,6 +312,57 @@
DEFAULT_MTU, null, null);
}
+ /**
+ * Creates an {@link RtNetlinkLinkMessage} instance that can be used to set the flags of a
+ * network interface.
+ *
+ * @param interfaceName The name of the network interface to query.
+ * @param sequenceNumber The sequence number for the Netlink message.
+ * @param flags power-of-two integer flags to set or unset. A flag to set should be passed as
+ * is as a power-of-two value, and a flag to remove should be passed inversed as -1 with
+ * a single bit down. For example: IFF_UP, ~IFF_BROADCAST...
+ * @return An `RtNetlinkLinkMessage` instance representing the request to query the interface.
+ */
+ @Nullable
+ public static RtNetlinkLinkMessage createSetFlagsMessage(@NonNull String interfaceName,
+ int sequenceNumber, int... flags) {
+ return createSetFlagsMessage(
+ interfaceName, sequenceNumber, new OsAccess(), flags);
+ }
+
+ @VisibleForTesting
+ @Nullable
+ protected static RtNetlinkLinkMessage createSetFlagsMessage(
+ @NonNull String interfaceName, int sequenceNumber, @NonNull OsAccess osAccess,
+ int... flags) {
+ final int interfaceIndex = osAccess.if_nametoindex(interfaceName);
+ if (interfaceIndex == OsAccess.INVALID_INTERFACE_INDEX) {
+ return null;
+ }
+
+ int flagsBits = 0;
+ int changeBits = 0;
+ for (int f : flags) {
+ if (Integer.bitCount(f) == 1) {
+ flagsBits |= f;
+ changeBits |= f;
+ } else if (Integer.bitCount(~f) == 1) {
+ flagsBits &= f;
+ changeBits |= ~f;
+ } else {
+ return null;
+ }
+ }
+ // RTM_NEWLINK is used here for create, modify, or notify changes about a internet
+ // interface, including change in administrative state. While RTM_SETLINK is used to
+ // modify an existing link rather than creating a new one.
+ return RtNetlinkLinkMessage.build(
+ new StructNlMsgHdr(/*payloadLen*/ 0, RTM_NEWLINK, NLM_F_REQUEST, sequenceNumber),
+ new StructIfinfoMsg((short) AF_UNSPEC, /*type*/ 0, interfaceIndex,
+ flagsBits, changeBits),
+ DEFAULT_MTU, /*hardwareAddress*/ null, /*interfaceName*/ null);
+ }
+
@Override
public String toString() {
return "RtNetlinkLinkMessage{ "
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkPrefixMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkPrefixMessage.java
new file mode 100644
index 0000000..30c63fb
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkPrefixMessage.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import android.net.IpPrefix;
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+
+/**
+ * A NetlinkMessage subclass for rtnetlink address messages.
+ *
+ * RtNetlinkPrefixMessage.parse() must be called with a ByteBuffer that contains exactly one
+ * netlink message.
+ *
+ * see also:
+ *
+ * include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class RtNetlinkPrefixMessage extends NetlinkMessage {
+ public static final short PREFIX_ADDRESS = 1;
+ public static final short PREFIX_CACHEINFO = 2;
+
+ @NonNull
+ private StructPrefixMsg mPrefixmsg;
+ @NonNull
+ private IpPrefix mPrefix;
+ private long mPreferredLifetime;
+ private long mValidLifetime;
+
+ @VisibleForTesting
+ public RtNetlinkPrefixMessage(@NonNull final StructNlMsgHdr header,
+ @NonNull final StructPrefixMsg prefixmsg,
+ @NonNull final IpPrefix prefix,
+ long preferred, long valid) {
+ super(header);
+ mPrefixmsg = prefixmsg;
+ mPrefix = prefix;
+ mPreferredLifetime = preferred;
+ mValidLifetime = valid;
+ }
+
+ private RtNetlinkPrefixMessage(@NonNull StructNlMsgHdr header) {
+ this(header, null, null, 0 /* preferredLifetime */, 0 /* validLifetime */);
+ }
+
+ @NonNull
+ public StructPrefixMsg getPrefixMsg() {
+ return mPrefixmsg;
+ }
+
+ @NonNull
+ public IpPrefix getPrefix() {
+ return mPrefix;
+ }
+
+ public long getPreferredLifetime() {
+ return mPreferredLifetime;
+ }
+
+ public long getValidLifetime() {
+ return mValidLifetime;
+ }
+
+ /**
+ * Parse rtnetlink prefix message from {@link ByteBuffer}. This method must be called with a
+ * ByteBuffer that contains exactly one netlink message.
+ *
+ * RTM_NEWPREFIX Message Format:
+ * +----------+- - -+-------------+- - -+---------------------+-----------------------+
+ * | nlmsghdr | Pad | prefixmsg | Pad | PREFIX_ADDRESS attr | PREFIX_CACHEINFO attr |
+ * +----------+- - -+-------------+- - -+---------------------+-----------------------+
+ *
+ * @param header netlink message header.
+ * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes.
+ */
+ @Nullable
+ public static RtNetlinkPrefixMessage parse(@NonNull final StructNlMsgHdr header,
+ @NonNull final ByteBuffer byteBuffer) {
+ try {
+ final RtNetlinkPrefixMessage msg = new RtNetlinkPrefixMessage(header);
+ msg.mPrefixmsg = StructPrefixMsg.parse(byteBuffer);
+
+ // PREFIX_ADDRESS
+ final int baseOffset = byteBuffer.position();
+ StructNlAttr nlAttr = StructNlAttr.findNextAttrOfType(PREFIX_ADDRESS, byteBuffer);
+ if (nlAttr == null) return null;
+ final Inet6Address addr = (Inet6Address) nlAttr.getValueAsInetAddress();
+ if (addr == null) return null;
+ msg.mPrefix = new IpPrefix(addr, msg.mPrefixmsg.prefix_len);
+
+ // PREFIX_CACHEINFO
+ byteBuffer.position(baseOffset);
+ nlAttr = StructNlAttr.findNextAttrOfType(PREFIX_CACHEINFO, byteBuffer);
+ if (nlAttr == null) return null;
+ final ByteBuffer buffer = nlAttr.getValueAsByteBuffer();
+ if (buffer == null) return null;
+ final StructPrefixCacheInfo cacheinfo = StructPrefixCacheInfo.parse(buffer);
+ msg.mPreferredLifetime = cacheinfo.preferred_time;
+ msg.mValidLifetime = cacheinfo.valid_time;
+
+ return msg;
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Write a rtnetlink prefix message to {@link ByteBuffer}.
+ */
+ @VisibleForTesting
+ protected void pack(ByteBuffer byteBuffer) {
+ getHeader().pack(byteBuffer);
+ mPrefixmsg.pack(byteBuffer);
+
+ // PREFIX_ADDRESS attribute
+ final StructNlAttr prefixAddress =
+ new StructNlAttr(PREFIX_ADDRESS, mPrefix.getRawAddress());
+ prefixAddress.pack(byteBuffer);
+
+ // PREFIX_CACHEINFO attribute
+ final StructPrefixCacheInfo cacheinfo =
+ new StructPrefixCacheInfo(mPreferredLifetime, mValidLifetime);
+ final StructNlAttr prefixCacheinfo =
+ new StructNlAttr(PREFIX_CACHEINFO, cacheinfo.writeToBytes());
+ prefixCacheinfo.pack(byteBuffer);
+ }
+
+ @Override
+ public String toString() {
+ return "RtNetlinkPrefixMessage{ "
+ + "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
+ + "prefixmsg{" + mPrefixmsg.toString() + "}, "
+ + "IP Prefix{" + mPrefix + "}, "
+ + "preferred lifetime{" + mPreferredLifetime + "}, "
+ + "valid lifetime{" + mValidLifetime + "} "
+ + "}";
+ }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
index f3d8c4a..760d849 100644
--- a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
@@ -22,6 +22,7 @@
import android.util.Pair;
import android.util.SparseArray;
+import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -413,4 +414,68 @@
}
return -1;
}
+
+ /**
+ * Concatenates multiple arrays of the same type into a single new array.
+ */
+ public static byte[] concatArrays(@NonNull byte[]... arr) {
+ int size = 0;
+ for (byte[] a : arr) {
+ size += a.length;
+ }
+ final byte[] result = new byte[size];
+ int offset = 0;
+ for (byte[] a : arr) {
+ System.arraycopy(a, 0, result, offset, a.length);
+ offset += a.length;
+ }
+ return result;
+ }
+
+ /**
+ * Concatenates multiple arrays of the same type into a single new array.
+ */
+ public static <T> T[] concatArrays(@NonNull Class<T> clazz, @NonNull T[]... arr) {
+ int size = 0;
+ for (T[] a : arr) {
+ size += a.length;
+ }
+ final T[] result = (T[]) Array.newInstance(clazz, size);
+ int offset = 0;
+ for (T[] a : arr) {
+ System.arraycopy(a, 0, result, offset, a.length);
+ offset += a.length;
+ }
+ return result;
+ }
+
+ /**
+ * Prepends the elements of a variable number of prefixes to an existing array (suffix).
+ */
+ public static byte[] prependArray(@NonNull byte[] suffix, @NonNull byte... prefixes) {
+ return concatArrays(prefixes, suffix);
+ }
+
+ /**
+ * Prepends the elements of a variable number of prefixes to an existing array (suffix).
+ */
+ public static <T> T[] prependArray(@NonNull Class<T> clazz, @NonNull T[] suffix,
+ @NonNull T... prefixes) {
+ return concatArrays(clazz, prefixes, suffix);
+ }
+
+ /**
+ * Appends the elements of a variable number of suffixes to an existing array (prefix).
+ */
+ public static byte[] appendArray(@NonNull byte[] prefix, @NonNull byte... suffixes) {
+ return concatArrays(prefix, suffixes);
+ }
+
+ /**
+ * Appends the elements of a variable number of suffixes to an existing array (prefix).
+ */
+ public static <T> T[] appendArray(@NonNull Class<T> clazz, @NonNull T[] prefix,
+ @NonNull T... suffixes) {
+ return concatArrays(clazz, prefix, suffixes);
+ }
}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsPacket.java b/staticlibs/framework/com/android/net/module/util/DnsPacket.java
index 63106a1..b0c5e2e 100644
--- a/staticlibs/framework/com/android/net/module/util/DnsPacket.java
+++ b/staticlibs/framework/com/android/net/module/util/DnsPacket.java
@@ -50,7 +50,7 @@
*
* @hide
*/
-public abstract class DnsPacket {
+public class DnsPacket {
/**
* Type of the canonical name for an alias. Refer to RFC 1035 section 3.2.2.
*/
@@ -515,7 +515,14 @@
protected final DnsHeader mHeader;
protected final List<DnsRecord>[] mRecords;
- protected DnsPacket(@NonNull byte[] data) throws ParseException {
+ /**
+ * Returns the list of DNS records for a given section.
+ */
+ public List<DnsRecord> getRecords(@RecordType int section) {
+ return mRecords[section];
+ }
+
+ public DnsPacket(@NonNull byte[] data) throws ParseException {
if (null == data) {
throw new ParseException("Parse header failed, null input data");
}
@@ -548,7 +555,7 @@
*
* Note that authority records section and additional records section is not supported.
*/
- protected DnsPacket(@NonNull DnsHeader header, @NonNull List<DnsRecord> qd,
+ public DnsPacket(@NonNull DnsHeader header, @NonNull List<DnsRecord> qd,
@NonNull List<DnsRecord> an) {
mHeader = Objects.requireNonNull(header);
mRecords = new List[NUM_SECTIONS];
diff --git a/staticlibs/framework/com/android/net/module/util/HexDump.java b/staticlibs/framework/com/android/net/module/util/HexDump.java
index a22c258..409f611 100644
--- a/staticlibs/framework/com/android/net/module/util/HexDump.java
+++ b/staticlibs/framework/com/android/net/module/util/HexDump.java
@@ -202,7 +202,7 @@
if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
- throw new RuntimeException("Invalid hex char '" + c + "'");
+ throw new IllegalArgumentException("Invalid hex char '" + c + "'");
}
/**
diff --git a/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java b/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
index 80088b9..96d995a 100644
--- a/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
+++ b/staticlibs/framework/com/android/net/module/util/LruCacheWithExpiry.java
@@ -22,13 +22,13 @@
import com.android.internal.annotations.GuardedBy;
-import java.time.Clock;
import java.util.Objects;
+import java.util.function.LongSupplier;
import java.util.function.Predicate;
import java.util.function.Supplier;
/**
- * An LRU cache that stores key-value pairs with an expiry time.
+ * A thread-safe LRU cache that stores key-value pairs with an expiry time.
*
* <p>This cache uses an {@link LruCache} to store entries and evicts the least
* recently used entries when the cache reaches its maximum capacity. It also
@@ -41,7 +41,7 @@
* @hide
*/
public class LruCacheWithExpiry<K, V> {
- private final Clock mClock;
+ private final LongSupplier mTimeSupplier;
private final long mExpiryDurationMs;
@GuardedBy("mMap")
private final LruCache<K, CacheValue<V>> mMap;
@@ -50,16 +50,17 @@
/**
* Constructs a new {@link LruCacheWithExpiry} with the specified parameters.
*
- * @param clock The {@link Clock} to use for determining timestamps.
+ * @param timeSupplier The {@link java.util.function.LongSupplier} to use for
+ * determining timestamps.
* @param expiryDurationMs The expiry duration for cached entries in milliseconds.
* @param maxSize The maximum number of entries to hold in the cache.
* @param shouldCacheValue A {@link Predicate} that determines whether a given value should be
* cached. This can be used to filter out certain values from being
* stored in the cache.
*/
- public LruCacheWithExpiry(@NonNull Clock clock, long expiryDurationMs, int maxSize,
- Predicate<V> shouldCacheValue) {
- mClock = clock;
+ public LruCacheWithExpiry(@NonNull LongSupplier timeSupplier, long expiryDurationMs,
+ int maxSize, Predicate<V> shouldCacheValue) {
+ mTimeSupplier = timeSupplier;
mExpiryDurationMs = expiryDurationMs;
mMap = new LruCache<>(maxSize);
mShouldCacheValue = shouldCacheValue;
@@ -119,7 +120,26 @@
public void put(@NonNull K key, @NonNull V value) {
Objects.requireNonNull(value);
synchronized (mMap) {
- mMap.put(key, new CacheValue<>(mClock.millis(), value));
+ mMap.put(key, new CacheValue<>(mTimeSupplier.getAsLong(), value));
+ }
+ }
+
+ /**
+ * Stores a value in the cache if absent, associated with the given key.
+ *
+ * @param key The key to associate with the value.
+ * @param value The value to store in the cache.
+ * @return The existing value associated with the key, if present; otherwise, null.
+ */
+ @Nullable
+ public V putIfAbsent(@NonNull K key, @NonNull V value) {
+ Objects.requireNonNull(value);
+ synchronized (mMap) {
+ final V existingValue = get(key);
+ if (existingValue == null) {
+ put(key, value);
+ }
+ return existingValue;
}
}
@@ -133,7 +153,7 @@
}
private boolean isExpired(long timestamp) {
- return mClock.millis() > timestamp + mExpiryDurationMs;
+ return mTimeSupplier.getAsLong() > timestamp + mExpiryDurationMs;
}
private static class CacheValue<V> {
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 5d588cc..4878334 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -120,6 +120,8 @@
(byte) 0, (byte) 0, (byte) 0, (byte) 0,
(byte) 0, (byte) 0, (byte) 0, (byte) 0,
(byte) 0, (byte) 0, (byte) 0, (byte) 0 });
+ public static final Inet4Address IPV4_ADDR_ALL_HOST_MULTICAST =
+ (Inet4Address) InetAddresses.parseNumericAddress("224.0.0.1");
/**
* CLAT constants
diff --git a/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java b/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java
new file mode 100644
index 0000000..b4f7642
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.util.Log;
+
+import java.util.function.BiConsumer;
+
+/**
+ * Utility class for logging terrible errors and reporting them for tracking.
+ *
+ * @hide
+ */
+public class TerribleErrorLog {
+
+ private static final String TAG = TerribleErrorLog.class.getSimpleName();
+
+ /**
+ * Logs a terrible error and reports metrics through a provided statsLog.
+ */
+ public static void logTerribleError(@NonNull BiConsumer<Integer, Integer> statsLog,
+ @NonNull String message, int protoType, int errorType) {
+ statsLog.accept(protoType, errorType);
+ Log.wtf(TAG, message);
+ }
+}
diff --git a/staticlibs/native/bpfmapjni/Android.bp b/staticlibs/native/bpfmapjni/Android.bp
index 969ebd4..9a58a93 100644
--- a/staticlibs/native/bpfmapjni/Android.bp
+++ b/staticlibs/native/bpfmapjni/Android.bp
@@ -26,6 +26,7 @@
header_libs: [
"bpf_headers",
"jni_headers",
+ "libbase_headers",
],
shared_libs: [
"liblog",
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 1923ceb..d862f6b 100644
--- a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
@@ -24,37 +24,38 @@
#include "nativehelper/scoped_primitive_array.h"
#include "nativehelper/scoped_utf_chars.h"
-#define BPF_FD_JUST_USE_INT
+#include <android-base/unique_fd.h>
#include "BpfSyscallWrappers.h"
-
#include "bpf/KernelUtils.h"
namespace android {
+using ::android::base::unique_fd;
+
static jint com_android_net_module_util_BpfMap_nativeBpfFdGet(JNIEnv *env, jclass clazz,
jstring path, jint mode, jint keySize, jint valueSize) {
ScopedUtfChars pathname(env, path);
- jint fd = -1;
+ unique_fd fd;
switch (mode) {
case 0:
- fd = bpf::mapRetrieveRW(pathname.c_str());
+ fd.reset(bpf::mapRetrieveRW(pathname.c_str()));
break;
case BPF_F_RDONLY:
- fd = bpf::mapRetrieveRO(pathname.c_str());
+ fd.reset(bpf::mapRetrieveRO(pathname.c_str()));
break;
case BPF_F_WRONLY:
- fd = bpf::mapRetrieveWO(pathname.c_str());
+ fd.reset(bpf::mapRetrieveWO(pathname.c_str()));
break;
case BPF_F_RDONLY|BPF_F_WRONLY:
- fd = bpf::mapRetrieveExclusiveRW(pathname.c_str());
+ fd.reset(bpf::mapRetrieveExclusiveRW(pathname.c_str()));
break;
default:
errno = EINVAL;
break;
}
- if (fd < 0) {
+ if (!fd.ok()) {
jniThrowErrnoException(env, "nativeBpfFdGet", errno);
return -1;
}
@@ -62,18 +63,16 @@
if (bpf::isAtLeastKernelVersion(4, 14, 0)) {
// These likely fail with -1 and set errno to EINVAL on <4.14
if (bpf::bpfGetFdKeySize(fd) != keySize) {
- close(fd);
jniThrowErrnoException(env, "nativeBpfFdGet KeySize", EBADFD);
return -1;
}
if (bpf::bpfGetFdValueSize(fd) != valueSize) {
- close(fd);
jniThrowErrnoException(env, "nativeBpfFdGet ValueSize", EBADFD);
return -1;
}
}
- return fd;
+ return fd.release();
}
static void com_android_net_module_util_BpfMap_nativeWriteToMapEntry(JNIEnv *env, jobject self,
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 8c54e6a..9d1d291 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -55,6 +55,7 @@
// For mockito extended
"libdexmakerjvmtiagent",
"libstaticjvmtiagent",
+ "libcom_android_net_moduletests_util_jni",
],
jarjar_rules: "jarjar-rules.txt",
test_suites: ["device-tests"],
diff --git a/staticlibs/tests/unit/jni/Android.bp b/staticlibs/tests/unit/jni/Android.bp
new file mode 100644
index 0000000..e456471
--- /dev/null
+++ b/staticlibs/tests/unit/jni/Android.bp
@@ -0,0 +1,39 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_team: "trendy_team_fwk_core_networking",
+ // See: http://go/android-license-faq
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_shared {
+ name: "libcom_android_net_moduletests_util_jni",
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ "-Wthread-safety",
+ ],
+ srcs: [
+ "com_android_net_moduletests_util/onload.cpp",
+ ],
+ static_libs: [
+ "libnet_utils_device_common_timerfdjni",
+ ],
+ shared_libs: [
+ "liblog",
+ "libnativehelper",
+ ],
+}
diff --git a/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp b/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp
new file mode 100644
index 0000000..a035540
--- /dev/null
+++ b/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "jni.h"
+#include <nativehelper/JNIHelp.h>
+
+#define LOG_TAG "NetworkStaticLibTestsJni"
+#include <android/log.h>
+
+namespace android {
+
+int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
+ char const *class_name);
+
+extern "C" jint JNI_OnLoad(JavaVM *vm, void *) {
+ JNIEnv *env;
+ if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, "ERROR: GetEnv failed");
+ return JNI_ERR;
+ }
+
+ if (register_com_android_net_module_util_TimerFdUtils(
+ env, "com/android/net/moduletests/util/TimerFdUtils") < 0)
+ return JNI_ERR;
+
+ return JNI_VERSION_1_6;
+}
+
+}; // namespace android
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt
index 851d09a..bde55c3 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt
@@ -17,14 +17,19 @@
package com.android.net.module.util
import android.util.Log
+import com.android.testutils.TryTestConfig
import com.android.testutils.tryTest
+import java.util.function.Consumer
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+import org.junit.After
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
-import kotlin.test.assertTrue
-import kotlin.test.fail
private val TAG = CleanupTest::class.simpleName
@@ -34,6 +39,18 @@
class TestException2 : Exception()
class TestException3 : Exception()
+ private var originalDiagnosticsCollector: Consumer<Throwable>? = null
+
+ @Before
+ fun setUp() {
+ originalDiagnosticsCollector = TryTestConfig.swapDiagnosticsCollector(null)
+ }
+
+ @After
+ fun tearDown() {
+ TryTestConfig.swapDiagnosticsCollector(originalDiagnosticsCollector)
+ }
+
@Test
fun testNotThrow() {
var x = 1
@@ -220,4 +237,74 @@
assertTrue(thrown.suppressedExceptions[1] is TestException3)
assert(x == 7)
}
+
+ @Test
+ fun testNoErrorReportingWhenCaught() {
+ var error: Throwable? = null
+ TryTestConfig.swapDiagnosticsCollector {
+ error = it
+ }
+ var x = 1
+ tryTest {
+ x = 2
+ throw TestException1()
+ x = 3
+ }.catch<TestException1> {
+ x = 4
+ } cleanup {
+ x = 5
+ }
+
+ assertEquals(5, x)
+ assertNull(error)
+ }
+
+ @Test
+ fun testErrorReportingInTry() {
+ var error: Throwable? = null
+ TryTestConfig.swapDiagnosticsCollector {
+ assertNull(error)
+ error = it
+ }
+ var x = 1
+ assertFailsWith<TestException1> {
+ tryTest {
+ x = 2
+ throw TestException1()
+ x = 3
+ } cleanupStep {
+ throw TestException2()
+ x = 4
+ } cleanup {
+ x = 5
+ }
+ }
+
+ assertEquals(5, x)
+ assertTrue(error is TestException1)
+ }
+
+ @Test
+ fun testErrorReportingInCatch() {
+ var error: Throwable? = null
+ TryTestConfig.swapDiagnosticsCollector {
+ assertNull(error)
+ error = it
+ }
+ var x = 1
+ assertFailsWith<TestException2> {
+ tryTest {
+ throw TestException1()
+ x = 2
+ }.catch<TestException1> {
+ throw TestException2()
+ x = 3
+ } cleanup {
+ x = 4
+ }
+ }
+
+ assertEquals(4, x)
+ assertTrue(error is TestException2)
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
index 4ed3afd..1aa943e 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
@@ -19,7 +19,7 @@
import android.util.SparseArray
import androidx.test.filters.SmallTest
import androidx.test.runner.AndroidJUnit4
-import com.android.testutils.assertThrows
+import kotlin.test.assertContentEquals
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals
@@ -183,17 +183,101 @@
@Test
fun testGetIndexForValue() {
- val sparseArray = SparseArray<String>();
- sparseArray.put(5, "hello");
- sparseArray.put(10, "abcd");
- sparseArray.put(20, null);
+ val sparseArray = SparseArray<String>()
+ sparseArray.put(5, "hello")
+ sparseArray.put(10, "abcd")
+ sparseArray.put(20, null)
- val value1 = "abcd";
+ val value1 = "abcd"
val value1Copy = String(value1.toCharArray())
- val value2 = null;
+ val value2 = null
- assertEquals(1, CollectionUtils.getIndexForValue(sparseArray, value1));
- assertEquals(1, CollectionUtils.getIndexForValue(sparseArray, value1Copy));
- assertEquals(2, CollectionUtils.getIndexForValue(sparseArray, value2));
+ assertEquals(1, CollectionUtils.getIndexForValue(sparseArray, value1))
+ assertEquals(1, CollectionUtils.getIndexForValue(sparseArray, value1Copy))
+ assertEquals(2, CollectionUtils.getIndexForValue(sparseArray, value2))
+ }
+
+ @Test
+ fun testConcatEmptyByteArrays() {
+ assertContentEquals(
+ byteArrayOf(),
+ CollectionUtils.concatArrays(byteArrayOf(), byteArrayOf())
+ )
+ }
+
+ @Test
+ fun testConcatEmptyStringArrays() {
+ assertContentEquals(
+ arrayOf<String>(),
+ CollectionUtils.concatArrays(
+ String::class.java,
+ arrayOf<String>(),
+ arrayOf<String>()
+ )
+ )
+ }
+
+ @Test
+ fun testConcatByteArrays() {
+ val byteArr1 = byteArrayOf(1, 2, 3)
+ val byteArr2 = byteArrayOf(4, 5, 6)
+ val byteArr3 = byteArrayOf()
+ val byteArrExpected = byteArrayOf(1, 2, 3, 4, 5, 6)
+ assertContentEquals(
+ byteArrExpected,
+ CollectionUtils.concatArrays(byteArr1, byteArr2, byteArr3)
+ )
+ }
+
+ @Test
+ fun testConcatStringArrays() {
+ val stringArr1 = arrayOf("1", "2", "3")
+ val stringArr2 = arrayOf("4", "5", "6")
+ val strinvArr3 = arrayOf<String>()
+ val stringArrExpected = arrayOf("1", "2", "3", "4", "5", "6")
+ assertContentEquals(
+ stringArrExpected,
+ CollectionUtils.concatArrays(String::class.java, stringArr1, stringArr2, strinvArr3)
+ )
+ }
+
+ @Test
+ fun testPrependByteArrays() {
+ val byteArr2 = byteArrayOf(4, 5, 6)
+ val byteArrExpected = byteArrayOf(1, 2, 3, 4, 5, 6)
+ assertContentEquals(
+ byteArrExpected,
+ CollectionUtils.prependArray(byteArr2, 1, 2, 3)
+ )
+ }
+
+ @Test
+ fun testPrependStringArrays() {
+ val stringArr2 = arrayOf("4", "5", "6")
+ val stringArrExpected = arrayOf("1", "2", "3", "4", "5", "6")
+ assertContentEquals(
+ stringArrExpected,
+ CollectionUtils.prependArray(String::class.java, stringArr2, "1", "2", "3")
+ )
+ }
+
+ @Test
+ fun testAppendByteArrays() {
+ val byteArr1 = byteArrayOf(1, 2, 3)
+ val byteArrExpected = byteArrayOf(1, 2, 3, 4, 5, 6)
+ assertContentEquals(
+ byteArrExpected,
+ CollectionUtils.appendArray(byteArr1, 4, 5, 6)
+ )
+ }
+
+ @Test
+ fun testAppendStringArrays() {
+ val stringArr1 = arrayOf("1", "2", "3")
+ val stringArrExpected = arrayOf("1", "2", "3", "4", "5", "6")
+ assertContentEquals(
+ stringArrExpected,
+ CollectionUtils.appendArray(String::class.java, stringArr1, "4", "5", "6")
+ )
}
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java
index 5a15585..f81978a 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/HexDumpTest.java
@@ -18,6 +18,7 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -50,6 +51,11 @@
}
@Test
+ public void testInvalidHexStringToByteArray() {
+ assertThrows(IllegalArgumentException.class, () -> HexDump.hexStringToByteArray("abxX"));
+ }
+
+ @Test
public void testIntegerToByteArray() {
assertArrayEquals(new byte[]{(byte) 0xff, (byte) 0x00, (byte) 0x00, (byte) 0x04},
HexDump.toByteArray((int) 0xff000004));
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/LruCacheWithExpiryTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/LruCacheWithExpiryTest.kt
new file mode 100644
index 0000000..b6af892
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/LruCacheWithExpiryTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.function.LongSupplier
+
+@RunWith(DevSdkIgnoreRunner::class)
+class LruCacheWithExpiryTest {
+
+ companion object {
+ private const val CACHE_SIZE = 2
+ private const val EXPIRY_DURATION_MS = 1000L
+ }
+
+ private val timeSupplier = object : LongSupplier {
+ private var currentTimeMillis = 0L
+ override fun getAsLong(): Long = currentTimeMillis
+ fun advanceTime(millis: Long) { currentTimeMillis += millis }
+ }
+
+ private val cache = LruCacheWithExpiry<Int, String>(
+ timeSupplier, EXPIRY_DURATION_MS, CACHE_SIZE) { true }
+
+ @Test
+ fun testPutIfAbsent_keyNotPresent() {
+ val value = cache.putIfAbsent(1, "value1")
+ assertNull(value)
+ assertEquals("value1", cache.get(1))
+ }
+
+ @Test
+ fun testPutIfAbsent_keyPresent() {
+ cache.put(1, "value1")
+ val value = cache.putIfAbsent(1, "value2")
+ assertEquals("value1", value)
+ assertEquals("value1", cache.get(1))
+ }
+
+ @Test
+ fun testPutIfAbsent_keyPresentButExpired() {
+ cache.put(1, "value1")
+ // Advance time to expire the entry
+ timeSupplier.advanceTime(EXPIRY_DURATION_MS + 1)
+ val value = cache.putIfAbsent(1, "value2")
+ assertNull(value)
+ assertEquals("value2", cache.get(1))
+ }
+
+ @Test
+ fun testPutIfAbsent_maxSizeReached() {
+ cache.put(1, "value1")
+ cache.put(2, "value2")
+ cache.putIfAbsent(3, "value3") // This should evict the least recently used entry (1)
+ assertNull(cache.get(1))
+ assertEquals("value2", cache.get(2))
+ assertEquals("value3", cache.get(3))
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt
new file mode 100644
index 0000000..5fd634e
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.testutils.tryTest
+import kotlin.test.assertContentEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class TerribleErrorLogTest {
+ @Test
+ fun testLogTerribleError() {
+ val wtfCaptures = mutableListOf<String>()
+ val prevHandler = Log.setWtfHandler { tag, what, system ->
+ wtfCaptures.add("$tag,${what.message}")
+ }
+ val statsLogCapture = mutableListOf<Pair<Int, Int>>()
+ val testStatsLog = object {
+ fun write(protoType: Int, errorType: Int) {
+ statsLogCapture.add(protoType to errorType)
+ }
+ }
+ tryTest {
+ TerribleErrorLog.logTerribleError(testStatsLog::write, "error", 1, 2)
+ assertContentEquals(listOf(1 to 2), statsLogCapture)
+ assertContentEquals(listOf("TerribleErrorLog,error"), wtfCaptures)
+ } cleanup {
+ Log.setWtfHandler(prevHandler)
+ }
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/TimerFileDescriptorTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/TimerFileDescriptorTest.kt
new file mode 100644
index 0000000..f5e47c9
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/TimerFileDescriptorTest.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import android.os.Build
+import android.os.ConditionVariable
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.Message
+import androidx.test.filters.SmallTest
+import com.android.net.module.util.TimerFileDescriptor.ITask
+import com.android.net.module.util.TimerFileDescriptor.MessageTask
+import com.android.net.module.util.TimerFileDescriptor.RunnableTask
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.tryTest
+import com.android.testutils.visibleOnHandlerThread
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.time.Duration
+import java.time.Instant
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+private const val MSG_TEST = 1
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class TimerFileDescriptorTest {
+ private class TestHandler(looper: Looper) : Handler(looper) {
+ override fun handleMessage(msg: Message) {
+ val cv = msg.obj as ConditionVariable
+ cv.open()
+ }
+ }
+ private val thread = HandlerThread(TimerFileDescriptorTest::class.simpleName).apply { start() }
+ private val handler by lazy { TestHandler(thread.looper) }
+
+ @After
+ fun tearDown() {
+ thread.quitSafely()
+ thread.join()
+ }
+
+ private fun assertDelayedTaskPost(
+ timerFd: TimerFileDescriptor,
+ task: ITask,
+ cv: ConditionVariable
+ ) {
+ val delayTime = 10L
+ val startTime1 = Instant.now()
+ handler.post { timerFd.setDelayedTask(task, delayTime) }
+ assertTrue(cv.block(100L /* timeoutMs*/))
+ assertTrue(Duration.between(startTime1, Instant.now()).toMillis() >= delayTime)
+ }
+
+ @Test
+ fun testSetDelayedTask() {
+ val timerFd = TimerFileDescriptor(handler)
+ tryTest {
+ // Verify the delayed task is executed with the self-implemented ITask
+ val cv1 = ConditionVariable()
+ assertDelayedTaskPost(timerFd, { cv1.open() }, cv1)
+
+ // Verify the delayed task is executed with the RunnableTask
+ val cv2 = ConditionVariable()
+ assertDelayedTaskPost(timerFd, RunnableTask{ cv2.open() }, cv2)
+
+ // Verify the delayed task is executed with the MessageTask
+ val cv3 = ConditionVariable()
+ assertDelayedTaskPost(timerFd, MessageTask(handler.obtainMessage(MSG_TEST, cv3)), cv3)
+ } cleanup {
+ visibleOnHandlerThread(handler) { timerFd.close() }
+ }
+ }
+
+ @Test
+ fun testCancelTask() {
+ // The task is posted and canceled within the same handler loop, so the short delay used
+ // here won't cause flakes.
+ val delayTime = 10L
+ val timerFd = TimerFileDescriptor(handler)
+ val cv = ConditionVariable()
+ tryTest {
+ handler.post {
+ timerFd.setDelayedTask({ cv.open() }, delayTime)
+ assertTrue(timerFd.hasDelayedTask())
+ timerFd.cancelTask()
+ assertFalse(timerFd.hasDelayedTask())
+ }
+ assertFalse(cv.block(20L /* timeoutMs*/))
+ } cleanup {
+ visibleOnHandlerThread(handler) { timerFd.close() }
+ }
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
index bd0e31d..8104e3a 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
@@ -306,6 +306,28 @@
}
@Test
+ public void testCreateSetInterfaceFlagsMessage() {
+ final String expectedHexBytes =
+ "20000000100001006824000000000000" // struct nlmsghdr
+ + "00000000080000000100000001000100"; // struct ifinfomsg
+ final String interfaceName = "wlan0";
+ final int interfaceIndex = 8;
+ final int sequenceNumber = 0x2468;
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetFlagsMessage(
+ interfaceName,
+ sequenceNumber,
+ mOsAccess,
+ NetlinkConstants.IFF_UP,
+ ~NetlinkConstants.IFF_LOWER_UP);
+ assertNotNull(msg);
+ final byte[] bytes = msg.pack(ByteOrder.LITTLE_ENDIAN); // For testing.
+ assertEquals(expectedHexBytes, HexDump.toHexString(bytes));
+ }
+
+ @Test
public void testToString() {
final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_HEX);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkPrefixMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkPrefixMessageTest.java
new file mode 100644
index 0000000..b1779cb
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkPrefixMessageTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink;
+
+import static android.system.OsConstants.NETLINK_ROUTE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.IpPrefix;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RtNetlinkPrefixMessageTest {
+ private static final IpPrefix TEST_PREFIX = new IpPrefix("2001:db8:1:1::/64");
+
+ // An example of the full RTM_NEWPREFIX message.
+ private static final String RTM_NEWPREFIX_HEX =
+ "3C000000340000000000000000000000" // struct nlmsghr
+ + "0A0000002F00000003400300" // struct prefixmsg
+ + "1400010020010DB8000100010000000000000000" // PREFIX_ADDRESS
+ + "0C000200803A0900008D2700"; // PREFIX_CACHEINFO
+
+ private ByteBuffer toByteBuffer(final String hexString) {
+ return ByteBuffer.wrap(HexDump.hexStringToByteArray(hexString));
+ }
+
+ @Test
+ public void testParseRtmNewPrefix() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNotNull(msg);
+ assertTrue(msg instanceof RtNetlinkPrefixMessage);
+ final RtNetlinkPrefixMessage prefixmsg = (RtNetlinkPrefixMessage) msg;
+
+ final StructNlMsgHdr hdr = prefixmsg.getHeader();
+ assertNotNull(hdr);
+ assertEquals(60, hdr.nlmsg_len);
+ assertEquals(NetlinkConstants.RTM_NEWPREFIX, hdr.nlmsg_type);
+ assertEquals(0, hdr.nlmsg_flags);
+ assertEquals(0, hdr.nlmsg_seq);
+ assertEquals(0, hdr.nlmsg_pid);
+
+ final StructPrefixMsg prefixmsgHdr = prefixmsg.getPrefixMsg();
+ assertNotNull(prefixmsgHdr);
+ assertEquals((byte) OsConstants.AF_INET6, prefixmsgHdr.prefix_family);
+ assertEquals(3, prefixmsgHdr.prefix_type);
+ assertEquals(64, prefixmsgHdr.prefix_len);
+ assertEquals(0x03, prefixmsgHdr.prefix_flags);
+ assertEquals(0x2F, prefixmsgHdr.prefix_ifindex);
+
+ assertEquals(prefixmsg.getPrefix(), TEST_PREFIX);
+ assertEquals(604800L, prefixmsg.getPreferredLifetime());
+ assertEquals(2592000L, prefixmsg.getValidLifetime());
+ }
+
+ @Test
+ public void testPackRtmNewPrefix() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNotNull(msg);
+ assertTrue(msg instanceof RtNetlinkPrefixMessage);
+ final RtNetlinkPrefixMessage prefixmsg = (RtNetlinkPrefixMessage) msg;
+
+ final ByteBuffer packBuffer = ByteBuffer.allocate(60);
+ packBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ prefixmsg.pack(packBuffer);
+ assertEquals(RTM_NEWPREFIX_HEX, HexDump.toHexString(packBuffer.array()));
+ }
+
+ private static final String RTM_NEWPREFIX_WITHOUT_PREFIX_ADDRESS_HEX =
+ "24000000340000000000000000000000" // struct nlmsghr
+ + "0A0000002F00000003400300" // struct prefixmsg
+ + "0C000200803A0900008D2700"; // PREFIX_CACHEINFO
+
+ @Test
+ public void testParseRtmNewPrefix_withoutPrefixAddressAttribute() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_WITHOUT_PREFIX_ADDRESS_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNull(msg);
+ }
+
+ private static final String RTM_NEWPREFIX_WITHOUT_PREFIX_CACHEINFO_HEX =
+ "30000000340000000000000000000000" // struct nlmsghr
+ + "0A0000002F00000003400300" // struct prefixmsg
+ + "140001002A0079E10ABCF6050000000000000000"; // PREFIX_ADDRESS
+
+ @Test
+ public void testParseRtmNewPrefix_withoutPrefixCacheinfoAttribute() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_WITHOUT_PREFIX_CACHEINFO_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNull(msg);
+ }
+
+ private static final String RTM_NEWPREFIX_TRUNCATED_PREFIX_ADDRESS_HEX =
+ "3C000000340000000000000000000000" // struct nlmsghr
+ + "0A0000002F00000003400300" // struct prefixmsg
+ + "140001002A0079E10ABCF605000000000000" // PREFIX_ADDRESS (truncated)
+ + "0C000200803A0900008D2700"; // PREFIX_CACHEINFO
+
+ @Test
+ public void testParseRtmNewPrefix_truncatedPrefixAddressAttribute() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_TRUNCATED_PREFIX_ADDRESS_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNull(msg);
+ }
+
+ private static final String RTM_NEWPREFIX_TRUNCATED_PREFIX_CACHEINFO_HEX =
+ "3C000000340000000000000000000000" // struct nlmsghr
+ + "0A0000002F00000003400300" // struct prefixmsg
+ + "140001002A0079E10ABCF6050000000000000000" // PREFIX_ADDRESS
+ + "0C000200803A0900008D"; // PREFIX_CACHEINFO (truncated)
+
+ @Test
+ public void testParseRtmNewPrefix_truncatedPrefixCacheinfoAttribute() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_TRUNCATED_PREFIX_CACHEINFO_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNull(msg);
+ }
+
+ @Test
+ public void testToString() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNotNull(msg);
+ assertTrue(msg instanceof RtNetlinkPrefixMessage);
+ final RtNetlinkPrefixMessage prefixmsg = (RtNetlinkPrefixMessage) msg;
+ final String expected = "RtNetlinkPrefixMessage{ "
+ + "nlmsghdr{StructNlMsgHdr{ nlmsg_len{60}, nlmsg_type{52(RTM_NEWPREFIX)}, "
+ + "nlmsg_flags{0()}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+ + "prefixmsg{prefix_family: 10, prefix_ifindex: 47, prefix_type: 3, "
+ + "prefix_len: 64, prefix_flags: 3}, "
+ + "IP Prefix{2001:db8:1:1::/64}, "
+ + "preferred lifetime{604800}, valid lifetime{2592000} }";
+ assertEquals(expected, prefixmsg.toString());
+ }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index 2a26ef8..86aa8f1 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -98,6 +98,7 @@
"cts",
"mts-networking",
"mts-tethering",
+ "mcts-tethering",
],
device_common_data: [":ConnectivityTestPreparer"],
}
diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/CarrierConfigSetupTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/CarrierConfigSetupTest.kt
index 46e511e..78b34a8 100644
--- a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/CarrierConfigSetupTest.kt
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/CarrierConfigSetupTest.kt
@@ -123,7 +123,13 @@
"""telephony/com\.android\.internal\.telephony\.flags\.force_iwlan_mms:""" +
""".*ENABLED \(system\)""")
ParcelFileDescriptor.AutoCloseInputStream(
- uiAutomation.executeShellCommand("printflags")).bufferedReader().use { reader ->
+ // If the command fails (for example if printflags is missing) this will return false
+ // and the IWLAN disable will be skipped, which should be fine at it only helps with
+ // flakiness.
+ // This uses "sh -c" to cover that case as if "printflags" is used directly and the
+ // binary is missing, the remote end will crash and the InputStream EOF is never
+ // reached, so the read would hang.
+ uiAutomation.executeShellCommand("sh -c printflags")).bufferedReader().use { reader ->
return reader.lines().anyMatch {
it.contains(flagEnabledRegex)
}
diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
index e634f0e..8e27c62 100644
--- a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
@@ -16,27 +16,167 @@
package com.android.testutils.connectivitypreparer
+import android.Manifest.permission.NETWORK_SETTINGS
import android.content.pm.PackageManager.FEATURE_TELEPHONY
import android.content.pm.PackageManager.FEATURE_WIFI
+import android.net.LinkAddress
+import android.net.Network
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.wifi.WifiInfo
import android.telephony.TelephonyManager
+import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.HexDump
+import com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY
+import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY
+import com.android.testutils.AutoReleaseNetworkCallbackRule
import com.android.testutils.ConnectUtil
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.runAsShell
+import com.android.testutils.tryTest
+import java.io.IOException
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.util.Random
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.fail
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+private const val QUIC_SOCKET_TIMEOUT_MS = 5_000
+private const val QUIC_RETRY_COUNT = 5
+
@RunWith(AndroidJUnit4::class)
class ConnectivityCheckTest {
+ @get:Rule
+ val networkCallbackRule = AutoReleaseNetworkCallbackRule()
+
+ private val logTag = ConnectivityCheckTest::class.simpleName
private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
private val pm by lazy { context.packageManager }
private val connectUtil by lazy { ConnectUtil(context) }
+ // Skip IPv6 checks on virtual devices which do not support it. Tests that require IPv6 will
+ // still fail even if the preparer does not.
+ private fun ipv6Unsupported(wifiSsid: String?) = ConnectUtil.VIRTUAL_SSIDS.contains(
+ WifiInfo.sanitizeSsid(wifiSsid))
+
@Test
fun testCheckWifiSetup() {
if (!pm.hasSystemFeature(FEATURE_WIFI)) return
connectUtil.ensureWifiValidated()
+
+ val (wifiNetwork, wifiSsid) = runAsShell(NETWORK_SETTINGS) {
+ val cb = networkCallbackRule.requestNetwork(
+ NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build()
+ )
+ val capChanged = cb.eventuallyExpect<CapabilitiesChanged>(from = 0)
+ val network = capChanged.network
+ val ssid = capChanged.caps.ssid
+ assertFalse(ssid.isNullOrEmpty(), "No SSID for wifi network $network")
+ // Expect a global IPv6 address, and native or stacked IPv4
+ val lpChange = cb.history.poll(
+ pos = 0,
+ timeoutMs = 30_000L
+ ) {
+ it is LinkPropertiesChanged &&
+ it.network == network &&
+ it.lp.allLinkAddresses.any(LinkAddress::isIpv4) &&
+ (ipv6Unsupported(ssid) || it.lp.hasGlobalIpv6Address())
+ }
+ assertNotNull(lpChange, "Wifi network $network needs an IPv4 address" +
+ if (ipv6Unsupported(ssid)) "" else " and a global IPv6 address")
+
+ Pair(network, ssid)
+ }
+
+ // Checking QUIC is more important on Wi-Fi than cellular, as it finds firewall
+ // configuration problems on Wi-Fi, but cellular is not actionable by the test lab.
+ checkQuic(wifiNetwork, wifiSsid, ipv6 = false)
+ if (!ipv6Unsupported(wifiSsid)) {
+ checkQuic(wifiNetwork, wifiSsid, ipv6 = true)
+ }
+ }
+
+ /**
+ * Check that QUIC is working on the specified network.
+ *
+ * Some tests require QUIC (UDP), and some lab networks have been observed to not let it
+ * through due to firewalling. Ensure that devices are setup on a network that has the proper
+ * allowlists before trying to run the tests.
+ */
+ private fun checkQuic(network: Network, ssid: String, ipv6: Boolean) {
+ // Same endpoint as used in MultinetworkApiTest in CTS
+ val hostname = "connectivitycheck.android.com"
+ val targetAddrs = network.getAllByName(hostname)
+ val bindAddr = if (ipv6) IPV6_ADDR_ANY else IPV4_ADDR_ANY
+ if (targetAddrs.isEmpty()) {
+ Log.d(logTag, "No addresses found for $hostname")
+ return
+ }
+
+ val socket = DatagramSocket(0, bindAddr)
+ tryTest {
+ socket.soTimeout = QUIC_SOCKET_TIMEOUT_MS
+ network.bindSocket(socket)
+
+ // For reference see Version-Independent Properties of QUIC:
+ // https://datatracker.ietf.org/doc/html/rfc8999
+ // This packet just contains a long header with an unsupported version number, to force
+ // a version-negotiation packet in response.
+ val connectionId = ByteArray(8).apply { Random().nextBytes(this) }
+ val quicData = byteArrayOf(
+ // long header
+ 0xc0.toByte(),
+ // version number (should be an unknown version for the server)
+ 0xaa.toByte(), 0xda.toByte(), 0xca.toByte(), 0xca.toByte(),
+ // destination connection ID length
+ 0x08,
+ ) + connectionId + byteArrayOf(
+ // source connection ID length
+ 0x00,
+ ) + ByteArray(1185) // Ensure the packet is 1200 bytes long
+ val targetAddr = targetAddrs.firstOrNull { it.javaClass == bindAddr.javaClass }
+ ?: fail("No ${bindAddr.javaClass} found for $hostname " +
+ "(got ${targetAddrs.joinToString()})")
+ repeat(QUIC_RETRY_COUNT) { i ->
+ socket.send(DatagramPacket(quicData, quicData.size, targetAddr, 443))
+
+ val receivedPacket = DatagramPacket(ByteArray(1500), 1500)
+ try {
+ socket.receive(receivedPacket)
+ } catch (e: IOException) {
+ Log.d(logTag, "No response from $hostname ($targetAddr) on QUIC try $i", e)
+ return@repeat
+ }
+
+ val receivedConnectionId = receivedPacket.data.copyOfRange(7, 7 + 8)
+ if (connectionId.contentEquals(receivedConnectionId)) {
+ return@tryTest
+ } else {
+ val headerBytes = receivedPacket.data.copyOfRange(
+ 0, receivedPacket.length.coerceAtMost(15))
+ Log.d(logTag, "Received invalid connection ID on QUIC try $i: " +
+ HexDump.toHexString(headerBytes))
+ }
+ }
+ fail("QUIC is not working on SSID $ssid connecting to $targetAddr " +
+ "with local source port ${socket.localPort}: check the firewall for UDP port " +
+ "443 access."
+ )
+ } cleanup {
+ socket.close()
+ }
}
@Test
@@ -53,12 +193,16 @@
if (tm.simState == TelephonyManager.SIM_STATE_ABSENT) {
fail("The device has no SIM card inserted. $commonError")
} else if (tm.simState != TelephonyManager.SIM_STATE_READY) {
- fail("The device is not setup with a usable SIM card. Sim state was ${tm.simState}. " +
- commonError)
+ fail(
+ "The device is not setup with a usable SIM card. Sim state was ${tm.simState}. " +
+ commonError
+ )
}
- assertTrue(tm.isDataConnectivityPossible,
+ assertTrue(
+ tm.isDataConnectivityPossible,
"The device has a SIM card, but it does not supports data connectivity. " +
- "Check the data plan, and verify that mobile data is working. " + commonError)
+ "Check the data plan, and verify that mobile data is working. " + commonError
+ )
connectUtil.ensureCellularValidated()
}
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
index 3857810..d60ab59 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
@@ -53,6 +53,10 @@
private const val WIFI_ERROR_BUSY = 2
class ConnectUtil(private val context: Context) {
+ companion object {
+ @JvmStatic
+ val VIRTUAL_SSIDS = listOf("VirtWifi", "AndroidWifi")
+ }
private val TAG = ConnectUtil::class.java.simpleName
private val cm = context.getSystemService(ConnectivityManager::class.java)
@@ -207,9 +211,8 @@
*/
private fun maybeConfigureVirtualNetwork(scanResults: List<ScanResult>): WifiConfiguration? {
// Virtual wifi networks used on the emulator and cloud testing infrastructure
- val virtualSsids = listOf("VirtWifi", "AndroidWifi")
Log.d(TAG, "Wifi scan results: $scanResults")
- val virtualScanResult = scanResults.firstOrNull { virtualSsids.contains(it.SSID) }
+ val virtualScanResult = scanResults.firstOrNull { VIRTUAL_SSIDS.contains(it.SSID) }
?: return null
// Only add the virtual configuration if the virtual AP is detected in scans
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
index ea86281..0624e5f 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
@@ -43,9 +43,14 @@
import androidx.test.platform.app.InstrumentationRegistry
import com.android.modules.utils.build.SdkLevel.isAtLeastS
import java.io.ByteArrayOutputStream
+import java.io.CharArrayWriter
import java.io.File
import java.io.FileOutputStream
+import java.io.FileReader
+import java.io.OutputStream
+import java.io.OutputStreamWriter
import java.io.PrintWriter
+import java.io.Reader
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.concurrent.CompletableFuture
@@ -76,11 +81,44 @@
private const val MAX_DUMPS = 20
private val TAG = ConnectivityDiagnosticsCollector::class.simpleName
+ @JvmStatic
var instance: ConnectivityDiagnosticsCollector? = null
}
+ /**
+ * Indicates tcpdump should be started and written to the diagnostics file on test case failure.
+ */
+ annotation class CollectTcpdumpOnFailure
+
+ private class DumpThread(
+ // Keep a reference to the ParcelFileDescriptor otherwise GC would close it
+ private val fd: ParcelFileDescriptor,
+ private val reader: Reader
+ ) : Thread() {
+ private val writer = CharArrayWriter()
+ override fun run() {
+ reader.copyTo(writer)
+ }
+
+ fun closeAndWriteTo(output: OutputStream?) {
+ join()
+ fd.close()
+ if (output != null) {
+ val outputWriter = OutputStreamWriter(output)
+ outputWriter.write("--- tcpdump stopped at ${ZonedDateTime.now()} ---\n")
+ writer.writeTo(outputWriter)
+ }
+ }
+ }
+
+ private data class TcpdumpRun(val pid: Int, val reader: DumpThread)
+
private var failureHeader: String? = null
+
+ // Accessed from the test listener methods which are synchronized by junit (see TestListener)
+ private var tcpdumpRun: TcpdumpRun? = null
private val buffer = ByteArrayOutputStream()
+ private val failureHeaderExtras = mutableMapOf<String, Any>()
private val collectorDir: File by lazy {
createAndEmptyDirectory(COLLECTOR_DIR)
}
@@ -112,7 +150,7 @@
override fun onSetUp() {
assertNull(instance, "ConnectivityDiagnosticsCollectors were set up multiple times")
instance = this
- TryTestConfig.setDiagnosticsCollector { throwable ->
+ TryTestConfig.swapDiagnosticsCollector { throwable ->
if (runOnFailure(throwable)) {
collectTestFailureDiagnostics(throwable)
}
@@ -155,7 +193,57 @@
flushBufferToFileMetric(testData, baseFilename)
}
+ override fun onTestStart(testData: DataRecord, description: Description) {
+ val tcpdumpAnn = description.annotations.firstOrNull { it is CollectTcpdumpOnFailure }
+ as? CollectTcpdumpOnFailure
+ if (tcpdumpAnn != null) {
+ startTcpdumpForTestcaseIfSupported()
+ }
+ }
+
+ private fun startTcpdumpForTestcaseIfSupported() {
+ if (!DeviceInfoUtils.isDebuggable()) {
+ Log.d(TAG, "Cannot start tcpdump, build is not debuggable")
+ return
+ }
+ if (tcpdumpRun != null) {
+ Log.e(TAG, "Cannot start tcpdump: it is already running")
+ return
+ }
+ // executeShellCommand won't tokenize quoted arguments containing spaces (like pcap filters)
+ // properly, so pass in the command in stdin instead of using sh -c 'command'
+ val fds = instrumentation.uiAutomation.executeShellCommandRw("sh")
+
+ val stdout = fds[0]
+ val stdin = fds[1]
+ ParcelFileDescriptor.AutoCloseOutputStream(stdin).use { writer ->
+ // Echo the current pid, and replace it (with exec) with the tcpdump process, so the
+ // tcpdump pid is known.
+ writer.write(
+ "echo $$; exec su 0 tcpdump -n -i any -U -xx".encodeToByteArray()
+ )
+ }
+ val reader = FileReader(stdout.fileDescriptor).buffered()
+ val tcpdumpPid = Integer.parseInt(reader.readLine())
+ val dumpThread = DumpThread(stdout, reader)
+ dumpThread.start()
+ tcpdumpRun = TcpdumpRun(tcpdumpPid, dumpThread)
+ }
+
+ private fun stopTcpdumpIfRunning(output: OutputStream?) {
+ val run = tcpdumpRun ?: return
+ // Send SIGTERM for graceful shutdown of tcpdump so that it can flush its output
+ executeCommandBlocking("su 0 kill ${run.pid}")
+ run.reader.closeAndWriteTo(output)
+ tcpdumpRun = null
+ }
+
override fun onTestEnd(testData: DataRecord, description: Description) {
+ // onTestFail is called before onTestEnd, so if the test failed tcpdump would already have
+ // been stopped and output dumped. Here this stops tcpdump if the test succeeded, throwing
+ // away its output.
+ stopTcpdumpIfRunning(output = null)
+
// Tests may call methods like collectDumpsysConnectivity to collect diagnostics at any time
// during the run, for example to observe state at various points to investigate a flake
// and compare passing/failing cases.
@@ -194,6 +282,7 @@
fos.write("\n".toByteArray())
}
fos.write(buffer.toByteArray())
+ stopTcpdumpIfRunning(fos)
}
failureHeader = null
buffer.reset()
@@ -218,6 +307,8 @@
val canUseShell = !isAtLeastS() ||
instr.uiAutomation.getAdoptedShellPermissions().isNullOrEmpty()
val headerObj = JSONObject()
+ failureHeaderExtras.forEach { (k, v) -> headerObj.put(k, v) }
+ failureHeaderExtras.clear()
if (canUseShell) {
runAsShell(READ_PRIVILEGED_PHONE_STATE, NETWORK_SETTINGS) {
headerObj.apply {
@@ -332,6 +423,15 @@
}
}
+ /**
+ * Add a key->value attribute to the failure data, to be written to the diagnostics file.
+ *
+ * <p>This is to be called by tests that know they will fail.
+ */
+ fun addFailureAttribute(key: String, value: Any) {
+ failureHeaderExtras[key] = value
+ }
+
private fun maybeWriteExceptionContext(writer: PrintWriter, exceptionContext: Throwable?) {
if (exceptionContext == null) return
writer.println("At: ")
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java
index ce55fdc..31879af 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java
@@ -16,6 +16,10 @@
package com.android.testutils;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
+
+import android.os.Build;
+import android.os.SystemProperties;
import android.os.VintfRuntimeInfo;
import android.text.TextUtils;
import android.util.Pair;
@@ -173,4 +177,14 @@
final KVersion from = DeviceInfoUtils.getMajorMinorSubminorVersion(version);
return current.isAtLeast(from);
}
+
+ /**
+ * Check if the current build is a debuggable build.
+ */
+ public static boolean isDebuggable() {
+ if (isAtLeastS()) {
+ return Build.isDebuggable();
+ }
+ return SystemProperties.getInt("ro.debuggable", 0) == 1;
+ }
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
index ae43c15..d9c51e5 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -32,6 +32,7 @@
import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
import com.android.testutils.RecorderCallback.CallbackEntry.Losing
import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
import com.android.testutils.RecorderCallback.CallbackEntry.Resumed
import com.android.testutils.RecorderCallback.CallbackEntry.Suspended
import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
@@ -66,6 +67,12 @@
// constructor by specifying override.
abstract val network: Network
+ data class Reserved private constructor(
+ override val network: Network,
+ val caps: NetworkCapabilities
+ ): CallbackEntry() {
+ constructor(caps: NetworkCapabilities) : this(NULL_NETWORK, caps)
+ }
data class Available(override val network: Network) : CallbackEntry()
data class CapabilitiesChanged(
override val network: Network,
@@ -100,6 +107,8 @@
// Convenience constants for expecting a type
companion object {
@JvmField
+ val RESERVED = Reserved::class
+ @JvmField
val AVAILABLE = Available::class
@JvmField
val NETWORK_CAPS_UPDATED = CapabilitiesChanged::class
@@ -127,6 +136,11 @@
val history = backingRecord.newReadHead()
val mark get() = history.mark
+ override fun onReserved(caps: NetworkCapabilities) {
+ Log.d(logTag, "onReserved $caps")
+ history.add(Reserved(caps))
+ }
+
override fun onAvailable(network: Network) {
Log.d(logTag, "onAvailable $network")
history.add(Available(network))
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt
index 21bd60c..a0078d2 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkOfferCallback.kt
@@ -52,10 +52,11 @@
inline fun <reified T : CallbackEntry> expectCallbackThat(
crossinline predicate: (T) -> Boolean
- ) {
+ ): T {
val event = history.poll(timeoutMs)
?: fail("Did not receive callback after ${timeoutMs}ms")
if (event !is T || !predicate(event)) fail("Received unexpected callback $event")
+ return event
}
fun expectOnNetworkNeeded(capabilities: NetworkCapabilities) =
diff --git a/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt b/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
index f6168af..a99359b 100644
--- a/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
+++ b/staticlibs/testutils/host/java/com/android/testutils/ConnectivityTestTargetPreparer.kt
@@ -18,12 +18,15 @@
import com.android.ddmlib.testrunner.TestResult
import com.android.tradefed.config.Option
+import com.android.tradefed.invoker.ExecutionFiles.FilesKey
import com.android.tradefed.invoker.TestInformation
+import com.android.tradefed.log.LogUtil.CLog
import com.android.tradefed.result.CollectingTestListener
import com.android.tradefed.result.ddmlib.DefaultRemoteAndroidTestRunner
import com.android.tradefed.targetprep.BaseTargetPreparer
import com.android.tradefed.targetprep.TargetSetupError
import com.android.tradefed.targetprep.suite.SuiteApkInstaller
+import java.io.File
private const val CONNECTIVITY_CHECKER_APK = "ConnectivityTestPreparer.apk"
private const val CONNECTIVITY_PKG_NAME = "com.android.testutils.connectivitypreparer"
@@ -48,7 +51,41 @@
* --test-arg com.android.testutils.ConnectivityTestTargetPreparer:ignore-wifi-check:true".
*/
open class ConnectivityTestTargetPreparer : BaseTargetPreparer() {
- private val installer = SuiteApkInstaller()
+ private val installer = ApkInstaller()
+
+ private class ApkInstaller : SuiteApkInstaller() {
+ override fun getLocalPathForFilename(
+ testInfo: TestInformation,
+ apkFileName: String
+ ): File {
+ if (apkFileName == CONNECTIVITY_CHECKER_APK) {
+ // For the connectivity checker APK, explicitly look for it in the directory of the
+ // host-side preparer.
+ // This preparer is part of the net-tests-utils-host-common library, which includes
+ // the checker APK via device_common_data in its build rule. Both need to be at the
+ // same version so that the preparer calls the right test methods in the checker
+ // APK.
+ // The default strategy for finding test files is to do a recursive search in test
+ // directories, which may find wrong files in wrong directories. In particular,
+ // if some MTS test includes the checker APK, and that test is linked to a module
+ // that boards the train at a version different from this target preparer, there
+ // could be a version difference between the APK and the host-side preparer.
+ // Explicitly looking for the APK in the host-side preparer directory ensures that
+ // it uses the version that was packaged together with the host-side preparer.
+ val testsDir = testInfo.executionFiles().get(FilesKey.TESTS_DIRECTORY)
+ val f = File(testsDir, "net-tests-utils-host-common/$CONNECTIVITY_CHECKER_APK")
+ if (f.isFile) {
+ return f
+ }
+ // When running locally via atest, device_common_data does cause the APK to be put
+ // into the test temp directory, so recursive search is still used to find it in the
+ // directory of the test module that is being run. This is fine because atest runs
+ // are on local trees that do not have versioning problems.
+ CLog.i("APK not found at $f, falling back to recursive search")
+ }
+ return super.getLocalPathForFilename(testInfo, apkFileName)
+ }
+ }
@Option(
name = IGNORE_WIFI_CHECK,
@@ -179,7 +216,7 @@
.contains(":deny")
}
- private fun refreshTime(testInfo: TestInformation,) {
+ private fun refreshTime(testInfo: TestInformation) {
// Forces a synchronous time refresh using the network. Time is fetched synchronously but
// this does not guarantee that system time is updated when it returns.
// This avoids flakes where the system clock rolls back, for example when using test
diff --git a/staticlibs/testutils/host/python/tether_utils.py b/staticlibs/testutils/host/python/tether_utils.py
index 702b596..c63de1f 100644
--- a/staticlibs/testutils/host/python/tether_utils.py
+++ b/staticlibs/testutils/host/python/tether_utils.py
@@ -108,3 +108,6 @@
server.unregisterAll()
# Teardown the hotspot.
server.stopAllTethering()
+ # Some test cases would disable wifi, e.g. cellular upstream tests.
+ # Reconnect to it if feasible.
+ server.reconnectWifiIfSupported()
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt b/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
index dcd422c..45c69c9 100644
--- a/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
@@ -75,13 +75,21 @@
*/
object TryTestConfig {
- internal var diagnosticsCollector: Consumer<Throwable>? = null
+ private var diagnosticsCollector: Consumer<Throwable>? = null
/**
* Set the diagnostics collector to be used in case of failure in [tryTest].
+ *
+ * @return The previous collector.
*/
- fun setDiagnosticsCollector(collector: Consumer<Throwable>) {
+ fun swapDiagnosticsCollector(collector: Consumer<Throwable>?): Consumer<Throwable>? {
+ val oldCollector = diagnosticsCollector
diagnosticsCollector = collector
+ return oldCollector
+ }
+
+ fun reportError(e: Throwable) {
+ diagnosticsCollector?.accept(e)
}
}
@@ -90,14 +98,10 @@
try {
Result.success(block())
} catch (e: Throwable) {
- TryTestConfig.diagnosticsCollector?.accept(e)
Result.failure(e)
- })
+ }, skipErrorReporting = false)
-// Some downstream branches have an older kotlin that doesn't know about value classes.
-// TODO : Change this to "value class" when aosp no longer merges into such branches.
-@Suppress("INLINE_CLASS_DEPRECATED")
-inline class TryExpr<T>(val result: Result<T>) {
+class TryExpr<T>(val result: Result<T>, val skipErrorReporting: Boolean) {
inline infix fun <reified E : Throwable> catch(block: (E) -> T): TryExpr<T> {
val originalException = result.exceptionOrNull()
if (originalException !is E) return this
@@ -105,23 +109,32 @@
Result.success(block(originalException))
} catch (e: Throwable) {
Result.failure(e)
- })
+ }, this.skipErrorReporting)
}
@CheckReturnValue
inline infix fun cleanupStep(block: () -> Unit): TryExpr<T> {
+ // Report errors before the cleanup step, but after catch blocks that may suppress it
+ val originalException = result.exceptionOrNull()
+ var nextSkipErrorReporting = skipErrorReporting
+ if (!skipErrorReporting && originalException != null) {
+ TryTestConfig.reportError(originalException)
+ nextSkipErrorReporting = true
+ }
try {
block()
} catch (e: Throwable) {
- val originalException = result.exceptionOrNull()
- return TryExpr(if (null == originalException) {
- Result.failure(e)
+ return if (null == originalException) {
+ if (!skipErrorReporting) {
+ TryTestConfig.reportError(e)
+ }
+ TryExpr(Result.failure(e), skipErrorReporting = true)
} else {
originalException.addSuppressed(e)
- Result.failure(originalException)
- })
+ TryExpr(Result.failure(originalException), true)
+ }
}
- return this
+ return TryExpr(result, nextSkipErrorReporting)
}
inline infix fun cleanup(block: () -> Unit): T = cleanupStep(block).result.getOrThrow()
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index bb1009b..60a02fb 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -147,6 +147,7 @@
// meaning @hide APIs in framework-connectivity are resolved before @SystemApi
// stubs in framework
"framework-connectivity.impl",
+ "framework-connectivity-b.impl",
"framework-connectivity-t.impl",
"framework-tethering.impl",
"framework",
diff --git a/tests/common/java/android/net/CaptivePortalTest.java b/tests/common/java/android/net/CaptivePortalTest.java
index 15d3398..6655827 100644
--- a/tests/common/java/android/net/CaptivePortalTest.java
+++ b/tests/common/java/android/net/CaptivePortalTest.java
@@ -19,6 +19,7 @@
import static org.junit.Assert.assertEquals;
import android.os.Build;
+import android.os.IBinder;
import android.os.RemoteException;
import androidx.test.filters.SmallTest;
@@ -55,6 +56,10 @@
mCode = request;
}
+ @Override
+ public void setDelegateUid(int uid, IBinder binder, IIntResultListener listener) {
+ }
+
// This is only @Override on R-
public void logEvent(int eventId, String packageName) throws RemoteException {
mCode = eventId;
diff --git a/tests/common/java/android/net/NetworkAgentConfigTest.kt b/tests/common/java/android/net/NetworkAgentConfigTest.kt
index d640a73..fe869f8 100644
--- a/tests/common/java/android/net/NetworkAgentConfigTest.kt
+++ b/tests/common/java/android/net/NetworkAgentConfigTest.kt
@@ -20,6 +20,7 @@
import androidx.test.runner.AndroidJUnit4
import com.android.modules.utils.build.SdkLevel.isAtLeastS
import com.android.modules.utils.build.SdkLevel.isAtLeastT
+import com.android.modules.utils.build.SdkLevel.isAtLeastV
import com.android.testutils.ConnectivityModuleTest
import com.android.testutils.assertParcelingIsLossless
import org.junit.Assert.assertEquals
@@ -47,6 +48,9 @@
setLocalRoutesExcludedForVpn(true)
setVpnRequiresValidation(true)
}
+ if (isAtLeastV()) {
+ setSkipNativeNetworkCreation(true)
+ }
}.build()
assertParcelingIsLossless(config)
}
@@ -71,6 +75,9 @@
setLocalRoutesExcludedForVpn(true)
setVpnRequiresValidation(true)
}
+ if (isAtLeastV()) {
+ setSkipNativeNetworkCreation(true)
+ }
}.build()
assertTrue(config.isExplicitlySelected())
@@ -79,6 +86,9 @@
assertFalse(config.isPartialConnectivityAcceptable())
assertTrue(config.isUnvalidatedConnectivityAcceptable())
assertEquals("TEST_NETWORK", config.getLegacyTypeName())
+ if (isAtLeastV()) {
+ assertTrue(config.shouldSkipNativeNetworkCreation())
+ }
if (isAtLeastT()) {
assertTrue(config.areLocalRoutesExcludedForVpn())
assertTrue(config.isVpnValidationRequired())
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 0f0e2f1..d694637 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -382,6 +382,7 @@
netCap.setAllowedUids(allowedUids);
netCap.setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2));
netCap.setUids(uids);
+ netCap.setReservationId(42);
}
netCap.setOwnerUid(123);
@@ -1493,4 +1494,42 @@
// nc1 and nc2 are the same since invalid capability is ignored
assertEquals(nc1, nc2);
}
+
+ @Test
+ public void testReservationIdMatching() {
+ final NetworkCapabilities requestNc = new NetworkCapabilities();
+ final NetworkCapabilities reservationNc = new NetworkCapabilities();
+ reservationNc.setReservationId(42);
+
+ final NetworkCapabilities reservedNetworkNc = new NetworkCapabilities(reservationNc);
+ final NetworkCapabilities otherNetworkNc = new NetworkCapabilities();
+ final NetworkCapabilities otherReservedNetworkNc = new NetworkCapabilities();
+ otherReservedNetworkNc.setReservationId(99);
+ final NetworkCapabilities offerNc = new NetworkCapabilities();
+ offerNc.setReservationId(NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS);
+
+ // A regular request can match any network or offer except one with MATCH_ALL_RESERVATIONS
+ assertTrue(requestNc.satisfiedByNetworkCapabilities(reservedNetworkNc));
+ assertTrue(requestNc.satisfiedByNetworkCapabilities(otherNetworkNc));
+ assertTrue(requestNc.satisfiedByNetworkCapabilities(otherReservedNetworkNc));
+ assertFalse(requestNc.satisfiedByNetworkCapabilities(offerNc));
+
+ // A reservation request can only match the reservedNetwork and the blanket offer.
+ assertTrue(reservationNc.satisfiedByNetworkCapabilities(reservedNetworkNc));
+ assertFalse(reservationNc.satisfiedByNetworkCapabilities(otherNetworkNc));
+ assertFalse(reservationNc.satisfiedByNetworkCapabilities(otherReservedNetworkNc));
+ assertTrue(reservationNc.satisfiedByNetworkCapabilities(offerNc));
+ }
+
+ @Test @IgnoreUpTo(SC_V2)
+ public void testReservationIdEquals() {
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ nc.setReservationId(42);
+ final NetworkCapabilities other = new NetworkCapabilities(nc);
+
+ assertEquals(nc, other);
+
+ nc.setReservationId(43);
+ assertNotEquals(nc, other);
+ }
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
index ad98a29..ac60b0f 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
@@ -25,6 +25,7 @@
import android.content.Context;
import android.net.TetheringInterface;
import android.net.cts.util.CtsTetheringUtils;
+import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiSsid;
@@ -37,6 +38,7 @@
public class TetheringTest {
private CtsTetheringUtils mCtsTetheringUtils;
private TetheringHelperClient mTetheringHelperClient;
+ private TestTetheringEventCallback mTetheringEventCallback;
@Before
public void setUp() throws Exception {
@@ -44,11 +46,14 @@
mCtsTetheringUtils = new CtsTetheringUtils(targetContext);
mTetheringHelperClient = new TetheringHelperClient(targetContext);
mTetheringHelperClient.bind();
+ mTetheringEventCallback = mCtsTetheringUtils.registerTetheringEventCallback();
}
@After
public void tearDown() throws Exception {
mTetheringHelperClient.unbind();
+ mCtsTetheringUtils.unregisterTetheringEventCallback(mTetheringEventCallback);
+ mCtsTetheringUtils.stopAllTethering();
}
/**
@@ -57,24 +62,20 @@
*/
@Test
public void testSoftApConfigurationRedactedForOtherUids() throws Exception {
- final CtsTetheringUtils.TestTetheringEventCallback tetherEventCallback =
- mCtsTetheringUtils.registerTetheringEventCallback();
+ mTetheringEventCallback.assumeWifiTetheringSupported(
+ getInstrumentation().getTargetContext());
SoftApConfiguration softApConfig = new SoftApConfiguration.Builder()
.setWifiSsid(WifiSsid.fromBytes("This is an SSID!"
.getBytes(StandardCharsets.UTF_8))).build();
final TetheringInterface tetheringInterface =
- mCtsTetheringUtils.startWifiTethering(tetherEventCallback, softApConfig);
+ mCtsTetheringUtils.startWifiTethering(mTetheringEventCallback, softApConfig);
assertNotNull(tetheringInterface);
assertEquals(softApConfig, tetheringInterface.getSoftApConfiguration());
- try {
- TetheringInterface tetheringInterfaceForApp2 =
- mTetheringHelperClient.getTetheredWifiInterface();
- assertNotNull(tetheringInterfaceForApp2);
- assertNull(tetheringInterfaceForApp2.getSoftApConfiguration());
- assertEquals(
- tetheringInterface.getInterface(), tetheringInterfaceForApp2.getInterface());
- } finally {
- mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
- }
+ TetheringInterface tetheringInterfaceForApp2 =
+ mTetheringHelperClient.getTetheredWifiInterface();
+ assertNotNull(tetheringInterfaceForApp2);
+ assertNull(tetheringInterfaceForApp2.getSoftApConfiguration());
+ assertEquals(
+ tetheringInterface.getInterface(), tetheringInterfaceForApp2.getInterface());
}
}
diff --git a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
index ae572e6..b5e2450 100644
--- a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
@@ -91,8 +91,8 @@
}
private String[] getSysctlDirs() throws Exception {
- String interfaceDirs[] = mDevice.executeAdbCommand("shell", "ls", "-1",
- IPV6_SYSCTL_DIR).split("\n");
+ String[] interfaceDirs = mDevice.executeShellCommand("ls -1 " + IPV6_SYSCTL_DIR)
+ .split("\n");
List<String> interfaceDirsList = new ArrayList<String>(Arrays.asList(interfaceDirs));
interfaceDirsList.remove("all");
interfaceDirsList.remove("lo");
@@ -109,13 +109,13 @@
}
public int readIntFromPath(String path) throws Exception {
- String mode = mDevice.executeAdbCommand("shell", "stat", "-c", "%a", path).trim();
- String user = mDevice.executeAdbCommand("shell", "stat", "-c", "%u", path).trim();
- String group = mDevice.executeAdbCommand("shell", "stat", "-c", "%g", path).trim();
+ String mode = mDevice.executeShellCommand("stat -c %a " + path).trim();
+ String user = mDevice.executeShellCommand("stat -c %u " + path).trim();
+ String group = mDevice.executeShellCommand("stat -c %g " + path).trim();
assertEquals(mode, "644");
assertEquals(user, "0");
assertEquals(group, "0");
- return Integer.parseInt(mDevice.executeAdbCommand("shell", "cat", path).trim());
+ return Integer.parseInt(mDevice.executeShellCommand("cat " + path).trim());
}
/**
@@ -191,7 +191,7 @@
assumeTrue(new DeviceSdkLevel(mDevice).isDeviceAtLeastV());
String path = "/proc/sys/net/ipv4/tcp_congestion_control";
- String value = mDevice.executeAdbCommand("shell", "cat", path).trim();
+ String value = mDevice.executeShellCommand("cat " + path).trim();
assertEquals("cubic", value);
}
}
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
index 49688cc..6da7e9a 100644
--- a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -89,6 +89,11 @@
ctsNetUtils.expectNetworkIsSystemDefault(network)
}
+ @Rpc(description = "Reconnect to wifi if supported.")
+ fun reconnectWifiIfSupported() {
+ ctsNetUtils.reconnectWifiIfSupported()
+ }
+
@Rpc(description = "Unregister all connections.")
fun unregisterAll() {
cbHelper.unregisterAll()
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 55b6494..cb0e575 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -42,6 +42,7 @@
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="{PACKAGE}" />
+ <option name="shell-timeout" value="1500s"/>
<option name="runtime-hint" value="9m4s" />
<option name="hidden-api-checks" value="false" />
<option name="isolated-storage" value="false" />
diff --git a/tests/cts/net/jni/NativeMultinetworkJni.cpp b/tests/cts/net/jni/NativeMultinetworkJni.cpp
index f2214a3..1d848ec 100644
--- a/tests/cts/net/jni/NativeMultinetworkJni.cpp
+++ b/tests/cts/net/jni/NativeMultinetworkJni.cpp
@@ -415,9 +415,17 @@
strlcpy(dst, buf, size);
}
+static jobject create_query_test_result(JNIEnv* env, uint16_t src_port, int attempts, int errnum) {
+ jclass clazz = env->FindClass(
+ "android/net/cts/MultinetworkApiTest$QueryTestResult");
+ jmethodID ctor = env->GetMethodID(clazz, "<init>", "(III)V");
+
+ return env->NewObject(clazz, ctor, src_port, attempts, errnum);
+}
+
extern "C"
-JNIEXPORT jint Java_android_net_cts_MultinetworkApiTest_runDatagramCheck(
- JNIEnv*, jclass, jlong nethandle) {
+JNIEXPORT jobject Java_android_net_cts_MultinetworkApiTest_runDatagramCheck(
+ JNIEnv* env, jclass, jlong nethandle, jint src_port) {
const struct addrinfo kHints = {
.ai_flags = AI_ADDRCONFIG,
.ai_family = AF_UNSPEC,
@@ -433,7 +441,7 @@
LOGD("android_getaddrinfofornetwork(%llu, %s) returned rval=%d errno=%d",
handle, kHostname, rval, errno);
freeaddrinfo(res);
- return -errno;
+ return create_query_test_result(env, 0, 0, errno);
}
// Rely upon getaddrinfo sorting the best destination to the front.
@@ -442,7 +450,7 @@
LOGD("socket(%d, %d, %d) failed, errno=%d",
res->ai_family, res->ai_socktype, res->ai_protocol, errno);
freeaddrinfo(res);
- return -errno;
+ return create_query_test_result(env, 0, 0, errno);
}
rval = android_setsocknetwork(handle, fd);
@@ -451,7 +459,31 @@
if (rval != 0) {
close(fd);
freeaddrinfo(res);
- return -errno;
+ return create_query_test_result(env, 0, 0, errno);
+ }
+
+ sockaddr_storage src_addr;
+ socklen_t src_addrlen = sizeof(src_addr);
+ if (src_port) {
+ if (res->ai_family == AF_INET6) {
+ *reinterpret_cast<sockaddr_in6*>(&src_addr) = (sockaddr_in6) {
+ .sin6_family = AF_INET6,
+ .sin6_port = htons(src_port),
+ .sin6_addr = in6addr_any,
+ };
+ } else {
+ *reinterpret_cast<sockaddr_in*>(&src_addr) = (sockaddr_in) {
+ .sin_family = AF_INET,
+ .sin_port = htons(src_port),
+ .sin_addr = { .s_addr = INADDR_ANY },
+ };
+ }
+ if (bind(fd, (sockaddr *)&src_addr, src_addrlen) != 0) {
+ LOGD("Error binding to port %d", src_port);
+ close(fd);
+ freeaddrinfo(res);
+ return create_query_test_result(env, 0, 0, errno);
+ }
}
char addrstr[kSockaddrStrLen+1];
@@ -462,19 +494,28 @@
if (rval != 0) {
close(fd);
freeaddrinfo(res);
- return -errno;
+ return create_query_test_result(env, 0, 0, errno);
}
freeaddrinfo(res);
- struct sockaddr_storage src_addr;
- socklen_t src_addrlen = sizeof(src_addr);
if (getsockname(fd, (struct sockaddr *)&src_addr, &src_addrlen) != 0) {
close(fd);
- return -errno;
+ return create_query_test_result(env, 0, 0, errno);
}
sockaddr_ntop((const struct sockaddr *)&src_addr, sizeof(src_addr), addrstr, sizeof(addrstr));
LOGD("... from %s", addrstr);
+ uint16_t socket_src_port;
+ if (res->ai_family == AF_INET6) {
+ socket_src_port = ntohs(reinterpret_cast<sockaddr_in6*>(&src_addr)->sin6_port);
+ } else if (src_addr.ss_family == AF_INET) {
+ socket_src_port = ntohs(reinterpret_cast<sockaddr_in*>(&src_addr)->sin_port);
+ } else {
+ LOGD("Invalid source address family %d", src_addr.ss_family);
+ close(fd);
+ return create_query_test_result(env, 0, 0, EAFNOSUPPORT);
+ }
+
// Don't let reads or writes block indefinitely.
const struct timeval timeo = { 2, 0 }; // 2 seconds
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeo, sizeof(timeo));
@@ -503,7 +544,7 @@
errnum = errno;
LOGD("send(QUIC packet) returned sent=%zd, errno=%d", sent, errnum);
close(fd);
- return -errnum;
+ return create_query_test_result(env, socket_src_port, i + 1, errnum);
}
rcvd = recv(fd, response, sizeof(response), 0);
@@ -521,18 +562,19 @@
LOGD("Does this network block UDP port %s?", kPort);
}
close(fd);
- return -EPROTO;
+ return create_query_test_result(env, socket_src_port, i + 1,
+ rcvd <= 0 ? errnum : EPROTO);
}
int conn_id_cmp = memcmp(quic_packet + 6, response + 7, 8);
if (conn_id_cmp != 0) {
LOGD("sent and received connection IDs do not match");
close(fd);
- return -EPROTO;
+ return create_query_test_result(env, socket_src_port, i + 1, EPROTO);
}
// TODO: Replace this quick 'n' dirty test with proper QUIC-capable code.
close(fd);
- return 0;
+ return create_query_test_result(env, socket_src_port, i + 1, 0);
}
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 9ac2c67..320622b 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -19,7 +19,10 @@
package android.net.cts
+import android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG
import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.FEATURE_AUTOMOTIVE
import android.content.pm.PackageManager.FEATURE_WIFI
import android.net.ConnectivityManager
import android.net.Network
@@ -49,6 +52,7 @@
import android.os.Handler
import android.os.HandlerThread
import android.os.PowerManager
+import android.os.UserManager
import android.platform.test.annotations.AppModeFull
import android.provider.DeviceConfig
import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
@@ -140,15 +144,34 @@
fun turnScreenOff() {
if (!wakeLock.isHeld()) wakeLock.acquire()
runShellCommandOrThrow("input keyevent KEYCODE_SLEEP")
- val result = pollingCheck({ !powerManager.isInteractive() }, timeout_ms = 2000)
- assertThat(result).isTrue()
+ waitForInteractiveState(false)
}
fun turnScreenOn() {
if (wakeLock.isHeld()) wakeLock.release()
runShellCommandOrThrow("input keyevent KEYCODE_WAKEUP")
- val result = pollingCheck({ powerManager.isInteractive() }, timeout_ms = 2000)
- assertThat(result).isTrue()
+ waitForInteractiveState(true)
+ }
+
+ private fun waitForInteractiveState(interactive: Boolean) {
+ // TODO(b/366037029): This test condition should be removed once
+ // PowerManager#isInteractive is fully implemented on automotive
+ // form factor with visible background user.
+ if (isAutomotiveWithVisibleBackgroundUser()) {
+ // Wait for 2 seconds to ensure the interactive state is updated.
+ // This is a workaround for b/366037029.
+ Thread.sleep(2000L)
+ } else {
+ val result = pollingCheck({ powerManager.isInteractive() }, timeout_ms = 2000)
+ assertThat(result).isEqualTo(interactive)
+ }
+ }
+
+ private fun isAutomotiveWithVisibleBackgroundUser(): Boolean {
+ val packageManager = context.getPackageManager()
+ val userManager = context.getSystemService(UserManager::class.java)!!
+ return (packageManager.hasSystemFeature(FEATURE_AUTOMOTIVE)
+ && userManager.isVisibleBackgroundUsersSupported)
}
@BeforeClass
@@ -156,16 +179,18 @@
@Suppress("ktlint:standard:no-multi-spaces")
fun setupOnce() {
// TODO: assertions thrown in @BeforeClass / @AfterClass are not well supported in the
- // test infrastructure. Consider saving excepion and throwing it in setUp().
+ // test infrastructure. Consider saving exception and throwing it in setUp().
+
// APF must run when the screen is off and the device is not interactive.
turnScreenOff()
+
// Wait for APF to become active.
Thread.sleep(1000)
// TODO: check that there is no active wifi network. Otherwise, ApfFilter has already been
// created.
// APF adb cmds are only implemented in ApfFilter.java. Enable experiment to prevent
// LegacyApfFilter.java from being used.
- runAsShell(WRITE_DEVICE_CONFIG) {
+ runAsShell(WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG) {
DeviceConfig.setProperty(
NAMESPACE_CONNECTIVITY,
APF_NEW_RA_FILTER_VERSION,
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 3a8252a..9457a42 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -145,6 +145,7 @@
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.ConnectivitySettingsManager;
+import android.net.DnsResolver;
import android.net.InetAddresses;
import android.net.IpSecManager;
import android.net.IpSecManager.UdpEncapsulationSocket;
@@ -201,6 +202,7 @@
import com.android.internal.util.ArrayUtils;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DnsPacket;
import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
import com.android.networkstack.apishim.ConstantsShim;
import com.android.networkstack.apishim.NetworkInformationShimImpl;
@@ -249,7 +251,6 @@
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
-import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
@@ -306,6 +307,7 @@
private static final int LISTEN_ACTIVITY_TIMEOUT_MS = 30_000;
private static final int NO_CALLBACK_TIMEOUT_MS = 100;
private static final int NETWORK_REQUEST_TIMEOUT_MS = 3000;
+ private static final int DNS_REQUEST_TIMEOUT_MS = 1000;
private static final int SOCKET_TIMEOUT_MS = 100;
private static final int NUM_TRIES_MULTIPATH_PREF_CHECK = 20;
private static final long INTERVAL_MULTIPATH_PREF_CHECK_MS = 500;
@@ -861,6 +863,55 @@
});
}
+ @NonNull
+ private static String getDeviceIpv6AddressThroughDnsQuery(Network network) throws Exception {
+ final InetAddress dnsAddr = getAddrByName("ns1.google.com", AF_INET6);
+ assertNotNull("IPv6 address for ns1.google.com should not be null", dnsAddr);
+
+ try (DatagramSocket udpSocket = new DatagramSocket()) {
+ network.bindSocket(udpSocket);
+
+ final DnsPacket queryDnsPkt = new DnsPacket(
+ new DnsPacket.DnsHeader(new Random().nextInt(), DnsResolver.FLAG_EMPTY,
+ 1 /* qdcount */,
+ 0 /* ancount */),
+ List.of(DnsPacket.DnsRecord.makeQuestion("o-o.myaddr.l.google.com",
+ DnsResolver.TYPE_TXT, DnsResolver.CLASS_IN)),
+ List.of() /* an */
+ );
+ final byte[] queryDnsRawBytes = queryDnsPkt.getBytes();
+ final byte[] receiveBuffer = new byte[1500];
+ final int maxRetry = 3;
+ for (int attempt = 1; attempt <= maxRetry; ++attempt) {
+ try {
+ final DatagramPacket queryUdpPkt = new DatagramPacket(queryDnsRawBytes,
+ queryDnsRawBytes.length, dnsAddr, 53 /* port */);
+ udpSocket.send(queryUdpPkt);
+
+ final DatagramPacket replyUdpPkt = new DatagramPacket(receiveBuffer,
+ receiveBuffer.length);
+ udpSocket.setSoTimeout(DNS_REQUEST_TIMEOUT_MS);
+ udpSocket.receive(replyUdpPkt);
+ break;
+ } catch (IOException e) {
+ if (attempt == maxRetry) {
+ throw e; // If the last attempt fails, rethrow the exception.
+ } else {
+ Log.e(TAG, "DNS request failed (attempt " + attempt + ")" + e);
+ }
+ }
+ }
+
+ final DnsPacket replyDnsPkt = new DnsPacket(receiveBuffer);
+ final DnsPacket.DnsRecord answerRecord = replyDnsPkt.getRecords(
+ DnsPacket.ANSECTION).get(0);
+ final byte[] txtReplyRecord = answerRecord.getRR();
+ final byte dataLength = txtReplyRecord[0];
+ assertEquals(dataLength, txtReplyRecord.length - 1);
+ return new String(Arrays.copyOfRange(txtReplyRecord, 1, txtReplyRecord.length));
+ }
+ }
+
/**
* Tests that connections can be opened on WiFi and cellphone networks,
* and that they are made from different IP addresses.
@@ -886,8 +937,30 @@
// Verify that the IP addresses that the requests appeared to come from are actually on the
// respective networks.
- assertOnNetwork(wifiAddressString, wifiNetwork);
- assertOnNetwork(cellAddressString, cellNetwork);
+ final InetAddress wifiAddress = InetAddresses.parseNumericAddress(wifiAddressString);
+ final LinkProperties wifiLinkProperties = mCm.getLinkProperties(wifiNetwork);
+ // To make sure that the request went out on the right network, check that
+ // the IP address seen by the server is assigned to the expected network.
+ // We can only do this for IPv6 addresses, because in IPv4 we will likely
+ // have a private IPv4 address, and that won't match what the server sees.
+ if (wifiAddress instanceof Inet6Address) {
+ assertContains(wifiLinkProperties.getAddresses(), wifiAddress);
+ }
+
+ final LinkProperties cellLinkProperties = mCm.getLinkProperties(cellNetwork);
+ final InetAddress cellAddress = InetAddresses.parseNumericAddress(cellAddressString);
+ final List<InetAddress> cellNetworkAddresses = cellLinkProperties.getAddresses();
+ // In userdebug build, on cellular network, if the onNetwork check failed, we also try to
+ // re-verify it by obtaining the IP address through DNS query.
+ if (cellAddress instanceof Inet6Address) {
+ if (DeviceInfoUtils.isDebuggable() && !cellNetworkAddresses.contains(cellAddress)) {
+ final InetAddress ipv6AddressThroughDns = InetAddresses.parseNumericAddress(
+ getDeviceIpv6AddressThroughDnsQuery(cellNetwork));
+ assertContains(cellNetworkAddresses, ipv6AddressThroughDns);
+ } else {
+ assertContains(cellNetworkAddresses, cellAddress);
+ }
+ }
assertFalse("Unexpectedly equal: " + wifiNetwork, wifiNetwork.equals(cellNetwork));
}
@@ -919,17 +992,6 @@
}
}
- private void assertOnNetwork(String adressString, Network network) throws UnknownHostException {
- InetAddress address = InetAddress.getByName(adressString);
- LinkProperties linkProperties = mCm.getLinkProperties(network);
- // To make sure that the request went out on the right network, check that
- // the IP address seen by the server is assigned to the expected network.
- // We can only do this for IPv6 addresses, because in IPv4 we will likely
- // have a private IPv4 address, and that won't match what the server sees.
- if (address instanceof Inet6Address) {
- assertContains(linkProperties.getAddresses(), address);
- }
- }
private static<T> void assertContains(Collection<T> collection, T element) {
assertTrue(element + " not found in " + collection, collection.contains(element));
@@ -1553,7 +1615,7 @@
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
final ContentResolver resolver = mContext.getContentResolver();
mCtsNetUtils.ensureWifiConnected();
- final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID());
+ final String ssid = unquoteSSID(getSSID());
final String oldMeteredSetting = getWifiMeteredStatus(ssid);
final String oldMeteredMultipathPreference = Settings.Global.getString(
resolver, NETWORK_METERED_MULTIPATH_PREFERENCE);
@@ -1566,7 +1628,7 @@
// since R.
final Network network = setWifiMeteredStatusAndWait(ssid, true /* isMetered */,
false /* waitForValidation */);
- assertEquals(ssid, unquoteSSID(mWifiManager.getConnectionInfo().getSSID()));
+ assertEquals(ssid, unquoteSSID(getSSID()));
assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
NET_CAPABILITY_NOT_METERED), false);
assertMultipathPreferenceIsEventually(network, initialMeteredPreference,
@@ -1713,7 +1775,8 @@
}
}
- private InetAddress getAddrByName(final String hostname, final int family) throws Exception {
+ private static InetAddress getAddrByName(final String hostname, final int family)
+ throws Exception {
final InetAddress[] allAddrs = InetAddress.getAllByName(hostname);
for (InetAddress addr : allAddrs) {
if (family == AF_INET && addr instanceof Inet4Address) return addr;
@@ -2366,7 +2429,7 @@
mPackageManager.hasSystemFeature(FEATURE_WIFI));
final Network network = mCtsNetUtils.ensureWifiConnected();
- final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID());
+ final String ssid = unquoteSSID(getSSID());
assertNotNull("Ssid getting from WifiManager is null", ssid);
// This package should have no NETWORK_SETTINGS permission. Verify that no ssid is contained
// in the NetworkCapabilities.
@@ -2877,6 +2940,15 @@
new Handler(Looper.getMainLooper())), NETWORK_SETTINGS);
}
+ /**
+ * It needs android.Manifest.permission.INTERACT_ACROSS_USERS_FULL
+ * to use WifiManager.getConnectionInfo() on the visible background user.
+ */
+ private String getSSID() {
+ return runWithShellPermissionIdentity(() ->
+ mWifiManager.getConnectionInfo().getSSID());
+ }
+
private static final class OnCompleteListenerCallback {
final CompletableFuture<Object> mDone = new CompletableFuture<>();
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 1e2a212..5b2c9f7 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -77,6 +77,7 @@
import com.android.testutils.assertThrows
import com.android.testutils.runAsShell
import com.android.testutils.waitForIdle
+import com.google.common.truth.Truth.assertThat
import java.io.IOException
import java.net.Inet6Address
import java.net.Socket
@@ -145,6 +146,8 @@
private var tetheredInterfaceRequest: TetheredInterfaceRequest? = null
+ private var ethernetEnabled = true
+
private class EthernetTestInterface(
context: Context,
private val handler: Handler,
@@ -428,7 +431,7 @@
// when an interface comes up, we should always see a down cb before an up cb.
ifaceListener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
- if (hasCarrier) {
+ if (hasCarrier && ethernetEnabled) {
ifaceListener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
}
return iface
@@ -514,6 +517,7 @@
private fun setEthernetEnabled(enabled: Boolean) {
runAsShell(NETWORK_SETTINGS) { em.setEthernetEnabled(enabled) }
+ ethernetEnabled = enabled
val listener = EthernetStateListener()
addEthernetStateListener(listener)
listener.eventuallyExpect(if (enabled) ETHERNET_STATE_ENABLED else ETHERNET_STATE_DISABLED)
@@ -600,26 +604,6 @@
}
}
- @Test
- fun testCallbacks_withRunningInterface() {
- assumeFalse(isAdbOverEthernet())
- // Only run this test when no non-restricted / physical interfaces are present.
- assumeNoInterfaceForTetheringAvailable()
-
- val iface = createInterface()
- val listener = EthernetStateListener()
- addInterfaceStateListener(listener)
- listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
-
- // Remove running interface. The interface stays running but is no longer tracked.
- setEthernetEnabled(false)
- listener.expectCallback(iface, STATE_ABSENT, ROLE_NONE)
-
- setEthernetEnabled(true)
- listener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
- listener.assertNoCallback()
- }
-
private fun assumeNoInterfaceForTetheringAvailable() {
// Interfaces that have configured NetworkCapabilities will never be used for tethering,
// see aosp/2123900.
@@ -911,6 +895,30 @@
}
@Test
+ fun testEnableDisableInterface_disableEnableEthernet() {
+ val iface = createInterface()
+ val listener = EthernetStateListener()
+ addInterfaceStateListener(listener)
+ listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
+
+ // When ethernet is disabled, interface should be down and enable/disableInterface()
+ // should not bring the interfaces up.
+ setEthernetEnabled(false)
+ listener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+ enableInterface(iface).expectError()
+ disableInterface(iface).expectError()
+ listener.assertNoCallback()
+
+ // When ethernet is enabled, enable/disableInterface() should succeed.
+ setEthernetEnabled(true)
+ listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
+ disableInterface(iface).expectResult(iface.name)
+ listener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+ enableInterface(iface).expectResult(iface.name)
+ listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
+ }
+
+ @Test
fun testUpdateConfiguration_forBothIpConfigAndCapabilities() {
val iface = createInterface()
val cb = requestNetwork(ETH_REQUEST.copyWithEthernetSpecifier(iface.name))
@@ -1018,4 +1026,88 @@
cb.eventuallyExpectCapabilities(TEST_CAPS)
cb.eventuallyExpectLpForStaticConfig(STATIC_IP_CONFIGURATION.staticIpConfiguration)
}
+
+ @Test
+ fun testAddInterface_disableEnableEthernet() {
+ val listener = EthernetStateListener()
+ addInterfaceStateListener(listener)
+
+ // When ethernet is disabled, newly added interfaces should not be brought up.
+ setEthernetEnabled(false)
+ val iface = createInterface(/* hasCarrier */ true)
+ listener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+
+ // When ethernet is re-enabled after interface is added, it will be brought up.
+ setEthernetEnabled(true)
+ listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
+ }
+
+
+ @Test
+ fun testRemoveInterface_disableEnableEthernet() {
+ // Set up 2 interfaces for testing
+ val iface1 = createInterface()
+ val listener = EthernetStateListener()
+ addInterfaceStateListener(listener)
+ listener.eventuallyExpect(iface1, STATE_LINK_UP, ROLE_CLIENT)
+ val iface2 = createInterface()
+ listener.eventuallyExpect(iface2, STATE_LINK_UP, ROLE_CLIENT)
+
+ // Removing interfaces when ethernet is enabled will first send link down, then
+ // STATE_ABSENT/ROLE_NONE.
+ removeInterface(iface1)
+ listener.expectCallback(iface1, STATE_LINK_DOWN, ROLE_CLIENT)
+ listener.expectCallback(iface1, STATE_ABSENT, ROLE_NONE)
+
+ // Removing interfaces after ethernet is disabled will first send link down when ethernet is
+ // disabled, then STATE_ABSENT/ROLE_NONE when interface is removed.
+ setEthernetEnabled(false)
+ listener.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
+ removeInterface(iface2)
+ listener.expectCallback(iface2, STATE_ABSENT, ROLE_NONE)
+ }
+
+ @Test
+ fun testSetTetheringInterfaceMode_disableEnableEthernet() {
+ val listener = EthernetStateListener()
+ addInterfaceStateListener(listener)
+
+ val iface = createInterface()
+ requestTetheredInterface().expectOnAvailable()
+ listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_SERVER)
+
+ // (b/234743836): Currently the state of server mode interfaces always returns true due to
+ // that interface state for server mode interfaces is not tracked properly.
+ // So we do not get any state change when disabling ethernet.
+ setEthernetEnabled(false)
+ listener.assertNoCallback()
+
+ // When ethernet is disabled, change interface mode will not bring the interface up.
+ releaseTetheredInterface()
+ listener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+
+ // When ethernet is re-enabled, interface will be brought up.
+ setEthernetEnabled(true)
+ listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
+ }
+
+ @Test
+ fun testGetInterfaceList_disableEnableEthernet() {
+ // Test that interface list can be obtained when ethernet is disabled.
+ setEthernetEnabled(false)
+ // Create two test interfaces and check the return list contains the interface names.
+ val iface1 = createInterface()
+ val iface2 = createInterface()
+ var ifaces = em.getInterfaceList()
+ assertThat(ifaces).containsAtLeast(iface1.name, iface2.name)
+
+ // Remove one existing test interface and check the return list doesn't contain the
+ // removed interface name.
+ removeInterface(iface1)
+ ifaces = em.getInterfaceList()
+ assertThat(ifaces).doesNotContain(iface1.name)
+ assertThat(ifaces).contains(iface2.name)
+
+ removeInterface(iface2)
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
index 2c7d5c6..c67443e 100644
--- a/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
+++ b/tests/cts/net/src/android/net/cts/MultinetworkApiTest.java
@@ -39,10 +39,13 @@
import android.system.ErrnoException;
import android.system.OsConstants;
import android.util.ArraySet;
+import android.util.Log;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.net.module.util.CollectionUtils;
import com.android.testutils.AutoReleaseNetworkCallbackRule;
+import com.android.testutils.ConnectivityDiagnosticsCollector;
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.DeviceConfigRule;
@@ -51,6 +54,8 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Set;
@DevSdkIgnoreRunner.RestoreDefaultNetwork
@@ -70,13 +75,34 @@
private static final String TAG = "MultinetworkNativeApiTest";
static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google";
+ public static class QueryTestResult {
+ public final int sourcePort;
+ public final int attempts;
+ public final int errNo;
+
+ public QueryTestResult(int sourcePort, int attempts, int errNo) {
+ this.sourcePort = sourcePort;
+ this.attempts = attempts;
+ this.errNo = errNo;
+ }
+
+ @Override
+ public String toString() {
+ return "QueryTestResult{"
+ + "sourcePort=" + sourcePort
+ + ", attempts=" + attempts
+ + ", errNo=" + errNo
+ + '}';
+ }
+ }
+
/**
* @return 0 on success
*/
private static native int runGetaddrinfoCheck(long networkHandle);
private static native int runSetprocnetwork(long networkHandle);
private static native int runSetsocknetwork(long networkHandle);
- private static native int runDatagramCheck(long networkHandle);
+ private static native QueryTestResult runDatagramCheck(long networkHandle, int sourcePort);
private static native void runResNapiMalformedCheck(long networkHandle);
private static native void runResNcancelCheck(long networkHandle);
private static native void runResNqueryCheck(long networkHandle);
@@ -165,14 +191,69 @@
}
}
+ private void runNativeDatagramTransmissionDiagnostics(Network network,
+ QueryTestResult failedResult) {
+ final ConnectivityDiagnosticsCollector collector = ConnectivityDiagnosticsCollector
+ .getInstance();
+ if (collector == null) {
+ Log.e(TAG, "Missing ConnectivityDiagnosticsCollector, not adding diagnostics");
+ return;
+ }
+
+ final int numReruns = 10;
+ final ArrayList<QueryTestResult> reruns = new ArrayList<>(numReruns);
+ for (int i = 0; i < numReruns; i++) {
+ final QueryTestResult rerunResult =
+ runDatagramCheck(network.getNetworkHandle(), 0 /* sourcePort */);
+ Log.d(TAG, "Rerun result " + i + ": " + rerunResult);
+ reruns.add(rerunResult);
+ }
+ // Rerun on the original port after trying the other ports, to check that the results are
+ // consistent, as opposed to the network recovering halfway through.
+ int originalPortFailedReruns = 0;
+ for (int i = 0; i < numReruns; i++) {
+ final QueryTestResult originalPortRerun = runDatagramCheck(network.getNetworkHandle(),
+ failedResult.sourcePort);
+ Log.d(TAG, "Rerun result " + i + " with original port: " + originalPortRerun);
+ if (originalPortRerun.errNo != 0) {
+ originalPortFailedReruns++;
+ }
+ }
+
+ final int noRetrySuccessResults = reruns.stream()
+ .filter(result -> result.errNo == 0 && result.attempts == 1)
+ .mapToInt(result -> 1)
+ .sum();
+ final int failedResults = reruns.stream()
+ .filter(result -> result.errNo != 0)
+ .mapToInt(result -> 1)
+ .sum();
+ collector.addFailureAttribute("numReruns", numReruns);
+ collector.addFailureAttribute("noRetrySuccessReruns", noRetrySuccessResults);
+ collector.addFailureAttribute("failedReruns", failedResults);
+ collector.addFailureAttribute("originalPortFailedReruns", originalPortFailedReruns);
+ }
+
@Test
public void testNativeDatagramTransmission() throws Exception {
for (Network network : getTestableNetworks()) {
- int errno = runDatagramCheck(network.getNetworkHandle());
- if (errno != 0) {
- throw new ErrnoException(
- "DatagramCheck on " + mCM.getNetworkInfo(network), -errno);
+ final QueryTestResult result = runDatagramCheck(network.getNetworkHandle(),
+ 0 /* sourcePort */);
+ if (result.errNo == 0) {
+ continue;
}
+ final NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
+ final int[] transports = nc != null ? nc.getTransportTypes() : null;
+ if (CollectionUtils.contains(transports, TRANSPORT_WIFI)) {
+ runNativeDatagramTransmissionDiagnostics(network, result);
+ }
+
+ // Log the whole result (with source port and attempts) to logcat, but use only the
+ // errno and transport in the fail message so similar failures have consistent messages
+ final String error = "DatagramCheck on transport " + Arrays.toString(transports)
+ + " failed: " + result.errNo;
+ Log.e(TAG, error + ", result: " + result);
+ fail(error);
}
}
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index ff10e1a..1ca5a77 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -16,6 +16,7 @@
package android.net.cts;
+import static android.net.ConnectivityManager.TYPE_NONE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
@@ -41,6 +42,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
@@ -130,7 +132,7 @@
verifyNoCapabilities(builder.build());
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testTemporarilyNotMeteredCapability() {
assertTrue(new NetworkRequest.Builder()
.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED).build()
@@ -157,7 +159,6 @@
}
@Test
- @IgnoreUpTo(Build.VERSION_CODES.Q)
public void testSpecifier() {
assertNull(new NetworkRequest.Builder().build().getNetworkSpecifier());
final WifiNetworkSpecifier specifier = new WifiNetworkSpecifier.Builder()
@@ -192,7 +193,6 @@
}
@Test
- @IgnoreUpTo(Build.VERSION_CODES.Q)
public void testRequestorPackageName() {
assertNull(new NetworkRequest.Builder().build().getRequestorPackageName());
final String pkgName = "android.net.test";
@@ -216,7 +216,6 @@
}
@Test
- @IgnoreUpTo(Build.VERSION_CODES.Q)
public void testCanBeSatisfiedBy() {
final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */);
final LocalNetworkSpecifier specifier2 = new LocalNetworkSpecifier(5678 /* id */);
@@ -284,7 +283,6 @@
}
@Test
- @IgnoreUpTo(Build.VERSION_CODES.Q)
public void testInvariantInCanBeSatisfiedBy() {
// Test invariant that result of NetworkRequest.canBeSatisfiedBy() should be the same with
// NetworkCapabilities.satisfiedByNetworkCapabilities().
@@ -388,7 +386,7 @@
otherUidsRequest.canBeSatisfiedBy(ncWithOtherUid));
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testRequestorUid() {
final NetworkCapabilities nc = new NetworkCapabilities();
// Verify default value is INVALID_UID
@@ -558,4 +556,43 @@
.setBssidPattern(ARBITRARY_ADDRESS, ARBITRARY_ADDRESS)
.build();
}
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testNetworkReservation() {
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ final NetworkCapabilities blanketOffer = new NetworkCapabilities(nc);
+ blanketOffer.setReservationId(NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS);
+ final NetworkCapabilities specificOffer = new NetworkCapabilities(nc);
+ specificOffer.setReservationId(42);
+ final NetworkCapabilities otherSpecificOffer = new NetworkCapabilities(nc);
+ otherSpecificOffer.setReservationId(43);
+ final NetworkCapabilities regularOffer = new NetworkCapabilities(nc);
+
+ final NetworkRequest reservationNR = new NetworkRequest(new NetworkCapabilities(nc),
+ TYPE_NONE, 42 /* rId */, NetworkRequest.Type.RESERVATION);
+ final NetworkRequest requestNR = new NetworkRequest(new NetworkCapabilities(nc),
+ TYPE_NONE, 42 /* rId */, NetworkRequest.Type.REQUEST);
+
+ assertTrue(reservationNR.canBeSatisfiedBy(blanketOffer));
+ assertTrue(reservationNR.canBeSatisfiedBy(specificOffer));
+ assertFalse(reservationNR.canBeSatisfiedBy(otherSpecificOffer));
+ assertFalse(reservationNR.canBeSatisfiedBy(regularOffer));
+
+ assertFalse(requestNR.canBeSatisfiedBy(blanketOffer));
+ assertTrue(requestNR.canBeSatisfiedBy(specificOffer));
+ assertTrue(requestNR.canBeSatisfiedBy(otherSpecificOffer));
+ assertTrue(requestNR.canBeSatisfiedBy(regularOffer));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testNetworkRequest_throwsWhenPassingCapsWithReservationId() {
+ final NetworkCapabilities capsWithResId = new NetworkCapabilities();
+ capsWithResId.setReservationId(42);
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ new NetworkRequest(capsWithResId, TYPE_NONE, 42 /* rId */, NetworkRequest.Type.REQUEST);
+ });
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
index 10adee0..65daf57 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
@@ -114,7 +114,7 @@
} catch (ClassNotFoundException e) {
/* not vulnerable if hidden API no longer available */
return;
- } catch (NoSuchMethodException e) {
+ } catch (NoSuchMethodException | NoSuchMethodError e) {
/* not vulnerable if hidden API no longer available */
return;
} catch (RemoteException e) {
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index fef085d..eb2dbf7 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -63,6 +63,7 @@
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.UserHandle;
import android.platform.test.annotations.AppModeFull;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
@@ -74,6 +75,7 @@
import com.android.compatibility.common.util.SystemUtil;
import com.android.modules.utils.build.SdkLevel;
import com.android.testutils.AutoReleaseNetworkCallbackRule;
+import com.android.testutils.ConnectivityDiagnosticsCollector;
import com.android.testutils.ConnectivityModuleTest;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -94,8 +96,10 @@
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -113,8 +117,8 @@
private static final String LOG_TAG = "NetworkStatsManagerTest";
- private static final String APPOPS_SET_SHELL_COMMAND = "appops set {0} {1} {2}";
- private static final String APPOPS_GET_SHELL_COMMAND = "appops get {0} {1}";
+ private static final String APPOPS_SET_SHELL_COMMAND = "appops set --user {0} {1} {2} {3}";
+ private static final String APPOPS_GET_SHELL_COMMAND = "appops get --user {0} {1} {2}";
private static final long MINUTE = 1000 * 60;
private static final int TIMEOUT_MILLIS = 15000;
@@ -329,12 +333,14 @@
}
private void setAppOpsMode(String appop, String mode) throws Exception {
- final String command = MessageFormat.format(APPOPS_SET_SHELL_COMMAND, mPkg, appop, mode);
+ final String command = MessageFormat.format(APPOPS_SET_SHELL_COMMAND,
+ UserHandle.myUserId(), mPkg, appop, mode);
SystemUtil.runShellCommand(mInstrumentation, command);
}
private String getAppOpsMode(String appop) throws Exception {
- final String command = MessageFormat.format(APPOPS_GET_SHELL_COMMAND, mPkg, appop);
+ final String command = MessageFormat.format(APPOPS_GET_SHELL_COMMAND,
+ UserHandle.myUserId(), mPkg, appop);
String result = SystemUtil.runShellCommand(mInstrumentation, command);
if (result == null) {
Log.w(LOG_TAG, "App op " + appop + " could not be read.");
@@ -708,27 +714,57 @@
}
}
- class QueryResult {
- public final int tag;
- public final int state;
- public final long total;
+ class QueryResults {
+ private static class QueryKey {
+ private final int mTag;
+ private final int mState;
- QueryResult(int tag, int state, NetworkStats stats) {
- this.tag = tag;
- this.state = state;
- total = getTotalAndAssertNotEmpty(stats, tag, state);
+ QueryKey(int tag, int state) {
+ this.mTag = tag;
+ this.mState = state;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof QueryKey)) return false;
+
+ QueryKey queryKey = (QueryKey) o;
+ return mTag == queryKey.mTag && mState == queryKey.mState;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTag, mState);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("QueryKey(tag=%s, state=%s)", tagToString(mTag),
+ stateToString(mState));
+ }
}
- public String toString() {
- return String.format("QueryResult(tag=%s state=%s total=%d)",
- tagToString(tag), stateToString(state), total);
+ private final HashMap<QueryKey, Long> mSnapshot = new HashMap<>();
+
+ public long get(int tag, int state) {
+ // Expect all results are stored before access.
+ return Objects.requireNonNull(mSnapshot.get(new QueryKey(tag, state)));
+ }
+
+ public void put(int tag, int state, long total) {
+ mSnapshot.put(new QueryKey(tag, state), total);
}
}
- private NetworkStats getNetworkStatsForTagState(int i, int tag, int state) {
- return mNsm.queryDetailsForUidTagState(
+ private long getTotalForTagState(int i, int tag, int state, boolean assertNotEmpty,
+ long startTime, long endTime) {
+ final NetworkStats stats = mNsm.queryDetailsForUidTagState(
mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i),
- mStartTime, mEndTime, Process.myUid(), tag, state);
+ startTime, endTime, Process.myUid(), tag, state);
+ final long total = getTotal(stats, tag, state, assertNotEmpty, startTime, endTime);
+ stats.close();
+ return total;
}
private void assertWithinPercentage(String msg, long expected, long actual, int percentage) {
@@ -739,92 +775,103 @@
assertTrue(msg, upperBound >= actual);
}
- private void assertAlmostNoUnexpectedTraffic(NetworkStats result, int expectedTag,
+ private void assertAlmostNoUnexpectedTraffic(long total, int expectedTag,
int expectedState, long maxUnexpected) {
- long total = 0;
- NetworkStats.Bucket bucket = new NetworkStats.Bucket();
- while (result.hasNextBucket()) {
- assertTrue(result.getNextBucket(bucket));
- total += bucket.getRxBytes() + bucket.getTxBytes();
- }
if (total <= maxUnexpected) return;
- fail(String.format("More than %d bytes of traffic when querying for "
- + "tag %s state %s. Last bucket: uid=%d tag=%s state=%s bytes=%d/%d",
- maxUnexpected, tagToString(expectedTag), stateToString(expectedState),
- bucket.getUid(), tagToString(bucket.getTag()), stateToString(bucket.getState()),
- bucket.getRxBytes(), bucket.getTxBytes()));
+ fail(String.format("More than %d bytes of traffic when querying for tag %s state %s.",
+ maxUnexpected, tagToString(expectedTag), stateToString(expectedState)));
}
+ @ConnectivityDiagnosticsCollector.CollectTcpdumpOnFailure
@Test
public void testUidTagStateDetails() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
if (!shouldTestThisNetworkType(i)) {
continue;
}
- // Relatively large tolerance to accommodate for history bucket size.
- requestNetworkAndGenerateTraffic(i, LONG_TOLERANCE);
setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "allow");
- NetworkStats result = null;
- try {
- int currentState = isInForeground() ? STATE_FOREGROUND : STATE_DEFAULT;
- int otherState = (currentState == STATE_DEFAULT) ? STATE_FOREGROUND : STATE_DEFAULT;
- int[] tagsWithTraffic = {NETWORK_TAG, TAG_NONE};
- int[] statesWithTraffic = {currentState, STATE_ALL};
- ArrayList<QueryResult> resultsWithTraffic = new ArrayList<>();
+ int currentState = isInForeground() ? STATE_FOREGROUND : STATE_DEFAULT;
+ int otherState = (currentState == STATE_DEFAULT) ? STATE_FOREGROUND : STATE_DEFAULT;
- int[] statesWithNoTraffic = {otherState};
- int[] tagsWithNoTraffic = {NETWORK_TAG + 1};
- ArrayList<QueryResult> resultsWithNoTraffic = new ArrayList<>();
+ final List<Integer> statesWithTraffic = List.of(currentState, STATE_ALL);
+ final List<Integer> statesWithNoTraffic = List.of(otherState);
+ final ArrayList<Integer> allStates = new ArrayList<>();
+ allStates.addAll(statesWithTraffic);
+ allStates.addAll(statesWithNoTraffic);
- // Expect to see traffic when querying for any combination of a tag in
- // tagsWithTraffic and a state in statesWithTraffic.
- for (int tag : tagsWithTraffic) {
- for (int state : statesWithTraffic) {
- result = getNetworkStatsForTagState(i, tag, state);
- resultsWithTraffic.add(new QueryResult(tag, state, result));
- result.close();
- result = null;
+ final List<Integer> tagsWithTraffic = List.of(NETWORK_TAG, TAG_NONE);
+ final List<Integer> tagsWithNoTraffic = List.of(NETWORK_TAG + 1);
+ final ArrayList<Integer> allTags = new ArrayList<>();
+ allTags.addAll(tagsWithTraffic);
+ allTags.addAll(tagsWithNoTraffic);
+
+ // Relatively large tolerance to accommodate for history bucket size,
+ // and covering the entire test duration.
+ final long now = System.currentTimeMillis();
+ final long startTime = now - LONG_TOLERANCE;
+ final long endTime = now + LONG_TOLERANCE;
+
+ // Collect a baseline before generating network traffic.
+ QueryResults baseline = new QueryResults();
+ final ArrayList<String> logNonEmptyBaseline = new ArrayList<>();
+ for (int tag : allTags) {
+ for (int state : allStates) {
+ final long total = getTotalForTagState(i, tag, state, false,
+ startTime, endTime);
+ baseline.put(tag, state, total);
+ if (total > 0) {
+ logNonEmptyBaseline.add(
+ new QueryResults.QueryKey(tag, state) + "=" + total);
}
}
-
- // Expect that the results are within a few percentage points of each other.
- // This is ensures that FIN retransmits after the transfer is complete don't cause
- // the test to be flaky. The test URL currently returns just over 100k so this
- // should not be too noisy. It also ensures that the traffic sent by the test
- // harness, which is untagged, won't cause a failure.
- long firstTotal = resultsWithTraffic.get(0).total;
- for (QueryResult queryResult : resultsWithTraffic) {
- assertWithinPercentage(queryResult + "", firstTotal, queryResult.total, 16);
- }
-
- // Expect to see no traffic when querying for any tag in tagsWithNoTraffic or any
- // state in statesWithNoTraffic.
- for (int tag : tagsWithNoTraffic) {
- for (int state : statesWithTraffic) {
- result = getNetworkStatsForTagState(i, tag, state);
- assertAlmostNoUnexpectedTraffic(result, tag, state, firstTotal / 100);
- result.close();
- result = null;
- }
- }
- for (int tag : tagsWithTraffic) {
- for (int state : statesWithNoTraffic) {
- result = getNetworkStatsForTagState(i, tag, state);
- assertAlmostNoUnexpectedTraffic(result, tag, state, firstTotal / 100);
- result.close();
- result = null;
- }
- }
- } finally {
- if (result != null) {
- result.close();
- }
}
+ // TODO: Remove debug log for b/368624224.
+ if (logNonEmptyBaseline.size() > 0) {
+ Log.v(LOG_TAG, "Baseline=" + logNonEmptyBaseline);
+ }
+
+ // Generate some traffic and release the network.
+ requestNetworkAndGenerateTraffic(i, LONG_TOLERANCE);
+
+ QueryResults results = new QueryResults();
+ // Collect results for all combinations of tags and states.
+ for (int tag : allTags) {
+ for (int state : allStates) {
+ final boolean assertNotEmpty = tagsWithTraffic.contains(tag)
+ && statesWithTraffic.contains(state);
+ final long total = getTotalForTagState(i, tag, state, assertNotEmpty,
+ startTime, endTime) - baseline.get(tag, state);
+ results.put(tag, state, total);
+ }
+ }
+
+ // Expect that the results are within a few percentage points of each other.
+ // This is ensures that FIN retransmits after the transfer is complete don't cause
+ // the test to be flaky. The test URL currently returns just over 100k so this
+ // should not be too noisy. It also ensures that the traffic sent by the test
+ // harness, which is untagged, won't cause a failure.
+ long totalOfNetworkTagAndCurrentState = results.get(NETWORK_TAG, currentState);
+ for (int tag : allTags) {
+ for (int state : allStates) {
+ final long result = results.get(tag, state);
+ final String queryKeyStr = new QueryResults.QueryKey(tag, state).toString();
+ if (tagsWithTraffic.contains(tag) && statesWithTraffic.contains(state)) {
+ assertWithinPercentage(queryKeyStr,
+ totalOfNetworkTagAndCurrentState, result, 16);
+ } else {
+ // Expect to see no traffic when querying for any combination with tag
+ // in tagsWithNoTraffic or any state in statesWithNoTraffic.
+ assertAlmostNoUnexpectedTraffic(result, tag, state,
+ totalOfNetworkTagAndCurrentState / 100);
+ }
+ }
+ }
+
setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, "deny");
try {
- result = mNsm.queryDetailsForUidTag(
+ mNsm.queryDetailsForUidTag(
mNetworkInterfacesToTest[i].getNetworkType(), getSubscriberId(i),
mStartTime, mEndTime, Process.myUid(), NETWORK_TAG);
fail("negative testUidDetails fails: no exception thrown.");
@@ -897,7 +944,7 @@
}
}
- private String tagToString(Integer tag) {
+ private static String tagToString(Integer tag) {
if (tag == null) return "null";
switch (tag) {
case TAG_NONE:
@@ -907,7 +954,7 @@
}
}
- private String stateToString(Integer state) {
+ private static String stateToString(Integer state) {
if (state == null) return "null";
switch (state) {
case STATE_ALL:
@@ -920,8 +967,8 @@
throw new IllegalArgumentException("Unknown state " + state);
}
- private long getTotalAndAssertNotEmpty(NetworkStats result, Integer expectedTag,
- Integer expectedState) {
+ private long getTotal(NetworkStats result, Integer expectedTag,
+ Integer expectedState, boolean assertNotEmpty, long startTime, long endTime) {
assertTrue(result != null);
NetworkStats.Bucket bucket = new NetworkStats.Bucket();
long totalTxPackets = 0;
@@ -930,7 +977,7 @@
long totalRxBytes = 0;
while (result.hasNextBucket()) {
assertTrue(result.getNextBucket(bucket));
- assertTimestamps(bucket);
+ assertTimestamps(bucket, startTime, endTime);
if (expectedTag != null) assertEquals(bucket.getTag(), (int) expectedTag);
if (expectedState != null) assertEquals(bucket.getState(), (int) expectedState);
assertEquals(bucket.getMetered(), METERED_ALL);
@@ -946,23 +993,29 @@
assertFalse(result.getNextBucket(bucket));
String msg = String.format("uid %d tag %s state %s",
Process.myUid(), tagToString(expectedTag), stateToString(expectedState));
- assertTrue("No Rx bytes usage for " + msg, totalRxBytes > 0);
- assertTrue("No Rx packets usage for " + msg, totalRxPackets > 0);
- assertTrue("No Tx bytes usage for " + msg, totalTxBytes > 0);
- assertTrue("No Tx packets usage for " + msg, totalTxPackets > 0);
+ if (assertNotEmpty) {
+ assertTrue("No Rx bytes usage for " + msg, totalRxBytes > 0);
+ assertTrue("No Rx packets usage for " + msg, totalRxPackets > 0);
+ assertTrue("No Tx bytes usage for " + msg, totalTxBytes > 0);
+ assertTrue("No Tx packets usage for " + msg, totalTxPackets > 0);
+ }
return totalRxBytes + totalTxBytes;
}
private long getTotalAndAssertNotEmpty(NetworkStats result) {
- return getTotalAndAssertNotEmpty(result, null, STATE_ALL);
+ return getTotal(result, null, STATE_ALL, true /*assertEmpty*/, mStartTime, mEndTime);
}
private void assertTimestamps(final NetworkStats.Bucket bucket) {
+ assertTimestamps(bucket, mStartTime, mEndTime);
+ }
+
+ private void assertTimestamps(final NetworkStats.Bucket bucket, long startTime, long endTime) {
assertTrue("Start timestamp " + bucket.getStartTimeStamp() + " is less than "
- + mStartTime, bucket.getStartTimeStamp() >= mStartTime);
+ + startTime, bucket.getStartTimeStamp() >= startTime);
assertTrue("End timestamp " + bucket.getEndTimeStamp() + " is greater than "
- + mEndTime, bucket.getEndTimeStamp() <= mEndTime);
+ + endTime, bucket.getEndTimeStamp() <= endTime);
}
private static class TestUsageCallback extends NetworkStatsManager.UsageCallback {
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
index 24af42b..1973899 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -92,7 +92,7 @@
assertEquals(downstreamIface.name, iface)
val request = TetheringRequest.Builder(TETHERING_ETHERNET)
.setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build()
- tetheringEventCallback = enableEthernetTethering(
+ tetheringEventCallback = enableTethering(
iface, request,
null /* any upstream */
).apply {
@@ -125,7 +125,7 @@
val request = TetheringRequest.Builder(TETHERING_ETHERNET)
.setStaticIpv4Addresses(localAddr, clientAddr)
.setShouldShowEntitlementUi(false).build()
- tetheringEventCallback = enableEthernetTethering(
+ tetheringEventCallback = enableTethering(
iface, request,
null /* any upstream */
).apply {
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 223efcb..ee31f1a 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -36,8 +36,11 @@
import android.net.TestNetworkSpecifier
import android.net.connectivity.ConnectivityCompatChanges
import android.net.cts.util.CtsNetUtils
+import android.net.nsd.AdvertisingRequest
+import android.net.nsd.AdvertisingRequest.FLAG_SKIP_PROBING
import android.net.nsd.DiscoveryRequest
import android.net.nsd.NsdManager
+import android.net.nsd.NsdManager.PROTOCOL_DNS_SD
import android.net.nsd.NsdServiceInfo
import android.net.nsd.OffloadEngine
import android.net.nsd.OffloadServiceInfo
@@ -88,9 +91,9 @@
import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdated
import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdatedLost
import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded
+import com.android.testutils.PollPacketReader
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
-import com.android.testutils.PollPacketReader
import com.android.testutils.TestDnsPacket
import com.android.testutils.TestableNetworkAgent
import com.android.testutils.TestableNetworkCallback
@@ -2565,6 +2568,49 @@
verifyCachedServicesRemoval(isCachedServiceRemoved = true)
}
+ @Test
+ fun testSkipProbing() {
+ val si = makeTestServiceInfo(testNetwork1.network)
+ val request = AdvertisingRequest.Builder(si)
+ .setFlags(FLAG_SKIP_PROBING)
+ .build()
+ assertEquals(FLAG_SKIP_PROBING, request.flags)
+ assertEquals(PROTOCOL_DNS_SD, request.protocolType)
+ assertEquals(si.serviceName, request.serviceInfo.serviceName)
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ nsdManager.registerService(request, { it.run() }, registrationRecord)
+ registrationRecord.expectCallback<ServiceRegistered>()
+ val packetReader = makePacketReader()
+
+ tryTest {
+ val srvRecordName = "$serviceName.$serviceType.local"
+ // Look for either announcements or probes
+ val packet = packetReader.pollForMdnsPacket {
+ it.isProbeFor(srvRecordName) || it.isReplyFor(srvRecordName)
+ }
+ assertNotNull(packet, "Probe or announcement not received within timeout")
+ // The first packet should be an announcement, not a probe.
+ assertTrue("Found initial probes with NSD_ADVERTISING_SKIP_PROBING enabled",
+ packet.isReplyFor(srvRecordName))
+
+ // Force a conflict now that the service is getting announced
+ val conflictingAnnouncement = buildConflictingAnnouncement()
+ packetReader.sendResponse(conflictingAnnouncement)
+
+ // Expect to see probes now (RFC6762 9., service is reset to probing state)
+ assertNotNull(packetReader.pollForProbe(serviceName, serviceType),
+ "Probe not received within timeout after conflict")
+ } cleanupStep {
+ nsdManager.unregisterService(registrationRecord)
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ } cleanup {
+ packetReader.handler.post { packetReader.stop() }
+ handlerThread.waitForIdle(TIMEOUT_MS)
+ }
+ }
+
private fun hasServiceTypeClientsForNetwork(clients: List<String>, network: Network): Boolean {
return clients.any { client -> client.substring(
client.indexOf("network=") + "network=".length,
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
index 0dd2a23..173d13f 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -53,6 +53,7 @@
import android.os.Build;
import android.os.ConditionVariable;
import android.os.IBinder;
+import android.os.UserHandle;
import android.system.Os;
import android.system.OsConstants;
import android.telephony.SubscriptionManager;
@@ -145,7 +146,8 @@
for (final String pkg : new String[] {"com.android.shell", mContext.getPackageName()}) {
final String cmd =
String.format(
- "appops set %s %s %s",
+ "appops set --user %d %s %s %s",
+ UserHandle.myUserId(), // user id
pkg, // Package name
opName, // Appop
(allow ? "allow" : "deny")); // Action
diff --git a/tests/cts/netpermission/updatestatspermission/AndroidTest.xml b/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
index fb6c814..82994c4 100644
--- a/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
+++ b/tests/cts/netpermission/updatestatspermission/AndroidTest.xml
@@ -20,6 +20,7 @@
<option name="config-descriptor:metadata" key="parameter" value="instant_app" />
<option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
<option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user_on_secondary_display" />
<option name="not-shardable" value="true" />
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index d9bc7f7..d167836 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -34,6 +34,7 @@
static_libs: [
"TetheringCommonTests",
+ "com.android.net.flags-aconfig-java",
"compatibility-device-util-axt",
"cts-net-utils",
"net-tests-utils",
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index 47d444f..5e94c06 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -17,6 +17,7 @@
import static android.Manifest.permission.MODIFY_PHONE_STATE;
import static android.Manifest.permission.TETHER_PRIVILEGED;
+import static android.Manifest.permission.WRITE_SETTINGS;
import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
@@ -28,6 +29,7 @@
import static android.net.TetheringManager.TETHERING_ETHERNET;
import static android.net.TetheringManager.TETHERING_NCM;
import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_VIRTUAL;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
@@ -70,6 +72,7 @@
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiSsid;
+import android.os.Build;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.ResultReceiver;
@@ -77,10 +80,12 @@
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
+import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
import com.android.testutils.ParcelUtils;
import org.junit.After;
@@ -93,6 +98,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@@ -225,19 +231,23 @@
}
- @Test
- public void testTetheringRequest() {
- SoftApConfiguration softApConfiguration;
+ private SoftApConfiguration createSoftApConfiguration(@NonNull String ssid) {
+ SoftApConfiguration config;
if (SdkLevel.isAtLeastT()) {
- softApConfiguration = new SoftApConfiguration.Builder()
- .setWifiSsid(WifiSsid.fromBytes(
- "This is an SSID!".getBytes(StandardCharsets.UTF_8)))
+ config = new SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes(ssid.getBytes(StandardCharsets.UTF_8)))
.build();
} else {
- softApConfiguration = new SoftApConfiguration.Builder()
- .setSsid("This is an SSID!")
+ config = new SoftApConfiguration.Builder()
+ .setSsid(ssid)
.build();
}
+ return config;
+ }
+
+ @Test
+ public void testTetheringRequest() {
+ SoftApConfiguration softApConfiguration = createSoftApConfiguration("SSID");
final TetheringRequest tr = new TetheringRequest.Builder(TETHERING_WIFI)
.setSoftApConfiguration(softApConfiguration)
.build();
@@ -298,21 +308,18 @@
tr3.setUid(uid);
tr3.setPackageName(packageName);
assertEquals(tr2, tr3);
+
+ final String interfaceName = "test_iface";
+ final TetheringRequest tr4 = new TetheringRequest.Builder(TETHERING_VIRTUAL)
+ .setInterfaceName(interfaceName)
+ .setConnectivityScope(CONNECTIVITY_SCOPE_GLOBAL).build();
+ assertEquals(interfaceName, tr4.getInterfaceName());
+ assertEquals(CONNECTIVITY_SCOPE_GLOBAL, tr4.getConnectivityScope());
}
@Test
public void testTetheringRequestSetSoftApConfigurationFailsWhenNotWifi() {
- final SoftApConfiguration softApConfiguration;
- if (SdkLevel.isAtLeastT()) {
- softApConfiguration = new SoftApConfiguration.Builder()
- .setWifiSsid(WifiSsid.fromBytes(
- "This is an SSID!".getBytes(StandardCharsets.UTF_8)))
- .build();
- } else {
- softApConfiguration = new SoftApConfiguration.Builder()
- .setSsid("This is an SSID!")
- .build();
- }
+ final SoftApConfiguration softApConfiguration = createSoftApConfiguration("SSID");
for (int type : List.of(TETHERING_USB, TETHERING_BLUETOOTH, TETHERING_WIFI_P2P,
TETHERING_NCM, TETHERING_ETHERNET)) {
try {
@@ -325,33 +332,40 @@
}
@Test
- public void testTetheringRequestParcelable() {
- final SoftApConfiguration softApConfiguration;
- if (SdkLevel.isAtLeastT()) {
- softApConfiguration = new SoftApConfiguration.Builder()
- .setWifiSsid(WifiSsid.fromBytes(
- "This is an SSID!".getBytes(StandardCharsets.UTF_8)))
- .build();
- } else {
- softApConfiguration = new SoftApConfiguration.Builder()
- .setSsid("This is an SSID!")
- .build();
+ public void testTetheringRequestSetInterfaceNameFailsExceptTetheringVirtual() {
+ for (int type : List.of(TETHERING_USB, TETHERING_BLUETOOTH, TETHERING_NCM,
+ TETHERING_ETHERNET)) {
+ try {
+ new TetheringRequest.Builder(type).setInterfaceName("test_iface");
+ fail("Was able to set interface name for tethering type " + type);
+ } catch (IllegalArgumentException e) {
+ // Success
+ }
}
+ }
+
+ @Test
+ public void testTetheringRequestParcelable() {
+ final SoftApConfiguration softApConfiguration = createSoftApConfiguration("SSID");
final LinkAddress localAddr = new LinkAddress("192.168.24.5/24");
final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24");
- final TetheringRequest withConfig = new TetheringRequest.Builder(TETHERING_WIFI)
+ final TetheringRequest withApConfig = new TetheringRequest.Builder(TETHERING_WIFI)
.setSoftApConfiguration(softApConfiguration)
.setStaticIpv4Addresses(localAddr, clientAddr)
.setExemptFromEntitlementCheck(true)
.setShouldShowEntitlementUi(false).build();
- final TetheringRequest withoutConfig = new TetheringRequest.Builder(TETHERING_WIFI)
+ final TetheringRequest withoutApConfig = new TetheringRequest.Builder(TETHERING_WIFI)
.setStaticIpv4Addresses(localAddr, clientAddr)
.setExemptFromEntitlementCheck(true)
.setShouldShowEntitlementUi(false).build();
- assertEquals(withConfig, ParcelUtils.parcelingRoundTrip(withConfig));
- assertEquals(withoutConfig, ParcelUtils.parcelingRoundTrip(withoutConfig));
- assertNotEquals(withConfig, ParcelUtils.parcelingRoundTrip(withoutConfig));
- assertNotEquals(withoutConfig, ParcelUtils.parcelingRoundTrip(withConfig));
+ assertEquals(withApConfig, ParcelUtils.parcelingRoundTrip(withApConfig));
+ assertEquals(withoutApConfig, ParcelUtils.parcelingRoundTrip(withoutApConfig));
+ assertNotEquals(withApConfig, ParcelUtils.parcelingRoundTrip(withoutApConfig));
+ assertNotEquals(withoutApConfig, ParcelUtils.parcelingRoundTrip(withApConfig));
+
+ final TetheringRequest withIfaceName = new TetheringRequest.Builder(TETHERING_VIRTUAL)
+ .setInterfaceName("test_iface").build();
+ assertEquals(withIfaceName, ParcelUtils.parcelingRoundTrip(withIfaceName));
}
@Test
@@ -363,9 +377,7 @@
tetherEventCallback.assumeWifiTetheringSupported(mContext);
tetherEventCallback.expectNoTetheringActive();
- SoftApConfiguration softApConfig = new SoftApConfiguration.Builder()
- .setWifiSsid(WifiSsid.fromBytes("This is an SSID!"
- .getBytes(StandardCharsets.UTF_8))).build();
+ SoftApConfiguration softApConfig = createSoftApConfiguration("SSID");
final TetheringInterface tetheredIface =
mCtsTetheringUtils.startWifiTethering(tetherEventCallback, softApConfig);
@@ -437,11 +449,40 @@
}
@Test
- public void testEnableTetheringPermission() throws Exception {
+ public void testStopTetheringRequest() throws Exception {
+ TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
+ Executor executor = Runnable::run;
+ TetheringManager.StopTetheringCallback callback =
+ new TetheringManager.StopTetheringCallback() {};
+ try {
+ mTM.stopTethering(request, executor, callback);
+ fail("stopTethering should throw UnsupportedOperationException");
+ } catch (UnsupportedOperationException expect) { }
+ }
+
+ private boolean isTetheringWithSoftApConfigEnabled() {
+ return Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM
+ && Flags.tetheringWithSoftApConfig();
+ }
+
+ @Test
+ public void testStartTetheringNoPermission() throws Exception {
final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
+
+ // No permission
mTM.startTethering(new TetheringRequest.Builder(TETHERING_WIFI).build(),
c -> c.run() /* executor */, startTetheringCallback);
startTetheringCallback.expectTetheringFailed(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+
+ // WRITE_SETTINGS not sufficient
+ if (isTetheringWithSoftApConfigEnabled()) {
+ runAsShell(WRITE_SETTINGS, () -> {
+ mTM.startTethering(new TetheringRequest.Builder(TETHERING_WIFI).build(),
+ c -> c.run() /* executor */, startTetheringCallback);
+ startTetheringCallback.expectTetheringFailed(
+ TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+ });
+ }
}
private class EntitlementResultListener implements OnTetheringEntitlementResultListener {
diff --git a/tests/unit/java/android/net/TrafficStatsTest.kt b/tests/unit/java/android/net/TrafficStatsTest.kt
new file mode 100644
index 0000000..0f85daf
--- /dev/null
+++ b/tests/unit/java/android/net/TrafficStatsTest.kt
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net
+
+import android.net.TrafficStats.UNSUPPORTED
+import android.net.netstats.StatsResult
+import android.net.netstats.TrafficStatsRateLimitCacheConfig
+import android.os.Build
+import com.android.server.net.NetworkStatsService.TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule.FeatureFlag
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.util.HashMap
+import java.util.function.LongSupplier
+
+const val TEST_EXPIRY_DURATION_MS = 1000
+const val TEST_IFACE = "wlan0"
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class TrafficStatsTest {
+ private val binder = mock(INetworkStatsService::class.java)
+ private val myUid = android.os.Process.myUid()
+ private val mockMyUidStatsResult = StatsResult(5L, 6L, 7L, 8L)
+ private val mockIfaceStatsResult = StatsResult(7L, 3L, 10L, 21L)
+ private val mockTotalStatsResult = StatsResult(8L, 1L, 5L, 2L)
+ private val secondUidStatsResult = StatsResult(3L, 7L, 10L, 5L)
+ private val secondIfaceStatsResult = StatsResult(9L, 8L, 7L, 6L)
+ private val secondTotalStatsResult = StatsResult(4L, 3L, 2L, 1L)
+ private val emptyStatsResult = StatsResult(0L, 0L, 0L, 0L)
+ private val unsupportedStatsResult =
+ StatsResult(UNSUPPORTED.toLong(), UNSUPPORTED.toLong(),
+ UNSUPPORTED.toLong(), UNSUPPORTED.toLong())
+
+ private val cacheDisabledConfig = TrafficStatsRateLimitCacheConfig.Builder()
+ .setIsCacheEnabled(false)
+ .setExpiryDurationMs(0)
+ .setMaxEntries(0)
+ .build()
+ private val cacheEnabledConfig = TrafficStatsRateLimitCacheConfig.Builder()
+ .setIsCacheEnabled(true)
+ .setExpiryDurationMs(TEST_EXPIRY_DURATION_MS)
+ .setMaxEntries(100)
+ .build()
+ private val mTestTimeSupplier = TestTimeSupplier()
+
+ private val featureFlags = HashMap<String, Boolean>()
+
+ // This will set feature flags from @FeatureFlag annotations
+ // into the map before setUp() runs.
+ @get:Rule
+ val setFeatureFlagsRule = SetFeatureFlagsRule(
+ { name, enabled -> featureFlags.put(name, enabled == true) },
+ { name -> featureFlags.getOrDefault(name, false) }
+ )
+
+ class TestTimeSupplier : LongSupplier {
+ private var currentTimeMillis = 0L
+
+ override fun getAsLong() = currentTimeMillis
+
+ fun advanceTime(millis: Int) {
+ currentTimeMillis += millis
+ }
+ }
+
+ @Before
+ fun setUp() {
+ TrafficStats.setServiceForTest(binder)
+ TrafficStats.setTimeSupplierForTest(mTestTimeSupplier)
+ mockStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+ if (featureFlags.getOrDefault(TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, false)) {
+ doReturn(cacheEnabledConfig).`when`(binder).getRateLimitCacheConfig()
+ } else {
+ doReturn(cacheDisabledConfig).`when`(binder).getRateLimitCacheConfig()
+ }
+ TrafficStats.reinitRateLimitCacheForTest()
+ }
+
+ @After
+ fun tearDown() {
+ TrafficStats.setServiceForTest(null)
+ TrafficStats.setTimeSupplierForTest(null)
+ TrafficStats.reinitRateLimitCacheForTest()
+ }
+
+ private fun assertUidStats(uid: Int, stats: StatsResult) {
+ assertEquals(stats.rxBytes, TrafficStats.getUidRxBytes(uid))
+ assertEquals(stats.rxPackets, TrafficStats.getUidRxPackets(uid))
+ assertEquals(stats.txBytes, TrafficStats.getUidTxBytes(uid))
+ assertEquals(stats.txPackets, TrafficStats.getUidTxPackets(uid))
+ }
+
+ private fun assertIfaceStats(iface: String, stats: StatsResult) {
+ assertEquals(stats.rxBytes, TrafficStats.getRxBytes(iface))
+ assertEquals(stats.rxPackets, TrafficStats.getRxPackets(iface))
+ assertEquals(stats.txBytes, TrafficStats.getTxBytes(iface))
+ assertEquals(stats.txPackets, TrafficStats.getTxPackets(iface))
+ }
+
+ private fun assertTotalStats(stats: StatsResult) {
+ assertEquals(stats.rxBytes, TrafficStats.getTotalRxBytes())
+ assertEquals(stats.rxPackets, TrafficStats.getTotalRxPackets())
+ assertEquals(stats.txBytes, TrafficStats.getTotalTxBytes())
+ assertEquals(stats.txPackets, TrafficStats.getTotalTxPackets())
+ }
+
+ private fun mockStats(uidStats: StatsResult?, ifaceStats: StatsResult?,
+ totalStats: StatsResult?) {
+ doReturn(uidStats).`when`(binder).getUidStats(myUid)
+ doReturn(ifaceStats).`when`(binder).getIfaceStats(TEST_IFACE)
+ doReturn(totalStats).`when`(binder).getTotalStats()
+ }
+
+ private fun assertStats(uidStats: StatsResult, ifaceStats: StatsResult,
+ totalStats: StatsResult) {
+ assertUidStats(myUid, uidStats)
+ assertIfaceStats(TEST_IFACE, ifaceStats)
+ assertTotalStats(totalStats)
+ }
+
+ private fun assertStatsFetchInvocations(wantedInvocations: Int) {
+ verify(binder, times(wantedInvocations)).getUidStats(myUid)
+ verify(binder, times(wantedInvocations)).getIfaceStats(TEST_IFACE)
+ verify(binder, times(wantedInvocations)).getTotalStats()
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @Test
+ fun testRateLimitCacheExpiry_cacheEnabled() {
+ // Initial fetch, verify binder calls.
+ assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+ assertStatsFetchInvocations(1)
+
+ // Advance time within expiry, verify cached values used.
+ clearInvocations(binder)
+ mockStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+ mTestTimeSupplier.advanceTime(1)
+ assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+ assertStatsFetchInvocations(0)
+
+ // Advance time to expire cache, verify new values fetched.
+ clearInvocations(binder)
+ mTestTimeSupplier.advanceTime(TEST_EXPIRY_DURATION_MS)
+ assertStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+ assertStatsFetchInvocations(1)
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+ @Test
+ fun testRateLimitCacheExpiry_cacheDisabled() {
+ // Initial fetch, verify binder calls.
+ assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+ assertStatsFetchInvocations(4)
+
+ // Advance time within expiry, verify new values fetched.
+ clearInvocations(binder)
+ mockStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+ mTestTimeSupplier.advanceTime(1)
+ assertStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+ assertStatsFetchInvocations(4)
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @Test
+ fun testInvalidStatsNotCached_cacheEnabled() {
+ doTestInvalidStatsNotCached()
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+ @Test
+ fun testInvalidStatsNotCached_cacheDisabled() {
+ doTestInvalidStatsNotCached()
+ }
+
+ private fun doTestInvalidStatsNotCached() {
+ // Mock null stats, this usually happens when the query is not valid,
+ // e.g. query uid stats of other application.
+ mockStats(null, null, null)
+ assertStats(unsupportedStatsResult, unsupportedStatsResult, unsupportedStatsResult)
+ assertStatsFetchInvocations(4)
+
+ // Verify null stats is not cached, and mock empty stats. This usually
+ // happens when queries with non-existent interface names.
+ clearInvocations(binder)
+ mockStats(emptyStatsResult, emptyStatsResult, emptyStatsResult)
+ assertStats(emptyStatsResult, emptyStatsResult, emptyStatsResult)
+ assertStatsFetchInvocations(4)
+
+ // Verify empty result is also not cached.
+ clearInvocations(binder)
+ assertStats(emptyStatsResult, emptyStatsResult, emptyStatsResult)
+ assertStatsFetchInvocations(4)
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG)
+ @Test
+ fun testClearRateLimitCaches_cacheEnabled() {
+ doTestClearRateLimitCaches(true)
+ }
+
+ @FeatureFlag(name = TRAFFICSTATS_CLIENT_RATE_LIMIT_CACHE_ENABLED_FLAG, enabled = false)
+ @Test
+ fun testClearRateLimitCaches_cacheDisabled() {
+ doTestClearRateLimitCaches(false)
+ }
+
+ private fun doTestClearRateLimitCaches(cacheEnabled: Boolean) {
+ // Initial fetch, verify binder calls.
+ assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+ assertStatsFetchInvocations(if (cacheEnabled) 1 else 4)
+
+ // Verify cached values are used.
+ clearInvocations(binder)
+ assertStats(mockMyUidStatsResult, mockIfaceStatsResult, mockTotalStatsResult)
+ assertStatsFetchInvocations(if (cacheEnabled) 0 else 4)
+
+ // Clear caches, verify fetching from the service.
+ clearInvocations(binder)
+ TrafficStats.clearRateLimitCaches()
+ mockStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+ assertStats(secondUidStatsResult, secondIfaceStatsResult, secondTotalStatsResult)
+ assertStatsFetchInvocations(if (cacheEnabled) 1 else 4)
+ }
+}
diff --git a/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
index c491f37..8117431 100644
--- a/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
+++ b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
@@ -44,14 +44,14 @@
serviceType = "_ipp._tcp"
}
val beforeParcel = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
- .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setFlags(NSD_ADVERTISING_UPDATE_ONLY)
.setTtl(Duration.ofSeconds(30L))
.build()
val afterParcel = parcelingRoundTrip(beforeParcel)
assertEquals(beforeParcel.serviceInfo.serviceType, afterParcel.serviceInfo.serviceType)
- assertEquals(beforeParcel.advertisingConfig, afterParcel.advertisingConfig)
+ assertEquals(beforeParcel.flags, afterParcel.flags)
}
@Test
@@ -72,13 +72,13 @@
serviceType = "_ipp._tcp"
}
val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
- .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setFlags(NSD_ADVERTISING_UPDATE_ONLY)
.setTtl(Duration.ofSeconds(100L))
.build()
assertEquals("_ipp._tcp", request.serviceInfo.serviceType)
assertEquals(PROTOCOL_DNS_SD, request.protocolType)
- assertEquals(NSD_ADVERTISING_UPDATE_ONLY, request.advertisingConfig)
+ assertEquals(NSD_ADVERTISING_UPDATE_ONLY, request.flags)
assertEquals(Duration.ofSeconds(100L), request.ttl)
}
@@ -90,11 +90,11 @@
val request1 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build()
val request2 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build()
val request3 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
- .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setFlags(NSD_ADVERTISING_UPDATE_ONLY)
.setTtl(Duration.ofSeconds(120L))
.build()
val request4 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
- .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+ .setFlags(NSD_ADVERTISING_UPDATE_ONLY)
.setTtl(Duration.ofSeconds(120L))
.build()
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index df48f6c..ba62114 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -42,17 +42,20 @@
import java.util.Objects
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
+import kotlin.test.assertTrue
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.argThat
import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.doCallRealMethod
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
@@ -185,12 +188,12 @@
@Before
fun setUp() {
thread.start()
- doReturn(TEST_HOSTNAME).`when`(mockDeps).generateHostname()
+ doReturn(TEST_HOSTNAME).`when`(mockDeps).generateHostname(anyBoolean())
doReturn(mockInterfaceAdvertiser1).`when`(mockDeps).makeAdvertiser(eq(mockSocket1),
- any(), any(), any(), any(), eq(TEST_HOSTNAME), any(), any()
+ any(), any(), any(), any(), any(), any(), any()
)
doReturn(mockInterfaceAdvertiser2).`when`(mockDeps).makeAdvertiser(eq(mockSocket2),
- any(), any(), any(), any(), eq(TEST_HOSTNAME), any(), any()
+ any(), any(), any(), any(), any(), any(), any()
)
doReturn(true).`when`(mockInterfaceAdvertiser1).isProbing(anyInt())
doReturn(true).`when`(mockInterfaceAdvertiser2).isProbing(anyInt())
@@ -578,11 +581,59 @@
fun testRemoveService_whenAllServiceRemoved_thenUpdateHostName() {
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
- verify(mockDeps, times(1)).generateHostname()
+ verify(mockDeps, times(1)).generateHostname(anyBoolean())
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
postSync { advertiser.removeService(SERVICE_ID_1) }
- verify(mockDeps, times(2)).generateHostname()
+ verify(mockDeps, times(2)).generateHostname(anyBoolean())
+ }
+
+ private fun doHostnameGenerationTest(shortHostname: Boolean): Array<String> {
+ doCallRealMethod().`when`(mockDeps).generateHostname(anyBoolean())
+ val flags = MdnsFeatureFlags.newBuilder().setIsShortHostnamesEnabled(shortHostname).build()
+ val advertiser =
+ MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
+ DEFAULT_ADVERTISING_OPTION, TEST_CLIENT_UID_1) }
+
+ val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+ verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
+
+ val socketCb = socketCbCaptor.value
+ postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+
+ val hostnameCaptor = ArgumentCaptor.forClass(Array<String>::class.java)
+ verify(mockDeps).makeAdvertiser(
+ eq(mockSocket1),
+ eq(listOf(TEST_LINKADDR)),
+ eq(thread.looper),
+ any(),
+ any(),
+ hostnameCaptor.capture(),
+ any(),
+ any()
+ )
+ return hostnameCaptor.value
+ }
+
+ @Test
+ fun testShortHostnameGeneration() {
+ val hostname = doHostnameGenerationTest(shortHostname = true)
+ // Short hostnames are [8 uppercase letters or digits].local
+ assertEquals(2, hostname.size)
+ assertTrue(Regex("Android_[A-Z0-9]{8}").matches(hostname[0]),
+ "Unexpected hostname: ${hostname.contentToString()}")
+ assertEquals("local", hostname[1])
+ }
+
+ @Test
+ fun testLongHostnameGeneration() {
+ val hostname = doHostnameGenerationTest(shortHostname = false)
+ // Long hostnames are Android_[32 lowercase hex characters].local
+ assertEquals(2, hostname.size)
+ assertTrue(Regex("Android_[a-f0-9]{32}").matches(hostname[0]),
+ "Unexpected hostname: ${hostname.contentToString()}")
+ assertEquals("local", hostname[1])
}
private fun postSync(r: () -> Unit) {
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 da0bc88..67f9d9c 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -16,13 +16,13 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsQueryScheduler.INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
+import static com.android.server.connectivity.mdns.MdnsQueryScheduler.MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS;
+import static com.android.server.connectivity.mdns.MdnsQueryScheduler.TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
import static com.android.server.connectivity.mdns.MdnsSearchOptions.ACTIVE_QUERY_MODE;
import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
import static com.android.server.connectivity.mdns.MdnsServiceTypeClient.EVENT_START_QUERYTASK;
-import static com.android.server.connectivity.mdns.QueryTaskConfig.INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
-import static com.android.server.connectivity.mdns.QueryTaskConfig.MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS;
-import static com.android.server.connectivity.mdns.QueryTaskConfig.TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertArrayEquals;
@@ -566,21 +566,21 @@
// This is the first query. We will ask for unicast response.
assertTrue(config.expectUnicastResponse);
- assertEquals(config.transactionId, 1);
+ assertEquals(config.getTransactionId(), 1);
// For the rest of queries in this burst, we will NOT ask for unicast response.
for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
- int oldTransactionId = config.transactionId;
+ int oldTransactionId = config.getTransactionId();
config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
assertFalse(config.expectUnicastResponse);
- assertEquals(config.transactionId, oldTransactionId + 1);
+ assertEquals(config.getTransactionId(), oldTransactionId + 1);
}
// This is the first query of a new burst. We will ask for unicast response.
- int oldTransactionId = config.transactionId;
+ int oldTransactionId = config.getTransactionId();
config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
assertTrue(config.expectUnicastResponse);
- assertEquals(config.transactionId, oldTransactionId + 1);
+ assertEquals(config.getTransactionId(), oldTransactionId + 1);
}
@Test
@@ -591,21 +591,21 @@
// This is the first query. We will ask for unicast response.
assertTrue(config.expectUnicastResponse);
- assertEquals(config.transactionId, 1);
+ assertEquals(config.getTransactionId(), 1);
// For the rest of queries in this burst, we will NOT ask for unicast response.
for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
- int oldTransactionId = config.transactionId;
+ int oldTransactionId = config.getTransactionId();
config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
assertFalse(config.expectUnicastResponse);
- assertEquals(config.transactionId, oldTransactionId + 1);
+ assertEquals(config.getTransactionId(), oldTransactionId + 1);
}
// This is the first query of a new burst. We will NOT ask for unicast response.
- int oldTransactionId = config.transactionId;
+ int oldTransactionId = config.getTransactionId();
config = config.getConfigForNextRun(ACTIVE_QUERY_MODE);
assertFalse(config.expectUnicastResponse);
- assertEquals(config.transactionId, oldTransactionId + 1);
+ assertEquals(config.getTransactionId(), oldTransactionId + 1);
}
@Test
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt
index a7083dc..b179aac 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt
@@ -150,7 +150,7 @@
// EXPIRE_LEGACY_REQUEST (=8) is only used in ConnectivityManager and not included.
// CALLBACK_TRANSITIVE_CALLS_ONLY (=0) is not a callback so not included either.
assertEquals(
- "PRECHK|AVAIL|LOSING|LOST|UNAVAIL|NC|LP|SUSP|RESUME|BLK|LOCALINF|0x7fffe101",
+ "PRECHK|AVAIL|LOSING|LOST|UNAVAIL|NC|LP|SUSP|RESUME|BLK|LOCALINF|RES|0x7fffc101",
ConnectivityService.declaredMethodsFlagsToString(0x7fff_ffff)
)
// The toString method and the assertion above need to be updated if constants are added
@@ -158,7 +158,7 @@
Modifier.isStatic(it.modifiers) && Modifier.isFinal(it.modifiers) &&
it.name.startsWith("CALLBACK_")
}
- assertEquals(12, constants.size)
+ assertEquals(13, constants.size)
}
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
index cb98454..16a30aa 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
@@ -55,25 +55,28 @@
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@IgnoreUpTo(Build.VERSION_CODES.R)
-class CSLocalAgentCreationTests(
- private val sdkLevel: Int,
- private val isTv: Boolean,
- private val addLocalNetCapToRequest: Boolean
-) : CSTest() {
+class CSLocalAgentCreationTests : CSTest() {
+ @Parameterized.Parameter(0) lateinit var params: TestParams
+
+ data class TestParams(
+ val sdkLevel: Int,
+ val isTv: Boolean = false,
+ val addLocalNetCapToRequest: Boolean = true)
+
companion object {
@JvmStatic
@Parameterized.Parameters
fun arguments() = listOf(
- arrayOf(VERSION_V, false /* isTv */, true /* addLocalNetCapToRequest */),
- arrayOf(VERSION_V, false /* isTv */, false /* addLocalNetCapToRequest */),
- arrayOf(VERSION_V, true /* isTv */, true /* addLocalNetCapToRequest */),
- arrayOf(VERSION_V, true /* isTv */, false /* addLocalNetCapToRequest */),
- arrayOf(VERSION_U, false /* isTv */, true /* addLocalNetCapToRequest */),
- arrayOf(VERSION_U, false /* isTv */, false /* addLocalNetCapToRequest */),
- arrayOf(VERSION_U, true /* isTv */, true /* addLocalNetCapToRequest */),
- arrayOf(VERSION_U, true /* isTv */, false /* addLocalNetCapToRequest */),
- arrayOf(VERSION_T, false /* isTv */, false /* addLocalNetCapToRequest */),
- arrayOf(VERSION_T, true /* isTv */, false /* addLocalNetCapToRequest */),
+ TestParams(VERSION_V, isTv = false, addLocalNetCapToRequest = true),
+ TestParams(VERSION_V, isTv = false, addLocalNetCapToRequest = false),
+ TestParams(VERSION_V, isTv = true, addLocalNetCapToRequest = true),
+ TestParams(VERSION_V, isTv = true, addLocalNetCapToRequest = false),
+ TestParams(VERSION_U, isTv = false, addLocalNetCapToRequest = true),
+ TestParams(VERSION_U, isTv = false, addLocalNetCapToRequest = false),
+ TestParams(VERSION_U, isTv = true, addLocalNetCapToRequest = true),
+ TestParams(VERSION_U, isTv = true, addLocalNetCapToRequest = false),
+ TestParams(VERSION_T, isTv = false, addLocalNetCapToRequest = false),
+ TestParams(VERSION_T, isTv = true, addLocalNetCapToRequest = false),
)
}
@@ -84,11 +87,11 @@
@Test
fun testLocalAgents() {
val netdInOrder = inOrder(netd)
- deps.setBuildSdk(sdkLevel)
- doReturn(isTv).`when`(packageManager).hasSystemFeature(FEATURE_LEANBACK)
+ deps.setBuildSdk(params.sdkLevel)
+ doReturn(params.isTv).`when`(packageManager).hasSystemFeature(FEATURE_LEANBACK)
val allNetworksCb = TestableNetworkCallback()
val request = NetworkRequest.Builder()
- if (addLocalNetCapToRequest) {
+ if (params.addLocalNetCapToRequest) {
request.addCapability(NET_CAPABILITY_LOCAL_NETWORK)
}
cm.registerNetworkCallback(request.build(), allNetworksCb)
@@ -96,7 +99,8 @@
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
addCapability(NET_CAPABILITY_LOCAL_NETWORK)
}.build()
- val localAgent = if (sdkLevel >= VERSION_V || sdkLevel == VERSION_U && isTv) {
+ val localAgent = if (params.sdkLevel >= VERSION_V
+ || params.sdkLevel == VERSION_U && params.isTv) {
Agent(nc = ncTemplate, score = keepConnectedScore(), lnc = defaultLnc())
} else {
assertFailsWith<IllegalArgumentException> { Agent(nc = ncTemplate, lnc = defaultLnc()) }
@@ -106,7 +110,7 @@
localAgent.connect()
netdInOrder.verify(netd).networkCreate(
makeNativeNetworkConfigLocal(localAgent.network.netId, INetd.PERMISSION_NONE))
- if (addLocalNetCapToRequest) {
+ if (params.addLocalNetCapToRequest) {
assertEquals(localAgent.network, allNetworksCb.expect<Available>().network)
} else {
allNetworksCb.assertNoCallback(NO_CALLBACK_TIMEOUT_MS)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
new file mode 100644
index 0000000..e698930
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P
+import android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkProvider
+import android.net.NetworkProvider.NetworkOfferCallback
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Reserved
+import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.TestableNetworkOfferCallback
+import com.android.testutils.TestableNetworkOfferCallback.CallbackEntry.OnNetworkNeeded
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private val ETHERNET_SCORE = NetworkScore.Builder().build()
+private val ETHERNET_CAPS = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .build()
+private val BLANKET_CAPS = NetworkCapabilities(ETHERNET_CAPS).apply {
+ reservationId = RES_ID_MATCH_ALL_RESERVATIONS
+}
+private val ETHERNET_REQUEST = NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addTransportType(TRANSPORT_TEST)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .build()
+
+private const val TIMEOUT_MS = 5_000L
+private const val NO_CB_TIMEOUT_MS = 200L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.R)
+class CSNetworkReservationTest : CSTest() {
+ private lateinit var provider: NetworkProvider
+ private val blanketOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+
+ @Before
+ fun subclassSetUp() {
+ provider = NetworkProvider(context, csHandlerThread.looper, "Ethernet provider")
+ cm.registerNetworkProvider(provider)
+
+ // register a blanket offer for use in tests.
+ provider.registerNetworkOffer(ETHERNET_SCORE, BLANKET_CAPS, blanketOffer)
+ }
+
+ fun NetworkCapabilities.copyWithReservationId(resId: Int) = NetworkCapabilities(this).also {
+ it.reservationId = resId
+ }
+
+ fun NetworkProvider.registerNetworkOffer(
+ score: NetworkScore,
+ caps: NetworkCapabilities,
+ cb: NetworkOfferCallback
+ ) {
+ registerNetworkOffer(score, caps, {r -> r.run()}, cb)
+ }
+
+ @Test
+ fun testReservationRequest() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ // validate the reservation matches the blanket offer.
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ // bring up reserved reservation offer
+ val reservedOfferCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, reservedOfferCaps, reservedOfferCb)
+
+ // validate onReserved was sent to the app
+ val onReservedCaps = cb.expect<Reserved>().caps
+ assertEquals(reservedOfferCaps, onReservedCaps)
+
+ // validate the reservation matches the reserved offer.
+ reservedOfferCb.expectOnNetworkNeeded(reservedOfferCaps)
+
+ // reserved offer goes away
+ provider.unregisterNetworkOffer(reservedOfferCb)
+ cb.expect<Unavailable>()
+ }
+
+ fun TestableNetworkOfferCallback.expectNoCallbackWhere(
+ predicate: (TestableNetworkOfferCallback.CallbackEntry) -> Boolean
+ ) {
+ val event = history.poll(NO_CB_TIMEOUT_MS) { predicate(it) }
+ assertNull(event)
+ }
+
+ @Test
+ fun testReservationRequest_notDeliveredToRegularOffer() {
+ val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, ETHERNET_CAPS, {r -> r.run()}, offerCb)
+
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ // validate the offer does not receive onNetworkNeeded for reservation request
+ offerCb.expectNoCallbackWhere {
+ it is OnNetworkNeeded && it.request.type == NetworkRequest.Type.RESERVATION
+ }
+ }
+
+ @Test
+ fun testReservedOffer_preventReservationIdUpdate() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ // validate the reservation matches the blanket offer.
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ // bring up reserved offer
+ val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+
+ cb.expect<Reserved>()
+ reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+
+ // try to update the offer's reservationId by reusing the same callback object.
+ // first file a new request to try and match the offer later.
+ val cb2 = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb2)
+
+ val reservationReq2 = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId2 = reservationReq2.networkCapabilities.reservationId
+
+ // try to update the offer's reservationId to an existing reservationId.
+ val updatedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId2)
+ provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, reservedOfferCb)
+
+ // validate the original offer disappeared.
+ cb.expect<Unavailable>()
+ // validate the new offer was rejected by CS.
+ reservedOfferCb.expectOnNetworkUnneeded(reservedCaps)
+ // validate cb2 never sees onReserved().
+ cb2.assertNoCallback()
+ }
+
+ @Test
+ fun testReservedOffer_capabilitiesCannotBeUpdated() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+
+ cb.expect<Reserved>()
+ reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+
+ // update reserved offer capabilities
+ val updatedCaps = NetworkCapabilities(reservedCaps).addCapability(NET_CAPABILITY_WIFI_P2P)
+ provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, reservedOfferCb)
+
+ cb.expect<Unavailable>()
+ reservedOfferCb.expectOnNetworkUnneeded(reservedCaps)
+ reservedOfferCb.assertNoCallback()
+ }
+
+ @Test
+ fun testBlanketOffer_updateAllowed() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+ blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS)
+
+ val updatedCaps = NetworkCapabilities(BLANKET_CAPS).addCapability(NET_CAPABILITY_WIFI_P2P)
+ provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, blanketOffer)
+ blanketOffer.assertNoCallback()
+
+ // Note: NetworkRequest.Builder(NetworkRequest) *does not* perform a defensive copy but
+ // changes the underlying request.
+ val p2pRequest = NetworkRequest.Builder(NetworkRequest(ETHERNET_REQUEST))
+ .addCapability(NET_CAPABILITY_WIFI_P2P)
+ .build()
+ cm.reserveNetwork(p2pRequest, csHandler, cb)
+ blanketOffer.expectOnNetworkNeeded(updatedCaps)
+ }
+
+ @Test
+ fun testReservationOffer_onlyAllowSingleOffer() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ val caps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ provider.registerNetworkOffer(ETHERNET_SCORE, caps, offerCb)
+ offerCb.expectOnNetworkNeeded(caps)
+ cb.expect<Reserved>()
+
+ val newOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, caps, newOfferCb)
+ newOfferCb.assertNoCallback()
+ cb.assertNoCallback()
+
+ // File a regular request and validate only the old offer gets onNetworkNeeded.
+ val cb2 = TestableNetworkCallback()
+ cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+ offerCb.expectOnNetworkNeeded(caps)
+ newOfferCb.assertNoCallback()
+ }
+
+ @Test
+ fun testReservationOffer_updateScore() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
+
+ val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+ reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+ reservedOfferCb.assertNoCallback()
+ cb.expect<Reserved>()
+
+ // update reserved offer capabilities
+ val newScore = NetworkScore.Builder().setShouldYieldToBadWifi(true).build()
+ provider.registerNetworkOffer(newScore, reservedCaps, reservedOfferCb)
+ cb.assertNoCallback()
+
+ val cb2 = TestableNetworkCallback()
+ cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+ reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+ reservedOfferCb.assertNoCallback()
+ }
+
+ @Test
+ fun testReservationOffer_regularOfferCanBeUpdated() {
+ val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ provider.registerNetworkOffer(ETHERNET_SCORE, ETHERNET_CAPS, offerCb)
+
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(ETHERNET_REQUEST, cb, csHandler)
+ offerCb.expectOnNetworkNeeded(ETHERNET_CAPS)
+ offerCb.assertNoCallback()
+
+ val updatedCaps = NetworkCapabilities(ETHERNET_CAPS).addCapability(NET_CAPABILITY_WIFI_P2P)
+ val newScore = NetworkScore.Builder().setShouldYieldToBadWifi(true).build()
+ provider.registerNetworkOffer(newScore, updatedCaps, offerCb)
+ offerCb.assertNoCallback()
+
+ val cb2 = TestableNetworkCallback()
+ cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+ offerCb.expectOnNetworkNeeded(ETHERNET_CAPS)
+ offerCb.assertNoCallback()
+ }
+}
diff --git a/tests/unit/jni/android_net_frameworktests_util/onload.cpp b/tests/unit/jni/android_net_frameworktests_util/onload.cpp
index 06a3986..a0ce4f8 100644
--- a/tests/unit/jni/android_net_frameworktests_util/onload.cpp
+++ b/tests/unit/jni/android_net_frameworktests_util/onload.cpp
@@ -24,6 +24,8 @@
int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name);
int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name);
+int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
+ char const *class_name);
extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
JNIEnv *env;
@@ -38,6 +40,10 @@
if (register_com_android_net_module_util_TcUtils(env,
"android/net/frameworktests/util/TcUtils") < 0) return JNI_ERR;
+ if (register_com_android_net_module_util_TimerFdUtils(
+ env, "android/net/frameworktests/util/TimerFdUtils") < 0)
+ return JNI_ERR;
+
return JNI_VERSION_1_6;
}
diff --git a/thread/demoapp/AndroidManifest.xml b/thread/demoapp/AndroidManifest.xml
index c31bb71..fddc151 100644
--- a/thread/demoapp/AndroidManifest.xml
+++ b/thread/demoapp/AndroidManifest.xml
@@ -33,6 +33,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
</application>
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 30d5a02..c55096b 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -351,14 +351,14 @@
}
otDaemon.initialize(
- mTunIfController.getTunFd(),
shouldEnableThread(),
newOtDaemonConfig(mPersistentSettings.getConfiguration()),
+ mTunIfController.getTunFd(),
mNsdPublisher,
getMeshcopTxtAttributes(mResources.get()),
- mOtDaemonCallbackProxy,
mCountryCodeSupplier.get(),
- FeatureFlags.isTrelEnabled());
+ FeatureFlags.isTrelEnabled(),
+ mOtDaemonCallbackProxy);
otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
mOtDaemon = otDaemon;
mHandler.post(mNat64CidrController::maybeUpdateNat64Cidr);
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index aeeed65..875a4ad 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -16,7 +16,6 @@
package android.net.thread;
-import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
import static android.net.InetAddresses.parseNumericAddress;
import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET;
import static android.net.thread.utils.IntegrationTestUtils.buildIcmpv4EchoReply;
@@ -38,7 +37,6 @@
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
-import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.truth.Truth.assertThat;
@@ -56,7 +54,6 @@
import android.net.RouteInfo;
import android.net.thread.utils.FullThreadDevice;
import android.net.thread.utils.InfraNetworkDevice;
-import android.net.thread.utils.IntegrationTestUtils;
import android.net.thread.utils.OtDaemonController;
import android.net.thread.utils.TestTunNetworkUtils;
import android.net.thread.utils.ThreadFeatureCheckerRule;
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
index 07d0390..801e21e 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -39,6 +39,7 @@
import android.os.SystemClock
import android.system.OsConstants
import android.system.OsConstants.IPPROTO_ICMP
+import android.util.Log
import androidx.test.core.app.ApplicationProvider
import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
import com.android.net.module.util.IpUtils
@@ -84,6 +85,8 @@
/** Utilities for Thread integration tests. */
object IntegrationTestUtils {
+ private val TAG = IntegrationTestUtils::class.simpleName
+
// The timeout of join() after restarting ot-daemon. The device needs to send 6 Link Request
// every 5 seconds, followed by 4 Parent Request every second. So this value needs to be 40
// seconds to be safe
@@ -388,7 +391,12 @@
raMsg ?: return pioList
val buf = ByteBuffer.wrap(raMsg)
- val ipv6Header = Struct.parse(Ipv6Header::class.java, buf)
+ val ipv6Header = try {
+ Struct.parse(Ipv6Header::class.java, buf)
+ } catch (e: IllegalArgumentException) {
+ // the packet is not IPv6
+ return pioList
+ }
if (ipv6Header.nextHeader != OsConstants.IPPROTO_ICMPV6.toByte()) {
return pioList
}
@@ -478,6 +486,7 @@
val serviceInfoFuture = CompletableFuture<NsdServiceInfo>()
val listener: NsdManager.DiscoveryListener = object : DefaultDiscoveryListener() {
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
+ Log.d(TAG, "onServiceFound: $serviceInfo")
serviceInfoFuture.complete(serviceInfo)
}
}
@@ -525,6 +534,7 @@
val resolvedServiceInfoFuture = CompletableFuture<NsdServiceInfo>()
val callback: NsdManager.ServiceInfoCallback = object : DefaultServiceInfoCallback() {
override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
+ Log.d(TAG, "onServiceUpdated: $serviceInfo")
if (predicate.test(serviceInfo)) {
resolvedServiceInfoFuture.complete(serviceInfo)
}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index 9af0b53..dcbb3f5 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -17,6 +17,7 @@
package com.android.server.thread;
import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
@@ -266,6 +267,7 @@
public void tearDown() throws Exception {
runAsShell(
WRITE_DEVICE_CONFIG,
+ WRITE_ALLOWLISTED_DEVICE_CONFIG,
() -> DeviceConfig.deleteProperty("thread_network", "TrelFeature__enabled"));
}
@@ -338,6 +340,7 @@
public void initialize_trelFeatureDisabled_trelDisabledAtOtDaemon() throws Exception {
runAsShell(
WRITE_DEVICE_CONFIG,
+ WRITE_ALLOWLISTED_DEVICE_CONFIG,
() ->
DeviceConfig.setProperty(
"thread_network", "TrelFeature__enabled", "false", false));
@@ -352,6 +355,7 @@
public void initialize_trelFeatureEnabled_setTrelEnabledAtOtDamon() throws Exception {
runAsShell(
WRITE_DEVICE_CONFIG,
+ WRITE_ALLOWLISTED_DEVICE_CONFIG,
() ->
DeviceConfig.setProperty(
"thread_network", "TrelFeature__enabled", "true", false));