Merge changes Ia25b28d5,Ic6bbd8c0 into main
* changes:
Fix flakes due to tests resetting CarrierConfig
Fix lint errors in NetworkAgentTest
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/common/TetheringLib/api/module-lib-current.txt b/Tethering/common/TetheringLib/api/module-lib-current.txt
index 01bd983..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);
}
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index 3b9708e..c0c0abc 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -20,9 +20,9 @@
}
public class TetheringManager {
- 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 stopAllTethering();
- method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopTethering(int);
+ 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_LOCAL = 2; // 0x2
field public static final String EXTRA_ACTIVE_LOCAL_ONLY = "android.net.extra.ACTIVE_LOCAL_ONLY";
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 25bfb45..21f36e8 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -28,6 +28,7 @@
import android.content.Context;
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiManager;
+import android.os.Build;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.IBinder;
@@ -388,7 +389,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();
@@ -423,9 +426,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;
}
@@ -656,6 +658,13 @@
}
}
+ private void unsupportedAfterV() {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ throw new UnsupportedOperationException("Not supported after SDK version "
+ + Build.VERSION_CODES.VANILLA_ICE_CREAM);
+ }
+ }
+
/**
* Attempt to tether the named interface. This will setup a dhcp server
* on the interface, forward and NAT IP v4 packets and forward DNS requests
@@ -665,8 +674,10 @@
* access will of course fail until an upstream network interface becomes
* active.
*
- * @deprecated The only usages is PanService. It uses this for legacy reasons
- * and will migrate away as soon as possible.
+ * @deprecated Legacy tethering API. Callers should instead use
+ * {@link #startTethering(int, Executor, StartTetheringCallback)}.
+ * On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will
+ * throw an UnsupportedOperationException.
*
* @param iface the interface name to tether.
* @return error a {@code TETHER_ERROR} value indicating success or failure type
@@ -676,6 +687,8 @@
@Deprecated
@SystemApi(client = MODULE_LIBRARIES)
public int tether(@NonNull final String iface) {
+ unsupportedAfterV();
+
final String callerPkg = mContext.getOpPackageName();
Log.i(TAG, "tether caller:" + callerPkg);
final RequestDispatcher dispatcher = new RequestDispatcher();
@@ -699,14 +712,18 @@
/**
* Stop tethering the named interface.
*
- * @deprecated The only usages is PanService. It uses this for legacy reasons
- * and will migrate away as soon as possible.
+ * @deprecated Legacy tethering API. Callers should instead use
+ * {@link #stopTethering(int)}.
+ * On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will
+ * throw an UnsupportedOperationException.
*
* {@hide}
*/
@Deprecated
@SystemApi(client = MODULE_LIBRARIES)
public int untether(@NonNull final String iface) {
+ unsupportedAfterV();
+
final String callerPkg = mContext.getOpPackageName();
Log.i(TAG, "untether caller:" + callerPkg);
@@ -1187,6 +1204,17 @@
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
@@ -1196,8 +1224,6 @@
&& 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);
}
@@ -1290,18 +1316,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) {
@@ -1312,14 +1332,9 @@
* 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();
@@ -1374,9 +1389,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.
@@ -1387,10 +1399,7 @@
* @hide
*/
@SystemApi
- @RequiresPermission(anyOf = {
- android.Manifest.permission.TETHER_PRIVILEGED,
- android.Manifest.permission.WRITE_SETTINGS
- })
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
public void requestLatestTetheringEntitlementResult(@TetheringType int type,
boolean showEntitlementUi,
@NonNull Executor executor,
@@ -2073,10 +2082,7 @@
* @hide
*/
@SystemApi
- @RequiresPermission(anyOf = {
- android.Manifest.permission.TETHER_PRIVILEGED,
- android.Manifest.permission.WRITE_SETTINGS
- })
+ @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
public void stopAllTethering() {
final String callerPkg = mContext.getOpPackageName();
Log.i(TAG, "stopAllTethering caller:" + callerPkg);
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 d6f4572..6229f6d 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -16,6 +16,7 @@
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;
@@ -173,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. */
@@ -906,7 +907,7 @@
ArraySet<IpPrefix> deprecatedPrefixes, ArraySet<IpPrefix> newPrefixes) {
// [1] Remove the routes that are deprecated.
if (!deprecatedPrefixes.isEmpty()) {
- removeRoutesFromNetworkAndLinkProperties(INetd.LOCAL_NET_ID,
+ removeRoutesFromNetworkAndLinkProperties(LOCAL_NET_ID,
getLocalRoutesFor(mIfaceName, deprecatedPrefixes));
}
@@ -918,7 +919,7 @@
}
if (!addedPrefixes.isEmpty()) {
- addRoutesToNetworkAndLinkProperties(INetd.LOCAL_NET_ID,
+ addRoutesToNetworkAndLinkProperties(LOCAL_NET_ID,
getLocalRoutesFor(mIfaceName, addedPrefixes));
}
}
@@ -1123,7 +1124,7 @@
}
try {
- NetdUtils.tetherInterface(mNetd, INetd.LOCAL_NET_ID, mIfaceName,
+ NetdUtils.tetherInterface(mNetd, LOCAL_NET_ID, mIfaceName,
asIpPrefix(mIpv4Address));
} catch (RemoteException | ServiceSpecificException | IllegalStateException e) {
mLog.e("Error Tethering", e);
@@ -1146,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);
@@ -1188,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:
@@ -1224,12 +1225,12 @@
}
// Remove deprecated routes from downstream network.
- removeRoutesFromNetworkAndLinkProperties(INetd.LOCAL_NET_ID,
+ removeRoutesFromNetworkAndLinkProperties(LOCAL_NET_ID,
List.of(getDirectConnectedRoute(deprecatedLinkAddress)));
mLinkProperties.removeLinkAddress(deprecatedLinkAddress);
// Add new routes to downstream network.
- addRoutesToNetworkAndLinkProperties(INetd.LOCAL_NET_ID,
+ addRoutesToNetworkAndLinkProperties(LOCAL_NET_ID,
List.of(getDirectConnectedRoute(mIpv4Address)));
mLinkProperties.addLinkAddress(mIpv4Address);
@@ -1473,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/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 254b60f..40b1ec0 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;
@@ -105,6 +108,7 @@
import android.net.wifi.p2p.WifiP2pInfo;
import android.net.wifi.p2p.WifiP2pManager;
import android.os.Binder;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -145,6 +149,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 +456,10 @@
return mSettingsObserver;
}
+ boolean isTetheringWithSoftApConfigEnabled() {
+ return mDeps.isTetheringWithSoftApConfigEnabled();
+ }
+
/**
* Start to register callbacks.
* Call this function when tethering is ready to handle callback events.
@@ -592,13 +601,40 @@
// This method needs to exist because TETHERING_BLUETOOTH before Android T and TETHERING_WIGIG
// can't use enableIpServing.
private void processInterfaceStateChange(final String iface, boolean enabled) {
+ final int type = ifaceNameToType(iface);
// Do not listen to USB interface state changes or USB interface add/removes. USB tethering
// is driven only by USB_ACTION broadcasts.
- final int type = ifaceNameToType(iface);
if (type == TETHERING_USB || type == TETHERING_NCM) return;
+ // On T+, BLUETOOTH uses enableIpServing.
if (type == TETHERING_BLUETOOTH && SdkLevel.isAtLeastT()) return;
+ // Cannot happen: on S+, tetherableWigigRegexps is always empty.
+ if (type == TETHERING_WIGIG && SdkLevel.isAtLeastS()) return;
+
+ // After V, disallow this legacy codepath from starting tethering of any type:
+ // everything must call ensureIpServerStarted directly.
+ //
+ // Don't touch the teardown path for now. It's more complicated because:
+ // - ensureIpServerStarted and ensureIpServerStopped act on different
+ // tethering types.
+ // - Depending on the type, ensureIpServerStopped is either called twice (once
+ // on interface down and once on interface removed) or just once (on
+ // interface removed).
+ //
+ // Note that this only affects WIFI and WIFI_P2P. The other types are either
+ // ignored above, or ignored by ensureIpServerStarted. Note that even for WIFI
+ // and WIFI_P2P, this code should not ever run in normal use, because the
+ // hotspot and p2p code do not call tether(). It's possible that this could
+ // happen in the field due to unforeseen OEM modifications. If it does happen,
+ // a terrible error is logged in tether().
+ // TODO: fix the teardown path to stop depending on interface state notifications.
+ // These are not necessary since most/all link layers have their own teardown
+ // notifications, and can race with those notifications.
+ if (enabled && Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ return;
+ }
+
if (enabled) {
ensureIpServerStarted(iface);
} else {
@@ -664,7 +700,7 @@
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)) {
+ if (unfinishedRequest != null && !unfinishedRequest.equalsIgnoreUidPackage(request)) {
enableTetheringInternal(type, false /* disabled */,
unfinishedRequest.getInterfaceName(), null);
mEntitlementMgr.stopProvisioningIfNeeded(type);
@@ -991,8 +1027,45 @@
return TETHER_ERROR_NO_ERROR;
}
+ /**
+ * Legacy tether API that starts tethering with CONNECTIVITY_SCOPE_GLOBAL on the given iface.
+ *
+ * This API relies on the IpServer having been started for the interface by
+ * processInterfaceStateChanged beforehand, which is only possible for
+ * - WIGIG Pre-S
+ * - BLUETOOTH Pre-T
+ * - WIFI
+ * - WIFI_P2P.
+ * Note that WIFI and WIFI_P2P already start tethering on their respective ifaces via
+ * WIFI_(AP/P2P_STATE_CHANGED broadcasts, which makes this API redundant for those types unless
+ * those broadcasts are disabled by OEM.
+ */
void tether(String iface, int requestedState, final IIntResultListener listener) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ // After V, the TetheringManager and ConnectivityManager tether and untether methods
+ // throw UnsupportedOperationException, so this cannot happen in normal use. Ensure
+ // that this code cannot run even if callers use raw binder calls or other
+ // unsupported methods.
+ return;
+ }
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) { }
@@ -1026,6 +1099,13 @@
}
void untether(String iface, final IIntResultListener listener) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ // After V, the TetheringManager and ConnectivityManager tether and untether methods
+ // throw UnsupportedOperationException, so this cannot happen in normal use. Ensure
+ // that this code cannot run even if callers use raw binder calls or other
+ // unsupported methods.
+ return;
+ }
mHandler.post(() -> {
try {
listener.onResult(untether(iface));
@@ -2089,7 +2169,7 @@
}
mRoutingCoordinator.maybeRemoveDeprecatedUpstreams();
- mUpstreamNetworkMonitor.startObserveAllNetworks();
+ mUpstreamNetworkMonitor.startObserveUpstreamNetworks();
// TODO: De-duplicate with updateUpstreamWanted() below.
if (upstreamWanted()) {
@@ -2207,9 +2287,9 @@
break;
}
case EVENT_REQUEST_CHANGE_DOWNSTREAM: {
- final boolean enabled = message.arg1 == 1;
- final TetheringRequest request = (TetheringRequest) message.obj;
- enableTetheringInternal(request.getTetheringType(), enabled, null, null);
+ final int tetheringType = message.arg1;
+ final Boolean enabled = (Boolean) message.obj;
+ enableTetheringInternal(tetheringType, enabled, null, null);
break;
}
default:
@@ -2787,9 +2867,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 6485ffd..0c44a38 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -310,6 +310,10 @@
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/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index c329142..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,9 +186,11 @@
// - 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);
// - Test previous enabled hotspot prefix(cached prefix) is reserved.
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
@@ -207,7 +209,6 @@
assertNotEquals(asIpPrefix(mLegacyWifiP2pAddress), etherPrefix);
assertNotEquals(asIpPrefix(mBluetoothAddress), etherPrefix);
assertNotEquals(hotspotPrefix, etherPrefix);
- releaseDownstream(mHotspotIpServer);
releaseDownstream(mEthernetIpServer);
}
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 d94852e..cc80251 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -154,31 +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 */, NETWORK_SETTINGS, TETHER_PRIVILEGED);
+ 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);
@@ -188,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();
@@ -213,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();
});
@@ -224,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();
});
@@ -252,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();
});
@@ -263,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();
});
@@ -297,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();
});
@@ -308,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();
});
@@ -341,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();
});
@@ -361,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();
});
@@ -446,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();
});
@@ -457,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();
});
@@ -486,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();
@@ -501,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();
});
}
@@ -600,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();
});
@@ -611,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();
});
@@ -637,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();
});
@@ -648,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 0c6a95d..97758cf 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -267,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;
@@ -785,7 +786,10 @@
if (interfaceName != null) {
builder.setInterfaceName(interfaceName);
}
- return builder.build();
+ TetheringRequest request = builder.build();
+ request.setUid(TEST_CALLER_UID);
+ request.setPackageName(TEST_CALLER_PKG);
+ return request;
}
@NonNull
@@ -1032,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);
@@ -1260,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.
@@ -1859,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(),
@@ -2587,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);
@@ -2786,6 +2790,17 @@
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,
@@ -3757,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));
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/headers/include/bpf_helpers.h b/bpf/headers/include/bpf_helpers.h
index b994a9f..0bd3421 100644
--- a/bpf/headers/include/bpf_helpers.h
+++ b/bpf/headers/include/bpf_helpers.h
@@ -62,8 +62,8 @@
// Android Mainline BpfLoader when running on Android V (sdk=35)
#define BPFLOADER_MAINLINE_V_VERSION (BPFLOADER_MAINLINE_U_QPR3_VERSION + 1u)
-// Android Mainline BpfLoader when running on Android W (sdk=36)
-#define BPFLOADER_MAINLINE_W_VERSION (BPFLOADER_MAINLINE_V_VERSION + 1u)
+// Android Mainline BpfLoader when running on Android 25Q2 (sdk=36)
+#define BPFLOADER_MAINLINE_25Q2_VERSION (BPFLOADER_MAINLINE_V_VERSION + 1u)
/* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
* before #include "bpf_helpers.h" to change which bpfloaders will
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index ce144a7..038786c 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -1409,17 +1409,15 @@
//
// Also note that 'android_get_device_api_level()' is what the
// //system/core/init/apex_init_util.cpp
- // apex init .XXrc parsing code uses for XX filtering.
- //
- // That code has a hack to bump <35 to 35 (to force aosp/main to parse .35rc),
- // but could (should?) perhaps be adjusted to match this.
- const int effective_api_level = android_get_device_api_level() + (int)unreleased;
- const bool isAtLeastT = (effective_api_level >= __ANDROID_API_T__);
- const bool isAtLeastU = (effective_api_level >= __ANDROID_API_U__);
- const bool isAtLeastV = (effective_api_level >= __ANDROID_API_V__);
- const bool isAtLeastW = (effective_api_level > __ANDROID_API_V__); // TODO: switch to W
+ // apex init .XXrc parsing code uses for XX filtering, and that code
+ // (now) similarly uses __ANDROID_API_FUTURE__ for non 'REL' codenames.
+ const int api_level = unreleased ? __ANDROID_API_FUTURE__ : android_get_device_api_level();
+ const bool isAtLeastT = (api_level >= __ANDROID_API_T__);
+ const bool isAtLeastU = (api_level >= __ANDROID_API_U__);
+ const bool isAtLeastV = (api_level >= __ANDROID_API_V__);
+ const bool isAtLeast25Q2 = (api_level > __ANDROID_API_V__); // TODO: fix >
- const int first_api_level = GetIntProperty("ro.board.first_api_level", effective_api_level);
+ const int first_api_level = GetIntProperty("ro.board.first_api_level", api_level);
// last in U QPR2 beta1
const bool has_platform_bpfloader_rc = exists("/system/etc/init/bpfloader.rc");
@@ -1432,10 +1430,10 @@
if (isAtLeastU) ++bpfloader_ver; // [44] BPFLOADER_MAINLINE_U_VERSION
if (runningAsRoot) ++bpfloader_ver; // [45] BPFLOADER_MAINLINE_U_QPR3_VERSION
if (isAtLeastV) ++bpfloader_ver; // [46] BPFLOADER_MAINLINE_V_VERSION
- if (isAtLeastW) ++bpfloader_ver; // [47] BPFLOADER_MAINLINE_W_VERSION
+ if (isAtLeast25Q2) ++bpfloader_ver; // [47] BPFLOADER_MAINLINE_25Q2_VERSION
ALOGI("NetBpfLoad v0.%u (%s) api:%d/%d kver:%07x (%s) uid:%d rc:%d%d",
- bpfloader_ver, argv[0], android_get_device_api_level(), effective_api_level,
+ bpfloader_ver, argv[0], android_get_device_api_level(), api_level,
kernelVersion(), describeArch(), getuid(),
has_platform_bpfloader_rc, has_platform_netbpfload_rc);
@@ -1475,6 +1473,13 @@
return 1;
}
+ // 25Q2 bumps the kernel requirement up to 5.4
+ // see also: //system/netd/tests/kernel_test.cpp TestKernel54
+ if (isAtLeast25Q2 && !isAtLeastKernelVersion(5, 4, 0)) {
+ ALOGE("Android 25Q2 requires kernel 5.4.");
+ return 1;
+ }
+
// Technically already required by U, but only enforce on V+
// see also: //system/netd/tests/kernel_test.cpp TestKernel64Bit
if (isAtLeastV && isKernel32Bit() && isAtLeastKernelVersion(5, 16, 0)) {
@@ -1498,13 +1503,13 @@
bool bad = false;
if (!isLtsKernel()) {
- ALOGW("Android V only supports LTS kernels.");
+ ALOGW("Android V+ only supports LTS kernels.");
bad = true;
}
#define REQUIRE(maj, min, sub) \
if (isKernelVersion(maj, min) && !isAtLeastKernelVersion(maj, min, sub)) { \
- ALOGW("Android V requires %d.%d kernel to be %d.%d.%d+.", maj, min, maj, min, sub); \
+ ALOGW("Android V+ requires %d.%d kernel to be %d.%d.%d+.", maj, min, maj, min, sub); \
bad = true; \
}
diff --git a/bpf/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
index 340acda..bcd0cba 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -70,6 +70,13 @@
return netdutils::status::ok;
}
+// Checks if the device is running on release version of Android 25Q2 or newer.
+static bool isAtLeast25Q2() {
+ return android_get_device_api_level() >= 36 ||
+ (android_get_device_api_level() == 35 &&
+ modules::sdklevel::detail::IsAtLeastPreReleaseCodename("Baklava"));
+}
+
static Status initPrograms(const char* cg2_path) {
if (!cg2_path) return Status("cg2_path is NULL");
@@ -91,6 +98,16 @@
return Status("U+ platform with cg2_path != /sys/fs/cgroup is unsupported");
}
+ // V bumps the kernel requirement up to 4.19
+ if (modules::sdklevel::IsAtLeastV() && !bpf::isAtLeastKernelVersion(4, 19, 0)) {
+ return Status("V+ platform with kernel version < 4.19.0 is unsupported");
+ }
+
+ // 25Q2 bumps the kernel requirement up to 5.4
+ if (isAtLeast25Q2() && !bpf::isAtLeastKernelVersion(5, 4, 0)) {
+ return Status("25Q2+ platform with kernel version < 5.4.0 is unsupported");
+ }
+
unique_fd cg_fd(open(cg2_path, O_DIRECTORY | O_RDONLY | O_CLOEXEC));
if (!cg_fd.ok()) {
const int err = errno;
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/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/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-t/src/android/net/nsd/OffloadServiceInfo.java b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
index e4b2f43..fd824f3 100644
--- a/framework-t/src/android/net/nsd/OffloadServiceInfo.java
+++ b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
@@ -282,7 +282,7 @@
}
/**
- * Get the service type. (e.g. "_http._tcp.local" )
+ * Get the service type. (e.g. "_http._tcp" )
*/
@NonNull
public String getServiceType() {
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 7bc0cf3..323c533 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
}
@@ -231,6 +233,32 @@
field @NonNull public static final android.os.Parcelable.Creator<android.net.IpPrefix> CREATOR;
}
+ @FlaggedApi("com.android.net.flags.ipv6_over_ble") public final class L2capNetworkSpecifier extends android.net.NetworkSpecifier implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getHeaderCompression();
+ method public int getPsm();
+ method @Nullable public android.net.MacAddress getRemoteAddress();
+ method public int getRole();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.L2capNetworkSpecifier> CREATOR;
+ field public static final int HEADER_COMPRESSION_6LOWPAN = 2; // 0x2
+ field public static final int HEADER_COMPRESSION_ANY = 0; // 0x0
+ field public static final int HEADER_COMPRESSION_NONE = 1; // 0x1
+ field public static final int PSM_ANY = 0; // 0x0
+ field public static final int ROLE_ANY = 0; // 0x0
+ field public static final int ROLE_CLIENT = 1; // 0x1
+ field public static final int ROLE_SERVER = 2; // 0x2
+ }
+
+ public static final class L2capNetworkSpecifier.Builder {
+ ctor public L2capNetworkSpecifier.Builder();
+ method @NonNull public android.net.L2capNetworkSpecifier build();
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setHeaderCompression(int);
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setPsm(@IntRange(from=0, to=255) int);
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setRemoteAddress(@Nullable android.net.MacAddress);
+ method @NonNull public android.net.L2capNetworkSpecifier.Builder setRole(int);
+ }
+
public class LinkAddress implements android.os.Parcelable {
method public int describeContents();
method public java.net.InetAddress getAddress();
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 009344d..5d99b74 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;
@@ -3064,7 +3065,8 @@
* <p>WARNING: New clients should not use this function. The only usages should be in PanService
* and WifiStateMachine which need direct access. All other clients should use
* {@link #startTethering} and {@link #stopTethering} which encapsulate proper provisioning
- * logic.</p>
+ * logic. On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will throw
+ * an UnsupportedOperationException.</p>
*
* @param iface the interface name to tether.
* @return error a {@code TETHER_ERROR} value indicating success or failure type
@@ -3089,7 +3091,8 @@
* <p>WARNING: New clients should not use this function. The only usages should be in PanService
* and WifiStateMachine which need direct access. All other clients should use
* {@link #startTethering} and {@link #stopTethering} which encapsulate proper provisioning
- * logic.</p>
+ * logic. On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will throw
+ * an UnsupportedOperationException.</p>
*
* @param iface the interface name to untether.
* @return error a {@code TETHER_ERROR} value indicating success or failure type
@@ -4271,12 +4274,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() {}
@@ -4417,6 +4426,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;
}
@@ -4468,6 +4499,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.
@@ -4487,6 +4520,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);
}
@@ -4517,6 +4551,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.
@@ -4596,6 +4631,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;
@@ -4977,6 +5017,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.
*
diff --git a/framework/src/android/net/L2capNetworkSpecifier.java b/framework/src/android/net/L2capNetworkSpecifier.java
new file mode 100644
index 0000000..cfc9ed9
--- /dev/null
+++ b/framework/src/android/net/L2capNetworkSpecifier.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2025 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.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Build;
+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.util.Objects;
+
+/**
+ * A {@link NetworkSpecifier} used to identify an L2CAP network over BLE.
+ *
+ * An L2CAP network is not symmetrical, meaning there exists both a server (Bluetooth peripheral)
+ * and a client (Bluetooth central) node. This specifier contains the information required to
+ * request a client L2CAP network using {@link ConnectivityManager#requestNetwork} while specifying
+ * the remote MAC address, and Protocol/Service Multiplexer (PSM). It can also contain information
+ * allocated by the system when reserving a server network using {@link
+ * ConnectivityManager#reserveNetwork} such as the Protocol/Service Multiplexer (PSM). In both
+ * cases, the header compression option must be specified.
+ *
+ * An L2CAP server network allocates a Protocol/Service Multiplexer (PSM) to be advertised to the
+ * client. A new server network must always be reserved using {@code
+ * ConnectivityManager#reserveNetwork}. The subsequent {@link
+ * ConnectivityManager.NetworkCallback#onReserved(NetworkCapabilities)} callback includes an {@code
+ * L2CapNetworkSpecifier}. The {@link getPsm()} method will return the Protocol/Service Multiplexer
+ * (PSM) of the reserved network so that the server can advertise it to the client and the client
+ * can connect.
+ * An L2CAP server network is backed by a {@link android.bluetooth.BluetoothServerSocket} which can,
+ * in theory, accept many connections. However, before SDK version {@link
+ * Build.VERSION_CODES.VANILLA_ICE_CREAM} Bluetooth APIs do not expose the channel ID, so these
+ * connections are indistinguishable. In practice, this means that the network matching semantics in
+ * ConnectivityService will tear down all but the first connection.
+ *
+ * When the connection between client and server completes, a {@link Network} whose capabilities
+ * satisfy this {@code L2capNetworkSpecifier} will connect and the usual callbacks, such as {@link
+ * NetworkCallback#onAvailable}, will be called on the callback object passed to {@code
+ * ConnectivityManager#reserveNetwork} or {@code ConnectivityManager#requestNetwork}.
+ */
+@FlaggedApi(Flags.FLAG_IPV6_OVER_BLE)
+public final class L2capNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+ /**
+ * Match any role.
+ *
+ * This role is only meaningful in {@link NetworkRequest}s. Specifiers for actual L2CAP
+ * networks never have this role set.
+ */
+ public static final int ROLE_ANY = 0;
+ /** Specifier describes a client network, i.e., the device is the Bluetooth central. */
+ public static final int ROLE_CLIENT = 1;
+ /** Specifier describes a server network, i.e., the device is the Bluetooth peripheral. */
+ public static final int ROLE_SERVER = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, prefix = "ROLE_", value = {
+ ROLE_ANY,
+ ROLE_CLIENT,
+ ROLE_SERVER
+ })
+ public @interface Role {}
+ /** Role used to distinguish client from server networks. */
+ @Role
+ private final int mRole;
+
+ /**
+ * Accept any form of header compression.
+ *
+ * This option is only meaningful in {@link NetworkRequest}s. Specifiers for actual L2CAP
+ * networks never have this option set.
+ */
+ public static final int HEADER_COMPRESSION_ANY = 0;
+ /** Do not compress packets on this network. */
+ public static final int HEADER_COMPRESSION_NONE = 1;
+ /** Use 6lowpan header compression as specified in rfc6282. */
+ public static final int HEADER_COMPRESSION_6LOWPAN = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, prefix = "HEADER_COMPRESSION_", value = {
+ HEADER_COMPRESSION_ANY,
+ HEADER_COMPRESSION_NONE,
+ HEADER_COMPRESSION_6LOWPAN
+ })
+ public @interface HeaderCompression {}
+ /** Header compression mechanism used on this network. */
+ @HeaderCompression
+ private final int mHeaderCompression;
+
+ /** The MAC address of the remote. */
+ @Nullable
+ private final MacAddress mRemoteAddress;
+
+ /**
+ * Match any Protocol/Service Multiplexer (PSM).
+ *
+ * This PSM value is only meaningful in {@link NetworkRequest}s. Specifiers for actual L2CAP
+ * networks never have this value set.
+ */
+ public static final int PSM_ANY = 0;
+
+ /** The Bluetooth L2CAP Protocol/Service Multiplexer (PSM). */
+ private final int mPsm;
+
+ private L2capNetworkSpecifier(Parcel in) {
+ mRole = in.readInt();
+ mHeaderCompression = in.readInt();
+ mRemoteAddress = in.readParcelable(getClass().getClassLoader());
+ mPsm = in.readInt();
+ }
+
+ /** @hide */
+ public L2capNetworkSpecifier(@Role int role, @HeaderCompression int headerCompression,
+ MacAddress remoteAddress, int psm) {
+ mRole = role;
+ mHeaderCompression = headerCompression;
+ mRemoteAddress = remoteAddress;
+ mPsm = psm;
+ }
+
+ /** Returns the role to be used for this network. */
+ @Role
+ public int getRole() {
+ return mRole;
+ }
+
+ /** Returns the compression mechanism for this network. */
+ @HeaderCompression
+ public int getHeaderCompression() {
+ return mHeaderCompression;
+ }
+
+ /**
+ * Returns the remote MAC address for this network to connect to.
+ *
+ * The remote address is only meaningful for networks that have ROLE_CLIENT.
+ *
+ * When receiving this {@link L2capNetworkSpecifier} from Connectivity APIs such as a {@link
+ * ConnectivityManager.NetworkCallback}, the MAC address is redacted.
+ */
+ public @Nullable MacAddress getRemoteAddress() {
+ return mRemoteAddress;
+ }
+
+ /** Returns the Protocol/Service Multiplexer (PSM) for this network to connect to. */
+ public int getPsm() {
+ return mPsm;
+ }
+
+ /** A builder class for L2capNetworkSpecifier. */
+ public static final class Builder {
+ @Role
+ private int mRole = ROLE_ANY;
+ @HeaderCompression
+ private int mHeaderCompression = HEADER_COMPRESSION_ANY;
+ @Nullable
+ private MacAddress mRemoteAddress;
+ private int mPsm = PSM_ANY;
+
+ /**
+ * Set the role to use for this network.
+ *
+ * If not set, defaults to {@link ROLE_ANY}.
+ *
+ * @param role the role to use.
+ */
+ @NonNull
+ public Builder setRole(@Role int role) {
+ mRole = role;
+ return this;
+ }
+
+ /**
+ * Set the header compression mechanism to use for this network.
+ *
+ * If not set, defaults to {@link HEADER_COMPRESSION_ANY}. This option must be specified
+ * (i.e. must not be set to {@link HEADER_COMPRESSION_ANY}) when requesting or reserving a
+ * new network.
+ *
+ * @param headerCompression the header compression mechanism to use.
+ */
+ @NonNull
+ public Builder setHeaderCompression(@HeaderCompression int headerCompression) {
+ mHeaderCompression = headerCompression;
+ return this;
+ }
+
+ /**
+ * Set the remote address for the client to connect to.
+ *
+ * Only valid for client networks. If not set, the specifier matches any MAC address.
+ *
+ * @param remoteAddress the MAC address to connect to, or null to match any MAC address.
+ */
+ @NonNull
+ public Builder setRemoteAddress(@Nullable MacAddress remoteAddress) {
+ mRemoteAddress = remoteAddress;
+ return this;
+ }
+
+ /**
+ * Set the Protocol/Service Multiplexer (PSM) for the client to connect to.
+ *
+ * If not set, defaults to {@link PSM_ANY}.
+ *
+ * @param psm the Protocol/Service Multiplexer (PSM) to connect to.
+ */
+ @NonNull
+ public Builder setPsm(@IntRange(from = 0, to = 255) int psm) {
+ if (psm < 0 /* PSM_ANY */ || psm > 0xFF) {
+ throw new IllegalArgumentException("PSM must be PSM_ANY or within range [1, 255]");
+ }
+ mPsm = psm;
+ return this;
+ }
+
+ /** Create the L2capNetworkSpecifier object. */
+ @NonNull
+ public L2capNetworkSpecifier build() {
+ if (mRole == ROLE_SERVER && mRemoteAddress != null) {
+ throw new IllegalArgumentException(
+ "Specifying a remote address is not valid for server role.");
+ }
+ return new L2capNetworkSpecifier(mRole, mHeaderCompression, mRemoteAddress, mPsm);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public boolean canBeSatisfiedBy(NetworkSpecifier other) {
+ if (!(other instanceof L2capNetworkSpecifier)) return false;
+ final L2capNetworkSpecifier rhs = (L2capNetworkSpecifier) other;
+
+ // A network / offer cannot be ROLE_ANY, but it is added for consistency.
+ if (mRole != rhs.mRole && mRole != ROLE_ANY && rhs.mRole != ROLE_ANY) {
+ return false;
+ }
+
+ if (mHeaderCompression != rhs.mHeaderCompression
+ && mHeaderCompression != HEADER_COMPRESSION_ANY
+ && rhs.mHeaderCompression != HEADER_COMPRESSION_ANY) {
+ return false;
+ }
+
+ if (!Objects.equals(mRemoteAddress, rhs.mRemoteAddress)
+ && mRemoteAddress != null && rhs.mRemoteAddress != null) {
+ return false;
+ }
+
+ if (mPsm != rhs.mPsm && mPsm != PSM_ANY && rhs.mPsm != PSM_ANY) {
+ return false;
+ }
+ return true;
+ }
+
+ /** @hide */
+ @Override
+ @Nullable
+ public NetworkSpecifier redact() {
+ final NetworkSpecifier redactedSpecifier = new Builder()
+ .setRole(mRole)
+ .setHeaderCompression(mHeaderCompression)
+ // The remote address is redacted.
+ .setRemoteAddress(null)
+ .setPsm(mPsm)
+ .build();
+ return redactedSpecifier;
+ }
+
+ /** @hide */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mRole, mHeaderCompression, mRemoteAddress, mPsm);
+ }
+
+ /** @hide */
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof L2capNetworkSpecifier)) return false;
+
+ final L2capNetworkSpecifier rhs = (L2capNetworkSpecifier) obj;
+ return mRole == rhs.mRole
+ && mHeaderCompression == rhs.mHeaderCompression
+ && Objects.equals(mRemoteAddress, rhs.mRemoteAddress)
+ && mPsm == rhs.mPsm;
+ }
+
+ /** @hide */
+ @Override
+ public String toString() {
+ final String role;
+ switch (mRole) {
+ case ROLE_CLIENT:
+ role = "ROLE_CLIENT";
+ break;
+ case ROLE_SERVER:
+ role = "ROLE_SERVER";
+ break;
+ default:
+ role = "ROLE_ANY";
+ break;
+ }
+
+ final String headerCompression;
+ switch (mHeaderCompression) {
+ case HEADER_COMPRESSION_NONE:
+ headerCompression = "HEADER_COMPRESSION_NONE";
+ break;
+ case HEADER_COMPRESSION_6LOWPAN:
+ headerCompression = "HEADER_COMPRESSION_6LOWPAN";
+ break;
+ default:
+ headerCompression = "HEADER_COMPRESSION_ANY";
+ break;
+ }
+
+ final String psm = (mPsm == PSM_ANY) ? "PSM_ANY" : String.valueOf(mPsm);
+
+ return String.format("L2capNetworkSpecifier(%s, %s, RemoteAddress=%s, PSM=%s)",
+ role, headerCompression, Objects.toString(mRemoteAddress), psm);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mRole);
+ dest.writeInt(mHeaderCompression);
+ dest.writeParcelable(mRemoteAddress, flags);
+ dest.writeInt(mPsm);
+ }
+
+ public static final @NonNull Creator<L2capNetworkSpecifier> CREATOR = new Creator<>() {
+ @Override
+ public L2capNetworkSpecifier createFromParcel(Parcel in) {
+ return new L2capNetworkSpecifier(in);
+ }
+
+ @Override
+ public L2capNetworkSpecifier[] newArray(int size) {
+ return new L2capNetworkSpecifier[size];
+ }
+ };
+}
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 5ae25ab..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;
@@ -256,6 +257,9 @@
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) {
@@ -278,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
@@ -674,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];
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/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
index 66ae79c..ac381b8 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -89,6 +89,9 @@
break;
case BroadcastRequest.PRESENCE_VERSION_V1:
if (adapter.isLeExtendedAdvertisingSupported()) {
+ if (mAdvertisingSetCallback == null) {
+ mAdvertisingSetCallback = getAdvertisingSetCallback();
+ }
bluetoothLeAdvertiser.startAdvertisingSet(
getAdvertisingSetParameters(),
advertiseData,
@@ -133,6 +136,11 @@
}
mBroadcastListener = null;
mIsAdvertising = false;
+ // If called startAdvertisingSet() but onAdvertisingSetStopped() is not invoked yet,
+ // using the same mAdvertisingSetCallback will cause new advertising cann't be stopped.
+ // Therefore, release the old mAdvertisingSetCallback and
+ // create a new mAdvertisingSetCallback when calling startAdvertisingSet.
+ mAdvertisingSetCallback = null;
}
}
diff --git a/networksecurity/TEST_MAPPING b/networksecurity/TEST_MAPPING
index 20ecbce..f9238c3 100644
--- a/networksecurity/TEST_MAPPING
+++ b/networksecurity/TEST_MAPPING
@@ -1,5 +1,14 @@
{
- "postsubmit": [
+ "presubmit": [
+ {
+ "name": "CtsNetSecConfigCertificateTransparencyTestCases"
+ },
+ {
+ "name": "CtsNetSecConfigCertificateTransparencyDefaultTestCases"
+ },
+ {
+ "name": "NetSecConfigCertificateTransparencySctLogListTestCases"
+ },
{
"name": "NetworkSecurityUnitTests"
}
diff --git a/networksecurity/service/Android.bp b/networksecurity/service/Android.bp
index f27acb7..d7aacdb 100644
--- a/networksecurity/service/Android.bp
+++ b/networksecurity/service/Android.bp
@@ -24,12 +24,14 @@
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: [
@@ -49,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 56a5ee5..ce14fc6 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -13,9 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.android.server.net.ct;
-import static java.nio.charset.StandardCharsets.UTF_8;
+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_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 android.annotation.RequiresApi;
import android.app.DownloadManager;
@@ -25,18 +29,19 @@
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Build;
+import android.provider.DeviceConfig;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
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.util.ArrayList;
+import java.util.List;
/** Helper class to download certificate transparency log files. */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@@ -48,35 +53,60 @@
private final DataStore mDataStore;
private final DownloadHelper mDownloadHelper;
private final SignatureVerifier mSignatureVerifier;
- private final CertificateTransparencyInstaller mInstaller;
+ private final CertificateTransparencyLogger mLogger;
+
+ private final List<CompatibilityVersion> mCompatVersions = new ArrayList<>();
+
+ private boolean started = false;
CertificateTransparencyDownloader(
Context context,
DataStore dataStore,
DownloadHelper downloadHelper,
SignatureVerifier signatureVerifier,
- CertificateTransparencyInstaller installer) {
+ CertificateTransparencyLogger logger) {
mContext = context;
mSignatureVerifier = signatureVerifier;
mDataStore = dataStore;
mDownloadHelper = downloadHelper;
- mInstaller = installer;
+ mLogger = logger;
}
- void initialize() {
- mInstaller.addCompatibilityVersion(Config.COMPATIBILITY_VERSION);
+ void addCompatibilityVersion(CompatibilityVersion compatVersion) {
+ mCompatVersions.add(compatVersion);
+ }
- IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
- mContext.registerReceiver(this, intentFilter, Context.RECEIVER_EXPORTED);
+ void start() {
+ if (started) {
+ return;
+ }
+ mContext.registerReceiver(
+ this,
+ new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
+ Context.RECEIVER_EXPORTED);
+ mDataStore.load();
+ started = true;
if (Config.DEBUG) {
- Log.d(TAG, "CertificateTransparencyDownloader initialized successfully");
+ Log.d(TAG, "CertificateTransparencyDownloader started.");
+ }
+ }
+
+ void stop() {
+ if (!started) {
+ return;
+ }
+ mContext.unregisterReceiver(this);
+ mDataStore.delete();
+ started = false;
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyDownloader stopped.");
}
}
long startPublicKeyDownload() {
- long downloadId = download(mDataStore.getProperty(Config.PUBLIC_KEY_URL));
+ long downloadId = download(Config.URL_PUBLIC_KEY);
if (downloadId != -1) {
mDataStore.setPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, downloadId);
mDataStore.store();
@@ -84,19 +114,31 @@
return downloadId;
}
- long startMetadataDownload() {
- long downloadId = download(mDataStore.getProperty(Config.METADATA_URL));
+ private long startMetadataDownload(CompatibilityVersion compatVersion) {
+ long downloadId = download(compatVersion.getMetadataUrl());
if (downloadId != -1) {
- mDataStore.setPropertyLong(Config.METADATA_DOWNLOAD_ID, downloadId);
+ mDataStore.setPropertyLong(compatVersion.getMetadataPropertyName(), downloadId);
mDataStore.store();
}
return downloadId;
}
- long startContentDownload() {
- long downloadId = download(mDataStore.getProperty(Config.CONTENT_URL));
+ @VisibleForTesting
+ void startMetadataDownload() {
+ for (CompatibilityVersion compatVersion : mCompatVersions) {
+ if (startMetadataDownload(compatVersion) == -1) {
+ Log.e(TAG, "Metadata download not started for " + compatVersion.getCompatVersion());
+ } else if (Config.DEBUG) {
+ Log.d(TAG, "Metadata download started for " + compatVersion.getCompatVersion());
+ }
+ }
+ }
+
+ @VisibleForTesting
+ long startContentDownload(CompatibilityVersion compatVersion) {
+ long downloadId = download(compatVersion.getContentUrl());
if (downloadId != -1) {
- mDataStore.setPropertyLong(Config.CONTENT_DOWNLOAD_ID, downloadId);
+ mDataStore.setPropertyLong(compatVersion.getContentPropertyName(), downloadId);
mDataStore.store();
}
return downloadId;
@@ -110,25 +152,28 @@
return;
}
- long completedId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
+ long completedId =
+ intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, /* defaultValue= */ -1);
if (completedId == -1) {
Log.e(TAG, "Invalid completed download Id");
return;
}
- if (isPublicKeyDownloadId(completedId)) {
+ if (getPublicKeyDownloadId() == completedId) {
handlePublicKeyDownloadCompleted(completedId);
return;
}
- if (isMetadataDownloadId(completedId)) {
- handleMetadataDownloadCompleted(completedId);
- return;
- }
+ for (CompatibilityVersion compatVersion : mCompatVersions) {
+ if (getMetadataDownloadId(compatVersion) == completedId) {
+ handleMetadataDownloadCompleted(compatVersion, completedId);
+ return;
+ }
- if (isContentDownloadId(completedId)) {
- handleContentDownloadCompleted(completedId);
- return;
+ if (getContentDownloadId(compatVersion) == completedId) {
+ handleContentDownloadCompleted(compatVersion, completedId);
+ return;
+ }
}
Log.i(TAG, "Download id " + completedId + " is not recognized.");
@@ -154,78 +199,138 @@
return;
}
- if (startMetadataDownload() == -1) {
- Log.e(TAG, "Metadata download not started.");
- } else if (Config.DEBUG) {
- Log.d(TAG, "Metadata download started successfully.");
- }
+ startMetadataDownload();
}
- private void handleMetadataDownloadCompleted(long downloadId) {
+ private void handleMetadataDownloadCompleted(
+ CompatibilityVersion compatVersion, long downloadId) {
DownloadStatus status = mDownloadHelper.getDownloadStatus(downloadId);
if (!status.isSuccessful()) {
handleDownloadFailed(status);
return;
}
- if (startContentDownload() == -1) {
- Log.e(TAG, "Content download not started.");
+ if (startContentDownload(compatVersion) == -1) {
+ Log.e(TAG, "Content download failed for" + compatVersion.getCompatVersion());
} else if (Config.DEBUG) {
- Log.d(TAG, "Content download started successfully.");
+ Log.d(TAG, "Content download started for" + compatVersion.getCompatVersion());
}
}
- private void handleContentDownloadCompleted(long downloadId) {
+ private void handleContentDownloadCompleted(
+ CompatibilityVersion compatVersion, long downloadId) {
DownloadStatus status = mDownloadHelper.getDownloadStatus(downloadId);
if (!status.isSuccessful()) {
handleDownloadFailed(status);
return;
}
- Uri contentUri = getContentDownloadUri();
- Uri metadataUri = getMetadataDownloadUri();
+ Uri contentUri = getContentDownloadUri(compatVersion);
+ Uri metadataUri = getMetadataDownloadUri(compatVersion);
if (contentUri == null || metadataUri == null) {
Log.e(TAG, "Invalid URIs");
return;
}
boolean success = false;
+ int failureReason = -1;
+
try {
success = mSignatureVerifier.verify(contentUri, metadataUri);
+ } catch (MissingPublicKeyException e) {
+ if (updateFailureCount()) {
+ failureReason = CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND;
+ }
+ } catch (InvalidKeyException e) {
+ if (updateFailureCount()) {
+ failureReason = CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION;
+ }
} catch (IOException | GeneralSecurityException e) {
Log.e(TAG, "Could not verify new log list", e);
}
+
if (!success) {
Log.w(TAG, "Log list did not pass verification");
- return;
- }
- 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);
+ // Avoid logging failure twice
+ if (failureReason == -1 && updateFailureCount()) {
+ failureReason = CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_VERIFICATION;
+ }
+
+ if (failureReason != -1) {
+ mLogger.logCTLogListUpdateFailedEvent(
+ failureReason,
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0));
+ }
return;
}
try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
- success = mInstaller.install(Config.COMPATIBILITY_VERSION, inputStream, version);
+ success = compatVersion.install(inputStream);
} catch (IOException e) {
Log.e(TAG, "Could not install new content", e);
return;
}
if (success) {
- // Update information about the stored version on successful install.
- mDataStore.setProperty(Config.VERSION, version);
+ // 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, "Download failed with " + status);
- // TODO(378626065): Report failure via statsd.
+
+ if (updateFailureCount()) {
+ int failureCount =
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0);
+
+ if (status.isHttpError()) {
+ 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.logCTLogListUpdateFailedEventWithDownloadStatus(
+ status.reason(), failureCount);
+ }
+ }
+ }
+
+ /**
+ * 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();
+
+ int threshold = DeviceConfig.getInt(
+ Config.NAMESPACE_NETWORK_SECURITY,
+ Config.FLAG_LOG_FAILURE_THRESHOLD,
+ Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+
+ boolean shouldReport = new_failure_count >= threshold;
+ if (shouldReport) {
+ Log.d(TAG, "Log list update failure count exceeds threshold: " + new_failure_count);
+ }
+ return shouldReport;
}
private long download(String url) {
@@ -239,17 +344,19 @@
@VisibleForTesting
long getPublicKeyDownloadId() {
- return mDataStore.getPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, -1);
+ return mDataStore.getPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, /* defaultValue= */ -1);
}
@VisibleForTesting
- long getMetadataDownloadId() {
- return mDataStore.getPropertyLong(Config.METADATA_DOWNLOAD_ID, -1);
+ long getMetadataDownloadId(CompatibilityVersion compatVersion) {
+ return mDataStore.getPropertyLong(
+ compatVersion.getMetadataPropertyName(), /* defaultValue */ -1);
}
@VisibleForTesting
- long getContentDownloadId() {
- return mDataStore.getPropertyLong(Config.CONTENT_DOWNLOAD_ID, -1);
+ long getContentDownloadId(CompatibilityVersion compatVersion) {
+ return mDataStore.getPropertyLong(
+ compatVersion.getContentPropertyName(), /* defaultValue= */ -1);
}
@VisibleForTesting
@@ -259,38 +366,27 @@
@VisibleForTesting
boolean hasMetadataDownloadId() {
- return getMetadataDownloadId() != -1;
+ return mCompatVersions.stream()
+ .map(this::getMetadataDownloadId)
+ .anyMatch(downloadId -> downloadId != -1);
}
@VisibleForTesting
boolean hasContentDownloadId() {
- return getContentDownloadId() != -1;
- }
-
- @VisibleForTesting
- boolean isPublicKeyDownloadId(long downloadId) {
- return getPublicKeyDownloadId() == downloadId;
- }
-
- @VisibleForTesting
- boolean isMetadataDownloadId(long downloadId) {
- return getMetadataDownloadId() == downloadId;
- }
-
- @VisibleForTesting
- boolean isContentDownloadId(long downloadId) {
- return getContentDownloadId() == downloadId;
+ return mCompatVersions.stream()
+ .map(this::getContentDownloadId)
+ .anyMatch(downloadId -> downloadId != -1);
}
private Uri getPublicKeyDownloadUri() {
return mDownloadHelper.getUri(getPublicKeyDownloadId());
}
- private Uri getMetadataDownloadUri() {
- return mDownloadHelper.getUri(getMetadataDownloadId());
+ private Uri getMetadataDownloadUri(CompatibilityVersion compatVersion) {
+ return mDownloadHelper.getUri(getMetadataDownloadId(compatVersion));
}
- private Uri getContentDownloadUri() {
- return mDownloadHelper.getUri(getContentDownloadId());
+ private Uri getContentDownloadUri(CompatibilityVersion compatVersion) {
+ return mDownloadHelper.getUri(getContentDownloadId(compatVersion));
}
}
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 3138ea7..0000000
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ /dev/null
@@ -1,128 +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 SignatureVerifier mSignatureVerifier;
- private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
-
- CertificateTransparencyFlagsListener(
- DataStore dataStore,
- SignatureVerifier signatureVerifier,
- CertificateTransparencyDownloader certificateTransparencyDownloader) {
- mDataStore = dataStore;
- mSignatureVerifier = signatureVerifier;
- 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 {
- mSignatureVerifier.setPublicKey(newPublicKey);
- } catch (GeneralSecurityException | IllegalArgumentException 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.CONTENT_URL, newContentUrl);
- mDataStore.setProperty(Config.METADATA_URL, newMetadataUrl);
- mDataStore.store();
-
- if (mCertificateTransparencyDownloader.startMetadataDownload() == -1) {
- Log.e(TAG, "Metadata download not started.");
- } else if (Config.DEBUG) {
- Log.d(TAG, "Metadata download started successfully.");
- }
- }
-}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
deleted file mode 100644
index 9970667..0000000
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
+++ /dev/null
@@ -1,92 +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.util.Log;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Installer of CT log lists. */
-public class CertificateTransparencyInstaller {
-
- private static final String TAG = "CertificateTransparencyInstaller";
-
- private final Map<String, CompatibilityVersion> mCompatVersions = new HashMap<>();
-
- // The CT root directory.
- private final File mRootDirectory;
-
- public CertificateTransparencyInstaller(File rootDirectory) {
- mRootDirectory = rootDirectory;
- }
-
- public CertificateTransparencyInstaller(String rootDirectoryPath) {
- this(new File(rootDirectoryPath));
- }
-
- public CertificateTransparencyInstaller() {
- this(Config.CT_ROOT_DIRECTORY_PATH);
- }
-
- void addCompatibilityVersion(String versionName) {
- removeCompatibilityVersion(versionName);
- CompatibilityVersion newCompatVersion =
- new CompatibilityVersion(new File(mRootDirectory, versionName));
- mCompatVersions.put(versionName, newCompatVersion);
- }
-
- void removeCompatibilityVersion(String versionName) {
- CompatibilityVersion compatVersion = mCompatVersions.remove(versionName);
- if (compatVersion != null && !compatVersion.delete()) {
- Log.w(TAG, "Could not delete compatibility version directory.");
- }
- }
-
- CompatibilityVersion getCompatibilityVersion(String versionName) {
- return mCompatVersions.get(versionName);
- }
-
- /**
- * Install a new log list to use during SCT verification.
- *
- * @param compatibilityVersion the compatibility version of the new log list
- * @param newContent an input stream providing the log list
- * @param version the minor version of the new log list
- * @return true if the log list was installed successfully, false otherwise.
- * @throws IOException if the list cannot be saved in the CT directory.
- */
- public boolean install(String compatibilityVersion, InputStream newContent, String version)
- throws IOException {
- CompatibilityVersion compatVersion = mCompatVersions.get(compatibilityVersion);
- if (compatVersion == null) {
- Log.e(TAG, "No compatibility version for " + compatibilityVersion);
- return false;
- }
- // Ensure root directory exists and is readable.
- DirectoryUtils.makeDir(mRootDirectory);
-
- if (!compatVersion.install(newContent, version)) {
- Log.e(TAG, "Failed to install logs version " + version);
- return false;
- }
- Log.i(TAG, "New logs installed at " + compatVersion.getLogsDir());
- return true;
- }
-}
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 bf23cb0..a8acc60 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
@@ -22,7 +22,9 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
import android.os.Build;
+import android.os.ConfigUpdate;
import android.os.SystemClock;
import android.util.Log;
@@ -31,57 +33,83 @@
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 static final String UPDATE_CONFIG_PERMISSION = "android.permission.UPDATE_CONFIG";
private final Context mContext;
- private final DataStore mDataStore;
+ private final CompatibilityVersion mCompatVersion;
private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
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) {
+ Context context, CertificateTransparencyDownloader certificateTransparencyDownloader) {
mContext = context;
- mDataStore = dataStore;
+ mCompatVersion =
+ new CompatibilityVersion(
+ Config.COMPATIBILITY_VERSION,
+ Config.URL_SIGNATURE,
+ Config.URL_LOG_LIST,
+ Config.CT_ROOT_DIRECTORY_PATH);
mCertificateTransparencyDownloader = certificateTransparencyDownloader;
+ mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
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();
+ mCompatVersion.delete();
+ 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;
}
+ if (context.checkCallingOrSelfPermission(UPDATE_CONFIG_PERMISSION)
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.e(TAG, "Caller does not have UPDATE_CONFIG permission.");
+ return;
+ }
if (Config.DEBUG) {
Log.d(TAG, "Starting CT daily job.");
}
-
- 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 (!mDependenciesReady) {
+ mCertificateTransparencyDownloader.start();
+ mDependenciesReady = true;
+ }
if (mCertificateTransparencyDownloader.startPublicKeyDownload() == -1) {
Log.e(TAG, "Public key download not started.");
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..913c472
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
@@ -0,0 +1,48 @@
+/*
+ * 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 failure is provided by DownloadManager.
+ *
+ * @param downloadStatus DownloadManager failure status why the log list wasn't updated
+ * @param failureCount number of consecutive log list update failures
+ */
+ void logCTLogListUpdateFailedEventWithDownloadStatus(int downloadStatus, int failureCount);
+
+ /**
+ * Logs a CTLogListUpdateFailed event to statsd, when no HTTP error status code is present.
+ *
+ * @param failureReason reason why the log list wasn't updated
+ * @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..b97a885
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
@@ -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.server.net.ct;
+
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED;
+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_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__PENDING_WAITING_FOR_WIFI;
+
+import android.app.DownloadManager;
+
+/** Implementation for logging to statsd for Certificate Transparency. */
+class CertificateTransparencyLoggerImpl implements CertificateTransparencyLogger {
+
+ @Override
+ public void logCTLogListUpdateFailedEventWithDownloadStatus(
+ int downloadStatus, int failureCount) {
+ logCTLogListUpdateFailedEvent(downloadStatusToFailureReason(downloadStatus), failureCount);
+ }
+
+ @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
+ );
+ }
+
+ /** 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;
+ }
+ }
+
+}
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 782e6b5..ed98056 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -18,7 +18,6 @@
import static android.security.Flags.certificateTransparencyConfiguration;
-import static com.android.net.ct.flags.Flags.certificateTransparencyJob;
import static com.android.net.ct.flags.Flags.certificateTransparencyService;
import android.annotation.RequiresApi;
@@ -26,26 +25,29 @@
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.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 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,
- /* defaultValue= */ false)
- && certificateTransparencyService()
- && certificateTransparencyConfiguration();
+ return certificateTransparencyService() && certificateTransparencyConfiguration();
}
/** Creates a new {@link CertificateTransparencyService} object. */
@@ -59,11 +61,8 @@
dataStore,
downloadHelper,
signatureVerifier,
- new CertificateTransparencyInstaller());
- mFlagsListener =
- new CertificateTransparencyFlagsListener(dataStore, signatureVerifier, downloader);
- mCertificateTransparencyJob =
- new CertificateTransparencyJob(context, dataStore, downloader);
+ new CertificateTransparencyLoggerImpl());
+ mCertificateTransparencyJob = new CertificateTransparencyJob(context, downloader);
}
/**
@@ -74,13 +73,50 @@
public void onBootPhase(int phase) {
switch (phase) {
case SystemService.PHASE_BOOT_COMPLETED:
- if (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/CompatibilityVersion.java b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
index 27488b5..9d60163 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
@@ -15,58 +15,95 @@
*/
package com.android.server.net.ct;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.annotation.RequiresApi;
+import android.os.Build;
import android.system.ErrnoException;
import android.system.Os;
+import android.util.Log;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
/** Represents a compatibility version directory. */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
class CompatibilityVersion {
+ private static final String TAG = "CompatibilityVersion";
+
static final String LOGS_DIR_PREFIX = "logs-";
static final String LOGS_LIST_FILE_NAME = "log_list.json";
+ static final String CURRENT_LOGS_DIR_SYMLINK_NAME = "current";
- private static final String CURRENT_LOGS_DIR_SYMLINK_NAME = "current";
+ private final String mCompatVersion;
+ private final String mMetadataUrl;
+ private final String mContentUrl;
private final File mRootDirectory;
+ private final File mVersionDirectory;
private final File mCurrentLogsDirSymlink;
- private File mCurrentLogsDir = null;
-
- CompatibilityVersion(File rootDirectory) {
+ CompatibilityVersion(
+ String compatVersion, String metadataUrl, String contentUrl, File rootDirectory) {
+ mCompatVersion = compatVersion;
+ mMetadataUrl = metadataUrl;
+ mContentUrl = contentUrl;
mRootDirectory = rootDirectory;
- mCurrentLogsDirSymlink = new File(mRootDirectory, CURRENT_LOGS_DIR_SYMLINK_NAME);
+ mVersionDirectory = new File(rootDirectory, compatVersion);
+ mCurrentLogsDirSymlink = new File(mVersionDirectory, CURRENT_LOGS_DIR_SYMLINK_NAME);
+ }
+
+ CompatibilityVersion(
+ String compatVersion, String metadataUrl, String contentUrl, String rootDirectoryPath) {
+ this(compatVersion, metadataUrl, contentUrl, new File(rootDirectoryPath));
}
/**
* Installs a log list within this compatibility version directory.
*
* @param newContent an input stream providing the log list
- * @param version the version number of the log list
* @return true if the log list was installed successfully, false otherwise.
* @throws IOException if the list cannot be saved in the CT directory.
*/
- boolean install(InputStream newContent, String version) throws IOException {
- // To support atomically replacing the old configuration directory with the new there's a
- // bunch of steps. We create a new directory with the logs and then do an atomic update of
- // the current symlink to point to the new directory.
- // 1. Ensure that the root directory exists and is readable.
- DirectoryUtils.makeDir(mRootDirectory);
+ boolean install(InputStream newContent) throws IOException {
+ String content = new String(newContent.readAllBytes(), UTF_8);
+ try {
+ return install(
+ new ByteArrayInputStream(content.getBytes()),
+ new JSONObject(content).getString("version"));
+ } catch (JSONException e) {
+ Log.e(TAG, "invalid log list format", e);
+ return false;
+ }
+ }
- File newLogsDir = new File(mRootDirectory, LOGS_DIR_PREFIX + version);
+ private boolean install(InputStream newContent, String version) throws IOException {
+ // To support atomically replacing the old configuration directory with the new
+ // there's a bunch of steps. We create a new directory with the logs and then do
+ // an atomic update of the current symlink to point to the new directory.
+ // 1. Ensure the path to the root and version directories exist and are readable.
+ DirectoryUtils.makeDir(mRootDirectory);
+ DirectoryUtils.makeDir(mVersionDirectory);
+
+ File newLogsDir = new File(mVersionDirectory, LOGS_DIR_PREFIX + version);
// 2. Handle the corner case where the new directory already exists.
if (newLogsDir.exists()) {
- // If the symlink has already been updated then the update died between steps 6 and 7
- // and so we cannot delete the directory since it is in use.
+ // If the symlink has already been updated then the update died between steps 6
+ // and 7 and so we cannot delete the directory since it is in use.
if (newLogsDir.getCanonicalPath().equals(mCurrentLogsDirSymlink.getCanonicalPath())) {
+ Log.i(TAG, newLogsDir + " already exists, skipping install.");
deleteOldLogDirectories();
return false;
}
- // If the symlink has not been updated then the previous installation failed and this is
- // a re-attempt. Clean-up leftover files and try again.
+ // If the symlink has not been updated then the previous installation failed and
+ // this is a re-attempt. Clean-up leftover files and try again.
DirectoryUtils.removeDir(newLogsDir);
}
try {
@@ -80,8 +117,8 @@
}
DirectoryUtils.setWorldReadable(logListFile);
- // 5. Create temp symlink. We rename this to the target symlink to get an atomic update.
- File tempSymlink = new File(mRootDirectory, "new_symlink");
+ // 5. Create temp symlink. We rename to the target symlink for an atomic update.
+ File tempSymlink = new File(mVersionDirectory, "new_symlink");
try {
Os.symlink(newLogsDir.getCanonicalPath(), tempSymlink.getCanonicalPath());
} catch (ErrnoException e) {
@@ -95,17 +132,33 @@
throw e;
}
// 7. Cleanup
- mCurrentLogsDir = newLogsDir;
+ Log.i(TAG, "New logs installed at " + newLogsDir);
deleteOldLogDirectories();
return true;
}
- File getRootDir() {
- return mRootDirectory;
+ String getCompatVersion() {
+ return mCompatVersion;
}
- File getLogsDir() {
- return mCurrentLogsDir;
+ String getMetadataUrl() {
+ return mMetadataUrl;
+ }
+
+ String getMetadataPropertyName() {
+ return mCompatVersion + "_" + Config.METADATA_DOWNLOAD_ID;
+ }
+
+ String getContentUrl() {
+ return mContentUrl;
+ }
+
+ String getContentPropertyName() {
+ return mCompatVersion + "_" + Config.CONTENT_DOWNLOAD_ID;
+ }
+
+ File getVersionDir() {
+ return mVersionDirectory;
}
File getLogsDirSymlink() {
@@ -113,19 +166,21 @@
}
File getLogsFile() {
- return new File(mCurrentLogsDir, LOGS_LIST_FILE_NAME);
+ return new File(mCurrentLogsDirSymlink, LOGS_LIST_FILE_NAME);
}
- boolean delete() {
- return DirectoryUtils.removeDir(mRootDirectory);
+ void delete() {
+ if (!DirectoryUtils.removeDir(mVersionDirectory)) {
+ Log.w(TAG, "Could not delete compatibility version directory " + mVersionDirectory);
+ }
}
private void deleteOldLogDirectories() throws IOException {
- if (!mRootDirectory.exists()) {
+ if (!mVersionDirectory.exists()) {
return;
}
File currentTarget = mCurrentLogsDirSymlink.getCanonicalFile();
- for (File file : mRootDirectory.listFiles()) {
+ for (File file : mVersionDirectory.listFiles()) {
if (!currentTarget.equals(file.getCanonicalFile())
&& file.getName().startsWith(LOGS_DIR_PREFIX)) {
DirectoryUtils.removeDir(file);
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 70d8e42..bc4efab 100644
--- a/networksecurity/service/src/com/android/server/net/ct/Config.java
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -45,19 +45,21 @@
static final String FLAG_METADATA_URL = FLAGS_PREFIX + "metadata_url";
static final String FLAG_VERSION = FLAGS_PREFIX + "version";
static final String FLAG_PUBLIC_KEY = FLAGS_PREFIX + "public_key";
+ static final String FLAG_LOG_FAILURE_THRESHOLD = FLAGS_PREFIX + "log_list_failure_threshold";
// properties
static final String VERSION = "version";
- static final String CONTENT_URL = "content_url";
static final String CONTENT_DOWNLOAD_ID = "content_download_id";
- static final String METADATA_URL = "metadata_url";
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 DEFAULT_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..8180316 100644
--- a/networksecurity/service/src/com/android/server/net/ct/DataStore.java
+++ b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
@@ -57,6 +57,11 @@
}
}
+ boolean delete() {
+ clear();
+ return mPropertyFile.delete();
+ }
+
long getPropertyLong(String key, long defaultValue) {
return Optional.ofNullable(getProperty(key)).map(Long::parseLong).orElse(defaultValue);
}
@@ -64,4 +69,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
index 0b775ca..96488fc 100644
--- a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
+++ b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
@@ -27,7 +27,6 @@
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;
@@ -74,9 +73,10 @@
mPublicKey = Optional.of(publicKey);
}
- boolean verify(Uri file, Uri signature) throws GeneralSecurityException, IOException {
+ boolean verify(Uri file, Uri signature)
+ throws GeneralSecurityException, IOException, MissingPublicKeyException {
if (!mPublicKey.isPresent()) {
- throw new InvalidKeyException("Missing public key for signature verification");
+ throw new MissingPublicKeyException("Missing public key for signature verification");
}
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(mPublicKey.get());
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 ffa1283..2f57fc9 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
@@ -13,14 +13,20 @@
* 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__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.anyString;
+import static org.mockito.ArgumentMatchers.anyInt;
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;
@@ -66,14 +72,14 @@
public class CertificateTransparencyDownloaderTest {
@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 CompatibilityVersion mCompatVersion;
private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
private long mNextDownloadId = 666;
@@ -87,8 +93,7 @@
mPublicKey = keyPair.getPublic();
mContext = InstrumentationRegistry.getInstrumentation().getContext();
- mTempFile = File.createTempFile("datastore-test", ".properties");
- mDataStore = new DataStore(mTempFile);
+ mDataStore = new DataStore(File.createTempFile("datastore-test", ".properties"));
mSignatureVerifier = new SignatureVerifier(mContext);
mCertificateTransparencyDownloader =
new CertificateTransparencyDownloader(
@@ -96,55 +101,64 @@
mDataStore,
new DownloadHelper(mDownloadManager),
mSignatureVerifier,
- mCertificateTransparencyInstaller);
+ mLogger);
+ mCompatVersion =
+ new CompatibilityVersion(
+ /* compatVersion= */ "v666",
+ Config.URL_SIGNATURE,
+ Config.URL_LOG_LIST,
+ mContext.getFilesDir());
- prepareDataStore();
prepareDownloadManager();
+ mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
+ mCertificateTransparencyDownloader.start();
}
@After
public void tearDown() {
- mTempFile.delete();
mSignatureVerifier.resetPublicKey();
+ mCertificateTransparencyDownloader.stop();
+ mCompatVersion.delete();
}
@Test
public void testDownloader_startPublicKeyDownload() {
assertThat(mCertificateTransparencyDownloader.hasPublicKeyDownloadId()).isFalse();
+
long downloadId = mCertificateTransparencyDownloader.startPublicKeyDownload();
assertThat(mCertificateTransparencyDownloader.hasPublicKeyDownloadId()).isTrue();
- assertThat(mCertificateTransparencyDownloader.isPublicKeyDownloadId(downloadId)).isTrue();
+ assertThat(mCertificateTransparencyDownloader.getPublicKeyDownloadId())
+ .isEqualTo(downloadId);
}
@Test
public void testDownloader_startMetadataDownload() {
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
- long downloadId = mCertificateTransparencyDownloader.startMetadataDownload();
+
+ mCertificateTransparencyDownloader.startMetadataDownload();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isTrue();
- assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isTrue();
}
@Test
public void testDownloader_startContentDownload() {
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
- long downloadId = mCertificateTransparencyDownloader.startContentDownload();
+
+ mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isTrue();
- assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isTrue();
}
@Test
public void testDownloader_publicKeyDownloadSuccess_updatePublicKey_startMetadataDownload()
throws Exception {
- long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
- setSuccessfulDownload(publicKeyId, writePublicKeyToFile(mPublicKey));
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(publicKeyId));
+ mContext, makePublicKeyDownloadCompleteIntent(writePublicKeyToFile(mPublicKey)));
assertThat(mSignatureVerifier.getPublicKey()).hasValue(mPublicKey);
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isTrue();
@@ -154,14 +168,14 @@
public void
testDownloader_publicKeyDownloadSuccess_updatePublicKeyFail_doNotStartMetadataDownload()
throws Exception {
- long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
- setSuccessfulDownload(
- publicKeyId, writeToFile("i_am_not_a_base64_encoded_public_key".getBytes()));
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(publicKeyId));
+ mContext,
+ makePublicKeyDownloadCompleteIntent(
+ writeToFile("i_am_not_a_base64_encoded_public_key".getBytes())));
assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
@@ -169,128 +183,447 @@
@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);
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_INSUFFICIENT_SPACE));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_HTTP_DATA_ERROR));
assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
}
@Test
+ public void testDownloader_publicKeyDownloadFail_failureThresholdExceeded_logsFailure()
+ throws Exception {
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+ Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_INSUFFICIENT_SPACE));
+
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, times(1))
+ .logCTLogListUpdateFailedEventWithDownloadStatus(
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ }
+
+ @Test
+ public void testDownloader_publicKeyDownloadFail_failureThresholdNotMet_doesNotLog()
+ throws Exception {
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
+ // Set the failure count to well below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makePublicKeyDownloadFailedIntent(DownloadManager.ERROR_HTTP_DATA_ERROR));
+
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(1);
+ verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
+ verify(mLogger, never()).logCTLogListUpdateFailedEventWithDownloadStatus(
+ anyInt(), anyInt());
+ }
+
+ @Test
public void testDownloader_metadataDownloadSuccess_startContentDownload() {
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setSuccessfulDownload(metadataId, new File("log_list.sig"));
+ mCertificateTransparencyDownloader.startMetadataDownload();
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(metadataId));
+ mContext,
+ makeMetadataDownloadCompleteIntent(mCompatVersion, new File("log_list.sig")));
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isTrue();
}
@Test
public void testDownloader_metadataDownloadFail_doNotStartContentDownload() {
- 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);
+ mCertificateTransparencyDownloader.startMetadataDownload();
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeMetadataDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeMetadataDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_HTTP_DATA_ERROR));
assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
}
@Test
- public void testDownloader_contentDownloadSuccess_installSuccess_updateDataStore()
+ public void testDownloader_metadataDownloadFail_failureThresholdExceeded_logsFailure()
throws Exception {
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+ Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeMetadataDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, times(1))
+ .logCTLogListUpdateFailedEventWithDownloadStatus(
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ }
+
+ @Test
+ public void testDownloader_metadataDownloadFail_failureThresholdNotMet_doesNotLog()
+ throws Exception {
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ // Set the failure count to well below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeMetadataDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(1);
+ verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
+ verify(mLogger, never()).logCTLogListUpdateFailedEventWithDownloadStatus(
+ anyInt(), anyInt());
+ }
+
+ @Test
+ public void testDownloader_contentDownloadSuccess_installSuccess() throws Exception {
String newVersion = "456";
File logListFile = makeLogListFile(newVersion);
File metadataFile = sign(logListFile);
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(), anyString()))
- .thenReturn(true);
+ mCertificateTransparencyDownloader.startMetadataDownload();
assertNoVersionIsInstalled();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
assertInstallSuccessful(newVersion);
}
@Test
public void testDownloader_contentDownloadFail_doNotInstall() throws Exception {
- 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);
+ mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
- mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeContentDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeContentDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_HTTP_DATA_ERROR));
- verify(mCertificateTransparencyInstaller, never()).install(any(), any(), any());
assertNoVersionIsInstalled();
}
@Test
- public void testDownloader_contentDownloadSuccess_installFail_doNotUpdateDataStore()
+ public void testDownloader_contentDownloadFail_failureThresholdExceeded_logsFailure()
throws Exception {
- File logListFile = makeLogListFile("456");
- File metadataFile = sign(logListFile);
+ mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+ Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeContentDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_INSUFFICIENT_SPACE));
+
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, times(1))
+ .logCTLogListUpdateFailedEventWithDownloadStatus(
+ DownloadManager.ERROR_INSUFFICIENT_SPACE,
+ Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ }
+
+ @Test
+ public void testDownloader_contentDownloadFail_failureThresholdNotMet_doesNotLog()
+ throws Exception {
+ mCertificateTransparencyDownloader.startContentDownload(mCompatVersion);
+ // Set the failure count to well below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext,
+ makeContentDownloadFailedIntent(
+ mCompatVersion, DownloadManager.ERROR_HTTP_DATA_ERROR));
+
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(1);
+ verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
+ verify(mLogger, never()).logCTLogListUpdateFailedEventWithDownloadStatus(
+ anyInt(), anyInt());
+ }
+
+ @Test
+ public void testDownloader_contentDownloadSuccess_invalidLogList_installFails()
+ throws Exception {
+ File invalidLogListFile = writeToFile("not_a_json_log_list".getBytes());
+ File metadataFile = sign(invalidLogListFile);
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(), anyString()))
- .thenReturn(false);
+ mCertificateTransparencyDownloader.startMetadataDownload();
assertNoVersionIsInstalled();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
assertNoVersionIsInstalled();
}
@Test
+ public void
+ testDownloader_contentDownloadSuccess_noSignatureFound_failureThresholdExceeded_logsSingleFailure()
+ throws Exception {
+ File logListFile = makeLogListFile("456");
+ File metadataFile = sign(logListFile);
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+ Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+
+ // Set the public key to be missing
+ mSignatureVerifier.resetPublicKey();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, times(1))
+ .logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_SIGNATURE_NOT_FOUND,
+ Config.DEFAULT_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());
+
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+ Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+
+ // Act
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+ // Assert
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.DEFAULT_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.DEFAULT_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());
+
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+ Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+
+ // Act
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+ // Assert
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.DEFAULT_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.DEFAULT_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 key to be deliberately wrong by using diff key pair
+ KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
+ mSignatureVerifier.setPublicKey(instance.generateKeyPair().getPublic());
+ // Set the failure count to well below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
+
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+
+ 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 invalidLogListFile = writeToFile("not_a_json_log_list".getBytes());
+ File metadataFile = sign(invalidLogListFile);
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ // Set the failure count to just below the threshold
+ mDataStore.setPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT,
+ Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD - 1);
+
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
+
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ verify(mLogger, times(1))
+ .logCTLogListUpdateFailedEvent(
+ CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_FAILED__FAILURE_REASON__FAILURE_VERSION_ALREADY_EXISTS,
+ Config.DEFAULT_LOG_LIST_UPDATE_FAILURE_THRESHOLD);
+ }
+
+ @Test
+ public void
+ testDownloader_contentDownloadSuccess_installFail_failureThresholdNotMet_doesNotLog()
+ throws Exception {
+ File invalidLogListFile = writeToFile("not_a_json_log_list".getBytes());
+ File metadataFile = sign(invalidLogListFile);
+ mSignatureVerifier.setPublicKey(mPublicKey);
+ // Set the failure count to well below the threshold
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, 0);
+
+ mCertificateTransparencyDownloader.startMetadataDownload();
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
+
+ assertThat(
+ mDataStore.getPropertyInt(
+ Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* defaultValue= */ 0))
+ .isEqualTo(1);
+ verify(mLogger, never()).logCTLogListUpdateFailedEvent(anyInt(), anyInt());
+ verify(mLogger, never()).logCTLogListUpdateFailedEventWithDownloadStatus(
+ 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.startMetadataDownload();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
- verify(mCertificateTransparencyInstaller, never())
- .install(eq(Config.COMPATIBILITY_VERSION), any(), anyString());
assertNoVersionIsInstalled();
}
@@ -300,17 +633,14 @@
File logListFile = makeLogListFile("456");
File metadataFile = sign(logListFile);
mSignatureVerifier.resetPublicKey();
- long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
- setSuccessfulDownload(metadataId, metadataFile);
- long contentId = mCertificateTransparencyDownloader.startContentDownload();
- setSuccessfulDownload(contentId, logListFile);
assertNoVersionIsInstalled();
+ mCertificateTransparencyDownloader.startMetadataDownload();
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
- verify(mCertificateTransparencyInstaller, never())
- .install(eq(Config.COMPATIBILITY_VERSION), any(), anyString());
assertNoVersionIsInstalled();
}
@@ -324,52 +654,37 @@
assertNoVersionIsInstalled();
// 1. Start download of public key.
- long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+ mCertificateTransparencyDownloader.startPublicKeyDownload();
- // 2. On successful public key download, set the key and start the metatadata download.
- setSuccessfulDownload(publicKeyId, publicKeyFile);
-
+ // 2. On successful public key download, set the key and start the metatadata
+ // download.
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(publicKeyId));
+ mContext, makePublicKeyDownloadCompleteIntent(publicKeyFile));
// 3. On successful metadata download, start the content download.
- long metadataId = mCertificateTransparencyDownloader.getMetadataDownloadId();
- setSuccessfulDownload(metadataId, metadataFile);
-
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(metadataId));
+ mContext, makeMetadataDownloadCompleteIntent(mCompatVersion, metadataFile));
- // 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);
-
+ // 4. On successful content download, verify the signature and install the new
+ // version.
mCertificateTransparencyDownloader.onReceive(
- mContext, makeDownloadCompleteIntent(contentId));
+ mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
assertInstallSuccessful(newVersion);
}
private void assertNoVersionIsInstalled() {
- assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
+ assertThat(mCompatVersion.getVersionDir().exists()).isFalse();
}
private void assertInstallSuccessful(String version) {
- assertThat(mDataStore.getProperty(Config.VERSION)).isEqualTo(version);
- }
-
- private Intent makeDownloadCompleteIntent(long downloadId) {
- return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
- .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
- }
-
- 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);
+ File logsDir =
+ new File(
+ mCompatVersion.getVersionDir(),
+ CompatibilityVersion.LOGS_DIR_PREFIX + version);
+ assertThat(logsDir.exists()).isTrue();
+ File logsFile = new File(logsDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
+ assertThat(logsFile.exists()).isTrue();
}
private void prepareDownloadManager() {
@@ -377,6 +692,32 @@
.thenAnswer(invocation -> mNextDownloadId++);
}
+ private Intent makePublicKeyDownloadCompleteIntent(File publicKeyfile) {
+ return makeDownloadCompleteIntent(
+ mCertificateTransparencyDownloader.getPublicKeyDownloadId(), publicKeyfile);
+ }
+
+ private Intent makeMetadataDownloadCompleteIntent(
+ CompatibilityVersion compatVersion, File signatureFile) {
+ return makeDownloadCompleteIntent(
+ mCertificateTransparencyDownloader.getMetadataDownloadId(compatVersion),
+ signatureFile);
+ }
+
+ private Intent makeContentDownloadCompleteIntent(
+ CompatibilityVersion compatVersion, File logListFile) {
+ return makeDownloadCompleteIntent(
+ mCertificateTransparencyDownloader.getContentDownloadId(compatVersion),
+ logListFile);
+ }
+
+ private Intent makeDownloadCompleteIntent(long downloadId, File file) {
+ when(mDownloadManager.query(any(Query.class))).thenReturn(makeSuccessfulDownloadCursor());
+ when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(Uri.fromFile(file));
+ return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
+ .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
+ }
+
private Cursor makeSuccessfulDownloadCursor() {
MatrixCursor cursor =
new MatrixCursor(
@@ -387,9 +728,26 @@
return cursor;
}
- private void setSuccessfulDownload(long downloadId, File file) {
- when(mDownloadManager.query(any(Query.class))).thenReturn(makeSuccessfulDownloadCursor());
- when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(Uri.fromFile(file));
+ private Intent makePublicKeyDownloadFailedIntent(int error) {
+ return makeDownloadFailedIntent(
+ mCertificateTransparencyDownloader.getPublicKeyDownloadId(), error);
+ }
+
+ private Intent makeMetadataDownloadFailedIntent(CompatibilityVersion compatVersion, int error) {
+ return makeDownloadFailedIntent(
+ mCertificateTransparencyDownloader.getMetadataDownloadId(compatVersion), error);
+ }
+
+ private Intent makeContentDownloadFailedIntent(CompatibilityVersion compatVersion, int error) {
+ return makeDownloadFailedIntent(
+ mCertificateTransparencyDownloader.getContentDownloadId(compatVersion), error);
+ }
+
+ private Intent makeDownloadFailedIntent(long downloadId, int error) {
+ when(mDownloadManager.query(any(Query.class))).thenReturn(makeFailedDownloadCursor(error));
+ when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(null);
+ return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
+ .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
}
private Cursor makeFailedDownloadCursor(int error) {
@@ -402,16 +760,6 @@
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()));
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
deleted file mode 100644
index 50d3f23..0000000
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
+++ /dev/null
@@ -1,183 +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 static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/** Tests for the {@link CertificateTransparencyInstaller}. */
-@RunWith(JUnit4.class)
-public class CertificateTransparencyInstallerTest {
-
- private static final String TEST_VERSION = "test-v1";
-
- private File mTestDir =
- new File(
- InstrumentationRegistry.getInstrumentation().getContext().getFilesDir(),
- "test-dir");
- private CertificateTransparencyInstaller mCertificateTransparencyInstaller =
- new CertificateTransparencyInstaller(mTestDir);
-
- @Before
- public void setUp() {
- mCertificateTransparencyInstaller.addCompatibilityVersion(TEST_VERSION);
- }
-
- @After
- public void tearDown() {
- mCertificateTransparencyInstaller.removeCompatibilityVersion(TEST_VERSION);
- DirectoryUtils.removeDir(mTestDir);
- }
-
- @Test
- public void testCompatibilityVersion_installSuccessful() throws IOException {
- assertThat(mTestDir.mkdir()).isTrue();
- String content = "i_am_compatible";
- String version = "i_am_version";
- CompatibilityVersion compatVersion =
- mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
-
- try (InputStream inputStream = asStream(content)) {
- assertThat(compatVersion.install(inputStream, version)).isTrue();
- }
- File logsDir = compatVersion.getLogsDir();
- assertThat(logsDir.exists()).isTrue();
- assertThat(logsDir.isDirectory()).isTrue();
- assertThat(logsDir.getAbsolutePath())
- .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION);
- File logsListFile = compatVersion.getLogsFile();
- assertThat(logsListFile.exists()).isTrue();
- assertThat(logsListFile.getAbsolutePath()).startsWith(logsDir.getAbsolutePath());
- assertThat(readAsString(logsListFile)).isEqualTo(content);
- File logsSymlink = compatVersion.getLogsDirSymlink();
- assertThat(logsSymlink.exists()).isTrue();
- assertThat(logsSymlink.isDirectory()).isTrue();
- assertThat(logsSymlink.getAbsolutePath())
- .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION + "/current");
- assertThat(logsSymlink.getCanonicalPath()).isEqualTo(logsDir.getCanonicalPath());
-
- assertThat(compatVersion.delete()).isTrue();
- assertThat(logsDir.exists()).isFalse();
- assertThat(logsSymlink.exists()).isFalse();
- assertThat(logsListFile.exists()).isFalse();
- }
-
- @Test
- public void testCompatibilityVersion_versionInstalledFailed() throws IOException {
- assertThat(mTestDir.mkdir()).isTrue();
-
- CompatibilityVersion compatVersion =
- mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
- File rootDir = compatVersion.getRootDir();
- assertThat(rootDir.mkdir()).isTrue();
-
- String existingVersion = "666";
- File existingLogDir =
- new File(rootDir, CompatibilityVersion.LOGS_DIR_PREFIX + existingVersion);
- assertThat(existingLogDir.mkdir()).isTrue();
-
- String existingContent = "somebody_tried_to_install_me_but_failed_halfway_through";
- File logsListFile = new File(existingLogDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
- assertThat(logsListFile.createNewFile()).isTrue();
- writeToFile(logsListFile, existingContent);
-
- String newContent = "i_am_the_real_content";
- try (InputStream inputStream = asStream(newContent)) {
- assertThat(compatVersion.install(inputStream, existingVersion)).isTrue();
- }
-
- assertThat(readAsString(logsListFile)).isEqualTo(newContent);
- }
-
- @Test
- public void testCertificateTransparencyInstaller_installSuccessfully() throws IOException {
- String content = "i_am_a_certificate_and_i_am_transparent";
- String version = "666";
-
- try (InputStream inputStream = asStream(content)) {
- assertThat(
- mCertificateTransparencyInstaller.install(
- TEST_VERSION, inputStream, version))
- .isTrue();
- }
-
- assertThat(mTestDir.exists()).isTrue();
- assertThat(mTestDir.isDirectory()).isTrue();
- CompatibilityVersion compatVersion =
- mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
- File logsDir = compatVersion.getLogsDir();
- assertThat(logsDir.exists()).isTrue();
- assertThat(logsDir.isDirectory()).isTrue();
- assertThat(logsDir.getAbsolutePath())
- .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION);
- File logsListFile = compatVersion.getLogsFile();
- assertThat(logsListFile.exists()).isTrue();
- assertThat(logsListFile.getAbsolutePath()).startsWith(logsDir.getAbsolutePath());
- assertThat(readAsString(logsListFile)).isEqualTo(content);
- }
-
- @Test
- public void testCertificateTransparencyInstaller_versionIsAlreadyInstalled()
- throws IOException {
- String existingVersion = "666";
- String existingContent = "i_was_already_installed_successfully";
- CompatibilityVersion compatVersion =
- mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
-
- DirectoryUtils.makeDir(mTestDir);
- try (InputStream inputStream = asStream(existingContent)) {
- assertThat(compatVersion.install(inputStream, existingVersion)).isTrue();
- }
-
- try (InputStream inputStream = asStream("i_will_be_ignored")) {
- assertThat(
- mCertificateTransparencyInstaller.install(
- TEST_VERSION, inputStream, existingVersion))
- .isFalse();
- }
-
- assertThat(readAsString(compatVersion.getLogsFile())).isEqualTo(existingContent);
- }
-
- private static InputStream asStream(String string) throws IOException {
- return new ByteArrayInputStream(string.getBytes());
- }
-
- private static String readAsString(File file) throws IOException {
- return new String(new FileInputStream(file).readAllBytes());
- }
-
- private static void writeToFile(File file, String string) throws IOException {
- try (OutputStream out = new FileOutputStream(file)) {
- out.write(string.getBytes());
- }
- }
-}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
new file mode 100644
index 0000000..38fff48
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.google.common.truth.Truth.assertThat;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** Tests for the {@link CompatibilityVersion}. */
+@RunWith(JUnit4.class)
+public class CompatibilityVersionTest {
+
+ private static final String TEST_VERSION = "v123";
+
+ private final File mTestDir =
+ InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
+ private final CompatibilityVersion mCompatVersion =
+ new CompatibilityVersion(
+ TEST_VERSION, Config.URL_SIGNATURE, Config.URL_LOG_LIST, mTestDir);
+
+ @After
+ public void tearDown() {
+ mCompatVersion.delete();
+ }
+
+ @Test
+ public void testCompatibilityVersion_versionDirectory_setupSuccessful() {
+ File versionDir = mCompatVersion.getVersionDir();
+ assertThat(versionDir.exists()).isFalse();
+ assertThat(versionDir.getAbsolutePath()).startsWith(mTestDir.getAbsolutePath());
+ assertThat(versionDir.getAbsolutePath()).endsWith(TEST_VERSION);
+ }
+
+ @Test
+ public void testCompatibilityVersion_symlink_setupSuccessful() {
+ File dirSymlink = mCompatVersion.getLogsDirSymlink();
+ assertThat(dirSymlink.exists()).isFalse();
+ assertThat(dirSymlink.getAbsolutePath())
+ .startsWith(mCompatVersion.getVersionDir().getAbsolutePath());
+ }
+
+ @Test
+ public void testCompatibilityVersion_logsFile_setupSuccessful() {
+ File logsFile = mCompatVersion.getLogsFile();
+ assertThat(logsFile.exists()).isFalse();
+ assertThat(logsFile.getAbsolutePath())
+ .startsWith(mCompatVersion.getLogsDirSymlink().getAbsolutePath());
+ }
+
+ @Test
+ public void testCompatibilityVersion_installSuccessful() throws Exception {
+ String version = "i_am_version";
+ JSONObject logList = makeLogList(version, "i_am_content");
+
+ try (InputStream inputStream = asStream(logList)) {
+ assertThat(mCompatVersion.install(inputStream)).isTrue();
+ }
+
+ File logListFile = mCompatVersion.getLogsFile();
+ assertThat(logListFile.exists()).isTrue();
+ assertThat(logListFile.getCanonicalPath())
+ .isEqualTo(
+ // <path-to-test-files>/v123/logs-i_am_version/log_list.json
+ new File(
+ new File(
+ mCompatVersion.getVersionDir(),
+ CompatibilityVersion.LOGS_DIR_PREFIX + version),
+ CompatibilityVersion.LOGS_LIST_FILE_NAME)
+ .getCanonicalPath());
+ assertThat(logListFile.getAbsolutePath())
+ .isEqualTo(
+ // <path-to-test-files>/v123/current/log_list.json
+ new File(
+ new File(
+ mCompatVersion.getVersionDir(),
+ CompatibilityVersion.CURRENT_LOGS_DIR_SYMLINK_NAME),
+ CompatibilityVersion.LOGS_LIST_FILE_NAME)
+ .getAbsolutePath());
+ }
+
+ @Test
+ public void testCompatibilityVersion_deleteSuccessfully() throws Exception {
+ try (InputStream inputStream = asStream(makeLogList(/* version= */ "123"))) {
+ assertThat(mCompatVersion.install(inputStream)).isTrue();
+ }
+
+ mCompatVersion.delete();
+
+ assertThat(mCompatVersion.getLogsFile().exists()).isFalse();
+ }
+
+ @Test
+ public void testCompatibilityVersion_invalidLogList() throws Exception {
+ try (InputStream inputStream = new ByteArrayInputStream(("not_a_valid_list".getBytes()))) {
+ assertThat(mCompatVersion.install(inputStream)).isFalse();
+ }
+
+ assertThat(mCompatVersion.getLogsFile().exists()).isFalse();
+ }
+
+ @Test
+ public void testCompatibilityVersion_incompleteVersionExists_replacesOldVersion()
+ throws Exception {
+ String existingVersion = "666";
+ File existingLogDir =
+ new File(
+ mCompatVersion.getVersionDir(),
+ CompatibilityVersion.LOGS_DIR_PREFIX + existingVersion);
+ assertThat(existingLogDir.mkdirs()).isTrue();
+ File logsListFile = new File(existingLogDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
+ assertThat(logsListFile.createNewFile()).isTrue();
+
+ JSONObject newLogList = makeLogList(existingVersion, "i_am_the_real_content");
+ try (InputStream inputStream = asStream(newLogList)) {
+ assertThat(mCompatVersion.install(inputStream)).isTrue();
+ }
+
+ assertThat(readAsString(logsListFile)).isEqualTo(newLogList.toString());
+ }
+
+ @Test
+ public void testCompatibilityVersion_versionAlreadyExists_installFails() throws Exception {
+ String existingVersion = "666";
+ JSONObject existingLogList = makeLogList(existingVersion, "i_was_installed_successfully");
+ try (InputStream inputStream = asStream(existingLogList)) {
+ assertThat(mCompatVersion.install(inputStream)).isTrue();
+ }
+
+ try (InputStream inputStream = asStream(makeLogList(existingVersion, "i_am_ignored"))) {
+ assertThat(mCompatVersion.install(inputStream)).isFalse();
+ }
+
+ assertThat(readAsString(mCompatVersion.getLogsFile()))
+ .isEqualTo(existingLogList.toString());
+ }
+
+ private static InputStream asStream(JSONObject logList) throws IOException {
+ return new ByteArrayInputStream(logList.toString().getBytes());
+ }
+
+ private static JSONObject makeLogList(String version) throws JSONException {
+ return new JSONObject().put("version", version);
+ }
+
+ private static JSONObject makeLogList(String version, String content) throws JSONException {
+ return makeLogList(version).put("content", content);
+ }
+
+ private static String readAsString(File file) throws IOException {
+ try (InputStream in = new FileInputStream(file)) {
+ return new String(in.readAllBytes());
+ }
+ }
+}
diff --git a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
index 9add6df..1d43d38 100644
--- a/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
+++ b/remoteauth/service/jni/src/remoteauth_jni_android_platform.rs
@@ -140,6 +140,7 @@
}
impl Platform for JavaPlatform {
+ #[allow(clippy::unit_arg)]
fn send_request(
&mut self,
connection_id: i32,
diff --git a/remoteauth/service/jni/src/unique_jvm.rs b/remoteauth/service/jni/src/unique_jvm.rs
index 46cc361..ddbb16f 100644
--- a/remoteauth/service/jni/src/unique_jvm.rs
+++ b/remoteauth/service/jni/src/unique_jvm.rs
@@ -41,6 +41,7 @@
Ok(())
}
/// Gets a 'static reference to the unique JavaVM. Returns None if set_once() was never called.
+#[allow(static_mut_refs)]
pub(crate) fn get_static_ref() -> Option<&'static Arc<JavaVM>> {
// Safety: follows [this pattern](https://doc.rust-lang.org/std/sync/struct.Once.html).
// Modification to static mut is nested inside call_once.
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/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 c4a9110..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.
@@ -89,11 +95,6 @@
"nsd_cached_services_retention_time";
public static final int DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS = 10000;
- /**
- * A feature flag to control whether the accurate delay callback should be enabled.
- */
- public static final String NSD_ACCURATE_DELAY_CALLBACK = "nsd_accurate_delay_callback";
-
// Flag for offload feature
public final boolean mIsMdnsOffloadFeatureEnabled;
@@ -127,8 +128,8 @@
// Retention Time for cached services
public final long mCachedServicesRetentionTime;
- // Flag for accurate delay callback
- public final boolean mIsAccurateDelayCallbackEnabled;
+ // Flag to use shorter (16 characters + .local) hostnames
+ public final boolean mIsShortHostnamesEnabled;
@Nullable
private final FlagOverrideProvider mOverrideProvider;
@@ -225,12 +226,8 @@
NSD_CACHED_SERVICES_RETENTION_TIME, (int) mCachedServicesRetentionTime);
}
- /**
- * Indicates whether {@link #NSD_ACCURATE_DELAY_CALLBACK} is enabled, including for testing.
- */
- public boolean isAccurateDelayCallbackEnabled() {
- return mIsAccurateDelayCallbackEnabled
- || isForceEnabledForTest(NSD_ACCURATE_DELAY_CALLBACK);
+ public boolean isShortHostnamesEnabled() {
+ return mIsShortHostnamesEnabled || isForceEnabledForTest(NSD_USE_SHORT_HOSTNAMES);
}
/**
@@ -247,7 +244,7 @@
boolean avoidAdvertisingEmptyTxtRecords,
boolean isCachedServicesRemovalEnabled,
long cachedServicesRetentionTime,
- boolean isAccurateDelayCallbackEnabled,
+ boolean isShortHostnamesEnabled,
@Nullable FlagOverrideProvider overrideProvider) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
@@ -260,7 +257,7 @@
mAvoidAdvertisingEmptyTxtRecords = avoidAdvertisingEmptyTxtRecords;
mIsCachedServicesRemovalEnabled = isCachedServicesRemovalEnabled;
mCachedServicesRetentionTime = cachedServicesRetentionTime;
- mIsAccurateDelayCallbackEnabled = isAccurateDelayCallbackEnabled;
+ mIsShortHostnamesEnabled = isShortHostnamesEnabled;
mOverrideProvider = overrideProvider;
}
@@ -284,7 +281,7 @@
private boolean mAvoidAdvertisingEmptyTxtRecords;
private boolean mIsCachedServicesRemovalEnabled;
private long mCachedServicesRetentionTime;
- private boolean mIsAccurateDelayCallbackEnabled;
+ private boolean mIsShortHostnamesEnabled;
private FlagOverrideProvider mOverrideProvider;
/**
@@ -302,7 +299,7 @@
mAvoidAdvertisingEmptyTxtRecords = true; // Default enabled.
mIsCachedServicesRemovalEnabled = false;
mCachedServicesRetentionTime = DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS;
- mIsAccurateDelayCallbackEnabled = false;
+ mIsShortHostnamesEnabled = true; // Default enabled.
mOverrideProvider = null;
}
@@ -429,12 +426,12 @@
}
/**
- * Set whether the accurate delay callback is enabled.
+ * Set whether the short hostnames feature is enabled.
*
- * @see #NSD_ACCURATE_DELAY_CALLBACK
+ * @see #NSD_USE_SHORT_HOSTNAMES
*/
- public Builder setIsAccurateDelayCallbackEnabled(boolean isAccurateDelayCallbackEnabled) {
- mIsAccurateDelayCallbackEnabled = isAccurateDelayCallbackEnabled;
+ public Builder setIsShortHostnamesEnabled(boolean isShortHostnamesEnabled) {
+ mIsShortHostnamesEnabled = isShortHostnamesEnabled;
return this;
}
@@ -453,7 +450,7 @@
mAvoidAdvertisingEmptyTxtRecords,
mIsCachedServicesRemovalEnabled,
mCachedServicesRetentionTime,
- mIsAccurateDelayCallbackEnabled,
+ 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/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 7a93fec..8c86fb8 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -20,7 +20,6 @@
import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
import static com.android.server.connectivity.mdns.MdnsServiceCache.ServiceExpiredCallback;
import static com.android.server.connectivity.mdns.MdnsServiceCache.findMatchedResponse;
-import static com.android.server.connectivity.mdns.MdnsQueryScheduler.ScheduledQueryTaskArgs;
import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
import static com.android.server.connectivity.mdns.util.MdnsUtils.buildMdnsServiceInfoFromResponse;
@@ -38,7 +37,6 @@
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.DnsUtils;
import com.android.net.module.util.SharedLog;
-import com.android.net.module.util.TimerFileDescriptor;
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
@@ -96,9 +94,6 @@
private final boolean removeServiceAfterTtlExpires =
MdnsConfigs.removeServiceAfterTtlExpires();
private final Clock clock;
- // Use TimerFileDescriptor for query scheduling, which allows for more accurate sending of
- // queries.
- @NonNull private final TimerFileDescriptor timerFd;
@Nullable private MdnsSearchOptions searchOptions;
@@ -144,7 +139,8 @@
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_START_QUERYTASK: {
- final ScheduledQueryTaskArgs taskArgs = (ScheduledQueryTaskArgs) msg.obj;
+ final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs =
+ (MdnsQueryScheduler.ScheduledQueryTaskArgs) msg.obj;
// QueryTask should be run immediately after being created (not be scheduled in
// advance). Because the result of "makeResponsesForResolve" depends on answers
// that were received before it is called, so to take into account all answers
@@ -178,7 +174,7 @@
final long now = clock.elapsedRealtime();
lastSentTime = now;
final long minRemainingTtl = getMinRemainingTtl(now);
- final ScheduledQueryTaskArgs args =
+ MdnsQueryScheduler.ScheduledQueryTaskArgs args =
mdnsQueryScheduler.scheduleNextRun(
sentResult.taskArgs.config,
minRemainingTtl,
@@ -193,14 +189,10 @@
sharedLog.log(String.format("Query sent with transactionId: %d. "
+ "Next run: sessionId: %d, in %d ms",
sentResult.transactionId, args.sessionId, timeToNextTaskMs));
- if (featureFlags.isAccurateDelayCallbackEnabled()) {
- setDelayedTask(args, timeToNextTaskMs);
- } else {
- dependencies.sendMessageDelayed(
- handler,
- handler.obtainMessage(EVENT_START_QUERYTASK, args),
- timeToNextTaskMs);
- }
+ dependencies.sendMessageDelayed(
+ handler,
+ handler.obtainMessage(EVENT_START_QUERYTASK, args),
+ timeToNextTaskMs);
break;
}
default:
@@ -262,14 +254,6 @@
return List.of(new DatagramPacket(queryBuffer, 0, queryBuffer.length, address));
}
}
-
- /**
- * @see TimerFileDescriptor
- */
- @Nullable
- public TimerFileDescriptor createTimerFd(@NonNull Handler handler) {
- return new TimerFileDescriptor(handler);
- }
}
/**
@@ -317,7 +301,6 @@
this.mdnsQueryScheduler = new MdnsQueryScheduler();
this.cacheKey = new MdnsServiceCache.CacheKey(serviceType, socketKey);
this.featureFlags = featureFlags;
- this.timerFd = dependencies.createTimerFd(handler);
}
/**
@@ -334,13 +317,6 @@
? serviceCache.getCachedServices(cacheKey) : Collections.emptyList();
}
- private void setDelayedTask(ScheduledQueryTaskArgs args, long timeToNextTaskMs) {
- timerFd.cancelTask();
- timerFd.setDelayedTask(new TimerFileDescriptor.MessageTask(
- handler.obtainMessage(EVENT_START_QUERYTASK, args)),
- timeToNextTaskMs);
- }
-
/**
* Registers {@code listener} for receiving discovery event of mDNS service instances, and
* starts
@@ -387,7 +363,7 @@
}
final long minRemainingTtl = getMinRemainingTtl(now);
if (hadReply) {
- final ScheduledQueryTaskArgs args =
+ MdnsQueryScheduler.ScheduledQueryTaskArgs args =
mdnsQueryScheduler.scheduleNextRun(
taskConfig,
minRemainingTtl,
@@ -401,14 +377,10 @@
final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
sharedLog.log(String.format("Schedule a query. Next run: sessionId: %d, in %d ms",
args.sessionId, timeToNextTaskMs));
- if (featureFlags.isAccurateDelayCallbackEnabled()) {
- setDelayedTask(args, timeToNextTaskMs);
- } else {
- dependencies.sendMessageDelayed(
- handler,
- handler.obtainMessage(EVENT_START_QUERYTASK, args),
- timeToNextTaskMs);
- }
+ dependencies.sendMessageDelayed(
+ handler,
+ handler.obtainMessage(EVENT_START_QUERYTASK, args),
+ timeToNextTaskMs);
} else {
final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
final QueryTask queryTask = new QueryTask(
@@ -448,11 +420,7 @@
}
private void removeScheduledTask() {
- if (featureFlags.isAccurateDelayCallbackEnabled()) {
- timerFd.cancelTask();
- } else {
- dependencies.removeMessages(handler, EVENT_START_QUERYTASK);
- }
+ dependencies.removeMessages(handler, EVENT_START_QUERYTASK);
sharedLog.log("Remove EVENT_START_QUERYTASK"
+ ", current session: " + currentSessionId);
++currentSessionId;
@@ -538,13 +506,10 @@
}
}
}
- final boolean hasScheduledTask = featureFlags.isAccurateDelayCallbackEnabled()
- ? timerFd.hasDelayedTask()
- : dependencies.hasMessages(handler, EVENT_START_QUERYTASK);
- if (hasScheduledTask) {
+ if (dependencies.hasMessages(handler, EVENT_START_QUERYTASK)) {
final long now = clock.elapsedRealtime();
final long minRemainingTtl = getMinRemainingTtl(now);
- final ScheduledQueryTaskArgs args =
+ MdnsQueryScheduler.ScheduledQueryTaskArgs args =
mdnsQueryScheduler.maybeRescheduleCurrentRun(now, minRemainingTtl,
lastSentTime, currentSessionId + 1,
searchOptions.numOfQueriesBeforeBackoff());
@@ -553,14 +518,10 @@
final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
sharedLog.log(String.format("Reschedule a query. Next run: sessionId: %d, in %d ms",
args.sessionId, timeToNextTaskMs));
- if (featureFlags.isAccurateDelayCallbackEnabled()) {
- setDelayedTask(args, timeToNextTaskMs);
- } else {
- dependencies.sendMessageDelayed(
- handler,
- handler.obtainMessage(EVENT_START_QUERYTASK, args),
- timeToNextTaskMs);
- }
+ dependencies.sendMessageDelayed(
+ handler,
+ handler.obtainMessage(EVENT_START_QUERYTASK, args),
+ timeToNextTaskMs);
}
}
}
@@ -725,10 +686,10 @@
private static class QuerySentArguments {
private final int transactionId;
private final List<String> subTypes = new ArrayList<>();
- private final ScheduledQueryTaskArgs taskArgs;
+ private final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs;
QuerySentArguments(int transactionId, @NonNull List<String> subTypes,
- @NonNull ScheduledQueryTaskArgs taskArgs) {
+ @NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs) {
this.transactionId = transactionId;
this.subTypes.addAll(subTypes);
this.taskArgs = taskArgs;
@@ -737,14 +698,14 @@
// A FutureTask that enqueues a single query, and schedule a new FutureTask for the next task.
private class QueryTask implements Runnable {
- private final ScheduledQueryTaskArgs taskArgs;
+ private final MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs;
private final List<MdnsResponse> servicesToResolve = new ArrayList<>();
private final List<String> subtypes = new ArrayList<>();
private final boolean sendDiscoveryQueries;
private final List<MdnsResponse> existingServices = new ArrayList<>();
private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
private final SocketKey socketKey;
- QueryTask(@NonNull ScheduledQueryTaskArgs taskArgs,
+ QueryTask(@NonNull MdnsQueryScheduler.ScheduledQueryTaskArgs taskArgs,
@NonNull Collection<MdnsResponse> servicesToResolve,
@NonNull Collection<String> subtypes, boolean sendDiscoveryQueries,
@NonNull Collection<MdnsResponse> existingServices,
@@ -769,7 +730,7 @@
serviceType,
subtypes,
taskArgs.config.expectUnicastResponse,
- taskArgs.config.transactionId,
+ taskArgs.config.getTransactionId(),
socketKey,
onlyUseIpv6OnIpv6OnlyNetworks,
sendDiscoveryQueries,
@@ -810,7 +771,7 @@
return minRemainingTtl == Long.MAX_VALUE ? 0 : minRemainingTtl;
}
- private static long calculateTimeToNextTask(ScheduledQueryTaskArgs args,
+ private static long calculateTimeToNextTask(MdnsQueryScheduler.ScheduledQueryTaskArgs args,
long now) {
return Math.max(args.timeToRun - now, 0);
}
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/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index adfb694..eedf427 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;
@@ -50,11 +51,11 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.NetdUtils;
-import com.android.net.module.util.PermissionUtils;
import com.android.net.module.util.SharedLog;
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;
@@ -592,18 +593,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);
- // Only bring the interface up when ethernet is enabled.
- if (mIsEthernetEnabled) {
- // 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);
- } else {
- NetdUtils.setInterfaceDown(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.
@@ -629,7 +623,7 @@
nc = mNetworkCapabilities.get(hwAddress);
if (nc == null) {
final boolean isTestIface = iface.matches(TEST_IFACE_REGEXP);
- nc = createDefaultNetworkCapabilities(isTestIface);
+ nc = createDefaultNetworkCapabilities(isTestIface, /* overrideTransport */ null);
}
}
@@ -663,15 +657,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);
}
@@ -752,9 +738,13 @@
*/
private void parseEthernetConfig(String configString) {
final EthernetTrackerConfig config = createEthernetTrackerConfig(configString);
- NetworkCapabilities nc = createNetworkCapabilities(
- !TextUtils.isEmpty(config.mCapabilities) /* clear default capabilities */,
- config.mCapabilities, config.mTransport).build();
+ NetworkCapabilities nc;
+ if (TextUtils.isEmpty(config.mCapabilities)) {
+ boolean isTestIface = config.mIface.matches(TEST_IFACE_REGEXP);
+ nc = createDefaultNetworkCapabilities(isTestIface, config.mTransport);
+ } else {
+ nc = createNetworkCapabilities(config.mCapabilities, config.mTransport).build();
+ }
mNetworkCapabilities.put(config.mIface, nc);
if (null != config.mIpConfig) {
@@ -769,15 +759,16 @@
return new EthernetTrackerConfig(configString.split(";", /* limit of tokens */ 4));
}
- private static NetworkCapabilities createDefaultNetworkCapabilities(boolean isTestIface) {
- NetworkCapabilities.Builder builder = createNetworkCapabilities(
- false /* clear default capabilities */, null, null)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+ private static NetworkCapabilities createDefaultNetworkCapabilities(
+ boolean isTestIface, @Nullable String overrideTransport) {
+ NetworkCapabilities.Builder builder =
+ createNetworkCapabilities(/* commaSeparatedCapabilities */ null, overrideTransport)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
if (isTestIface) {
builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST);
@@ -791,7 +782,6 @@
/**
* Parses a static list of network capabilities
*
- * @param clearDefaultCapabilities Indicates whether or not to clear any default capabilities
* @param commaSeparatedCapabilities A comma separated string list of integer encoded
* NetworkCapability.NET_CAPABILITY_* values
* @param overrideTransport A string representing a single integer encoded override transport
@@ -801,12 +791,12 @@
*/
@VisibleForTesting
static NetworkCapabilities.Builder createNetworkCapabilities(
- boolean clearDefaultCapabilities, @Nullable String commaSeparatedCapabilities,
- @Nullable String overrideTransport) {
+ @Nullable String commaSeparatedCapabilities, @Nullable String overrideTransport) {
- final NetworkCapabilities.Builder builder = clearDefaultCapabilities
- ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
- : new NetworkCapabilities.Builder();
+ final NetworkCapabilities.Builder builder =
+ TextUtils.isEmpty(commaSeparatedCapabilities)
+ ? new NetworkCapabilities.Builder()
+ : NetworkCapabilities.Builder.withoutDefaultCapabilities();
// Determine the transport type. If someone has tried to define an override transport then
// attempt to add it. Since we can only have one override, all errors with it will
@@ -973,11 +963,7 @@
}
for (String iface : interfaces) {
- if (enabled) {
- NetdUtils.setInterfaceUp(mNetd, iface);
- } else {
- NetdUtils.setInterfaceDown(mNetd, iface);
- }
+ setInterfaceUpState(iface, enabled);
}
broadcastEthernetStateChange(mIsEthernetEnabled);
});
@@ -1011,6 +997,12 @@
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());
diff --git a/service/Android.bp b/service/Android.bp
index 2659ebf..c4e2ef0 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -113,7 +113,6 @@
":services.connectivity-netstats-jni-sources",
"jni/com_android_server_connectivity_ClatCoordinator.cpp",
"jni/com_android_server_ServiceManagerWrapper.cpp",
- "jni/com_android_server_TestNetworkService.cpp",
"jni/onload.cpp",
],
header_libs: [
@@ -125,7 +124,7 @@
"libmodules-utils-build",
"libnetjniutils",
"libnet_utils_device_common_bpfjni",
- "libnet_utils_device_common_timerfdjni",
+ "libserviceconnectivityjni",
"netd_aidl_interface-lateststable-ndk",
],
shared_libs: [
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
index 4027038..d1d9e52 100644
--- a/service/ServiceConnectivityResources/res/values/config_thread.xml
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -71,4 +71,18 @@
-->
<string-array name="config_thread_mdns_vendor_specific_txts">
</string-array>
+
+ <!-- Whether to enable / start SRP server only when border routing is ready. SRP server and
+ border routing are mandatory features required by a Thread Border Router, and it takes 10 to
+ 20 seconds to establish border routing. Starting SRP server earlier is useful for use cases
+ where the user needs to know what are the devices in the network before actually needs to reach
+ to the devices, or reaching to Thread end devices doesn't require border routing to work.
+ -->
+ <bool name="config_thread_srp_server_wait_for_border_routing_enabled">true</bool>
+
+ <!-- Whether this border router will automatically join the previous connected network after
+ device reboots. Setting this value to false can allow the user to control the lifecycle of
+ the Thread border router state on this device.
+ -->
+ <bool name="config_thread_border_router_auto_join_enabled">true</bool>
</resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index fbaae05..7ac86aa 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -52,6 +52,8 @@
<item type="string" name="config_thread_vendor_oui" />
<item type="string" name="config_thread_model_name" />
<item type="array" name="config_thread_mdns_vendor_specific_txts" />
+ <item type="bool" name="config_thread_srp_server_wait_for_border_routing_enabled" />
+ <item type="bool" name="config_thread_border_router_auto_join_enabled" />
</policy>
</overlayable>
</resources>
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp
deleted file mode 100644
index 08d31a3..0000000
--- a/service/jni/com_android_server_TestNetworkService.cpp
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#define LOG_NDEBUG 0
-
-#define LOG_TAG "TestNetworkServiceJni"
-
-#include <arpa/inet.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <linux/if.h>
-#include <linux/if_tun.h>
-#include <linux/ipv6_route.h>
-#include <linux/route.h>
-#include <netinet/in.h>
-#include <stdio.h>
-#include <string.h>
-#include <sys/ioctl.h>
-#include <sys/socket.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-
-#include <log/log.h>
-
-#include "jni.h"
-#include <android-base/stringprintf.h>
-#include <android-base/unique_fd.h>
-#include <bpf/KernelUtils.h>
-#include <nativehelper/JNIHelp.h>
-#include <nativehelper/ScopedUtfChars.h>
-
-#ifndef IFF_NO_CARRIER
-#define IFF_NO_CARRIER 0x0040
-#endif
-
-namespace android {
-
-//------------------------------------------------------------------------------
-
-static void throwException(JNIEnv* env, int error, const char* action, const char* iface) {
- const std::string& msg = "Error: " + std::string(action) + " " + std::string(iface) + ": "
- + std::string(strerror(error));
- jniThrowException(env, "java/lang/IllegalStateException", msg.c_str());
-}
-
-// enable or disable carrier on tun / tap interface.
-static void setTunTapCarrierEnabledImpl(JNIEnv* env, const char* iface, int tunFd, bool enabled) {
- uint32_t carrierOn = enabled;
- if (ioctl(tunFd, TUNSETCARRIER, &carrierOn)) {
- throwException(env, errno, "set carrier", iface);
- }
-}
-
-static int createTunTapImpl(JNIEnv* env, bool isTun, bool hasCarrier, bool setIffMulticast,
- const char* iface) {
- base::unique_fd tun(open("/dev/tun", O_RDWR | O_NONBLOCK));
- ifreq ifr{};
-
- // Allocate interface.
- ifr.ifr_flags = (isTun ? IFF_TUN : IFF_TAP) | IFF_NO_PI;
- if (!hasCarrier) {
- // Using IFF_NO_CARRIER is supported starting in kernel version >= 6.0
- // Up until then, unsupported flags are ignored.
- if (!bpf::isAtLeastKernelVersion(6, 0, 0)) {
- throwException(env, EOPNOTSUPP, "IFF_NO_CARRIER not supported", ifr.ifr_name);
- return -1;
- }
- ifr.ifr_flags |= IFF_NO_CARRIER;
- }
- strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
- if (ioctl(tun.get(), TUNSETIFF, &ifr)) {
- throwException(env, errno, "allocating", ifr.ifr_name);
- return -1;
- }
-
- // Mark some TAP interfaces as supporting multicast
- if (setIffMulticast && !isTun) {
- base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
- ifr.ifr_flags = IFF_MULTICAST;
-
- if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
- throwException(env, errno, "set IFF_MULTICAST", ifr.ifr_name);
- return -1;
- }
- }
-
- return tun.release();
-}
-
-static void bringUpInterfaceImpl(JNIEnv* env, const char* iface) {
- // Activate interface using an unconnected datagram socket.
- base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
-
- ifreq ifr{};
- strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
- if (ioctl(inet6CtrlSock.get(), SIOCGIFFLAGS, &ifr)) {
- throwException(env, errno, "read flags", iface);
- return;
- }
- ifr.ifr_flags |= IFF_UP;
- if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
- throwException(env, errno, "set IFF_UP", iface);
- return;
- }
-}
-
-//------------------------------------------------------------------------------
-
-
-
-static void setTunTapCarrierEnabled(JNIEnv* env, jclass /* clazz */, jstring
- jIface, jint tunFd, jboolean enabled) {
- ScopedUtfChars iface(env, jIface);
- if (!iface.c_str()) {
- jniThrowNullPointerException(env, "iface");
- return;
- }
- setTunTapCarrierEnabledImpl(env, iface.c_str(), tunFd, enabled);
-}
-
-static jint createTunTap(JNIEnv* env, jclass /* clazz */, jboolean isTun,
- jboolean hasCarrier, jboolean setIffMulticast, jstring jIface) {
- ScopedUtfChars iface(env, jIface);
- if (!iface.c_str()) {
- jniThrowNullPointerException(env, "iface");
- return -1;
- }
-
- return createTunTapImpl(env, isTun, hasCarrier, setIffMulticast, iface.c_str());
-}
-
-static void bringUpInterface(JNIEnv* env, jclass /* clazz */, jstring jIface) {
- ScopedUtfChars iface(env, jIface);
- if (!iface.c_str()) {
- jniThrowNullPointerException(env, "iface");
- return;
- }
- bringUpInterfaceImpl(env, iface.c_str());
-}
-
-//------------------------------------------------------------------------------
-
-static const JNINativeMethod gMethods[] = {
- {"nativeSetTunTapCarrierEnabled", "(Ljava/lang/String;IZ)V", (void*)setTunTapCarrierEnabled},
- {"nativeCreateTunTap", "(ZZZLjava/lang/String;)I", (void*)createTunTap},
- {"nativeBringUpInterface", "(Ljava/lang/String;)V", (void*)bringUpInterface},
-};
-
-int register_com_android_server_TestNetworkService(JNIEnv* env) {
- return jniRegisterNativeMethods(env,
- "android/net/connectivity/com/android/server/TestNetworkService", gMethods,
- NELEM(gMethods));
-}
-
-}; // namespace android
diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp
index 8e01260..f87470d 100644
--- a/service/jni/onload.cpp
+++ b/service/jni/onload.cpp
@@ -21,12 +21,11 @@
namespace android {
-int register_com_android_server_TestNetworkService(JNIEnv* env);
int register_com_android_server_connectivity_ClatCoordinator(JNIEnv* env);
int register_android_server_net_NetworkStatsFactory(JNIEnv* env);
int register_android_server_net_NetworkStatsService(JNIEnv* env);
int register_com_android_server_ServiceManagerWrapper(JNIEnv* env);
-int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
+int register_com_android_net_module_util_ServiceConnectivityJni(JNIEnv *env,
char const *class_name);
extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
@@ -36,10 +35,6 @@
return JNI_ERR;
}
- if (register_com_android_server_TestNetworkService(env) < 0) {
- return JNI_ERR;
- }
-
if (register_com_android_server_ServiceManagerWrapper(env) < 0) {
return JNI_ERR;
}
@@ -58,9 +53,9 @@
}
}
- if (register_com_android_net_module_util_TimerFdUtils(
+ if (register_com_android_net_module_util_ServiceConnectivityJni(
env, "android/net/connectivity/com/android/net/module/util/"
- "TimerFdUtils") < 0) {
+ "ServiceConnectivityJni") < 0) {
return JNI_ERR;
}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index bad7246..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;
@@ -6763,7 +6766,7 @@
final NetworkOfferInfo offer =
findNetworkOfferInfoByCallback((INetworkOfferCallback) msg.obj);
if (null != offer) {
- handleUnregisterNetworkOffer(offer);
+ handleUnregisterNetworkOffer(offer, true /* releaseReservations */);
}
break;
}
@@ -7680,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");
+ }
}
/**
@@ -7801,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
@@ -7820,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;
@@ -7854,7 +7885,7 @@
@NetworkCallback.Flag int callbackFlags,
@Nullable String callingAttributionTag, int declaredMethodsFlags) {
super();
- ensureAllNetworkRequestsHaveType(r);
+ ensureAllNetworkRequestsHaveSupportedType(r);
mRequests = initializeRequests(r);
mNetworkRequestForCallback = requestForCallback;
mMessenger = m;
@@ -7874,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();
@@ -8160,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
@@ -8211,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));
}
@@ -8854,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));
}
@@ -8897,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),
@@ -8935,7 +8980,7 @@
}
}
for (final NetworkOfferInfo noi : toRemove) {
- handleUnregisterNetworkOffer(noi);
+ handleUnregisterNetworkOffer(noi, true /* releaseReservations */);
}
if (DBG) log("unregisterNetworkProvider for " + npi.name);
}
@@ -9368,7 +9413,7 @@
@Override
public void binderDied() {
- mHandler.post(() -> handleUnregisterNetworkOffer(this));
+ mHandler.post(() -> handleUnregisterNetworkOffer(this, true /* releaseReservations */));
}
}
@@ -9379,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
@@ -9395,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) {
@@ -9418,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);
@@ -9428,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 */);
}
@@ -10646,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) {
@@ -10660,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;
}
@@ -10667,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/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java
new file mode 100644
index 0000000..c5ec9ee
--- /dev/null
+++ b/service/src/com/android/server/L2capNetworkProvider.java
@@ -0,0 +1,244 @@
+/*
+ * 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 static android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN;
+import static android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_ANY;
+import static android.net.L2capNetworkSpecifier.PSM_ANY;
+import static android.net.L2capNetworkSpecifier.ROLE_SERVER;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS;
+import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
+import static android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.L2capNetworkSpecifier;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkProvider.NetworkOfferCallback;
+import android.net.NetworkRequest;
+import android.net.NetworkScore;
+import android.net.NetworkSpecifier;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Map;
+
+
+public class L2capNetworkProvider {
+ private static final String TAG = L2capNetworkProvider.class.getSimpleName();
+ private final Dependencies mDeps;
+ private final Context mContext;
+ private final HandlerThread mHandlerThread;
+ private final Handler mHandler;
+ private final NetworkProvider mProvider;
+ private final BlanketReservationOffer mBlanketOffer;
+ private final Map<Integer, ReservedServerOffer> mReservedServerOffers = new ArrayMap<>();
+
+ /**
+ * The blanket reservation offer is used to create an L2CAP server network, i.e. a network
+ * based on a BluetoothServerSocket.
+ *
+ * Note that NetworkCapabilities matching semantics will cause onNetworkNeeded to be called for
+ * requests that do not have a NetworkSpecifier set.
+ */
+ private class BlanketReservationOffer implements NetworkOfferCallback {
+ // TODO: ensure that once the incoming request is satisfied, the blanket offer does not get
+ // unneeded. This means the blanket offer must always outscore the reserved offer. This
+ // might require setting the blanket offer as setTransportPrimary().
+ public static final NetworkScore SCORE = new NetworkScore.Builder().build();
+ // Note the missing NET_CAPABILITY_NOT_RESTRICTED marking the network as restricted.
+ public static final NetworkCapabilities CAPABILITIES;
+ static {
+ final L2capNetworkSpecifier l2capNetworkSpecifier = new L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .build();
+ NetworkCapabilities caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ // TODO: consider removing NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED.
+ .addCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+ .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+ .addCapability(NET_CAPABILITY_NOT_METERED)
+ .addCapability(NET_CAPABILITY_NOT_ROAMING)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .addCapability(NET_CAPABILITY_NOT_VPN)
+ .setNetworkSpecifier(l2capNetworkSpecifier)
+ .build();
+ caps.setReservationId(RES_ID_MATCH_ALL_RESERVATIONS);
+ CAPABILITIES = caps;
+ }
+
+ // TODO: consider moving this into L2capNetworkSpecifier as #isValidServerReservation().
+ private boolean isValidL2capSpecifier(@Nullable NetworkSpecifier spec) {
+ if (spec == null) return false;
+ // If spec is not null, L2capNetworkSpecifier#canBeSatisfiedBy() guarantees the
+ // specifier is of type L2capNetworkSpecifier.
+ final L2capNetworkSpecifier l2capSpec = (L2capNetworkSpecifier) spec;
+
+ // The ROLE_SERVER offer can be satisfied by a ROLE_ANY request.
+ if (l2capSpec.getRole() != ROLE_SERVER) return false;
+
+ // HEADER_COMPRESSION_ANY is never valid in a request.
+ if (l2capSpec.getHeaderCompression() == HEADER_COMPRESSION_ANY) return false;
+
+ // remoteAddr must be null for ROLE_SERVER requests.
+ if (l2capSpec.getRemoteAddress() != null) return false;
+
+ // reservation must allocate a PSM, so only PSM_ANY can be passed.
+ if (l2capSpec.getPsm() != PSM_ANY) return false;
+
+ return true;
+ }
+
+ @Override
+ public void onNetworkNeeded(NetworkRequest request) {
+ Log.d(TAG, "New reservation request: " + request);
+ if (!isValidL2capSpecifier(request.getNetworkSpecifier())) {
+ Log.w(TAG, "Ignoring invalid reservation request: " + request);
+ return;
+ }
+
+ final NetworkCapabilities reservationCaps = request.networkCapabilities;
+ final ReservedServerOffer reservedOffer = new ReservedServerOffer(reservationCaps);
+
+ final NetworkCapabilities reservedCaps = reservedOffer.getReservedCapabilities();
+ mProvider.registerNetworkOffer(SCORE, reservedCaps, mHandler::post, reservedOffer);
+ mReservedServerOffers.put(request.requestId, reservedOffer);
+ }
+
+ @Override
+ public void onNetworkUnneeded(NetworkRequest request) {
+ if (!mReservedServerOffers.containsKey(request.requestId)) {
+ return;
+ }
+
+ final ReservedServerOffer reservedOffer = mReservedServerOffers.get(request.requestId);
+ // Note that the reserved offer gets torn down when the reservation goes away, even if
+ // there are lingering requests.
+ reservedOffer.tearDown();
+ mProvider.unregisterNetworkOffer(reservedOffer);
+ }
+ }
+
+ private class ReservedServerOffer implements NetworkOfferCallback {
+ private final boolean mUseHeaderCompression;
+ private final int mPsm;
+ private final NetworkCapabilities mReservedCapabilities;
+
+ public ReservedServerOffer(NetworkCapabilities reservationCaps) {
+ // getNetworkSpecifier() is guaranteed to return a non-null L2capNetworkSpecifier.
+ final L2capNetworkSpecifier reservationSpec =
+ (L2capNetworkSpecifier) reservationCaps.getNetworkSpecifier();
+ mUseHeaderCompression =
+ reservationSpec.getHeaderCompression() == HEADER_COMPRESSION_6LOWPAN;
+
+ // TODO: open BluetoothServerSocket and allocate a PSM.
+ mPsm = 0x80;
+
+ final L2capNetworkSpecifier reservedSpec = new L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(reservationSpec.getHeaderCompression())
+ .setPsm(mPsm)
+ .build();
+ mReservedCapabilities = new NetworkCapabilities.Builder(reservationCaps)
+ .setNetworkSpecifier(reservedSpec)
+ .build();
+ }
+
+ public NetworkCapabilities getReservedCapabilities() {
+ return mReservedCapabilities;
+ }
+
+ @Override
+ public void onNetworkNeeded(NetworkRequest request) {
+ // TODO: implement
+ }
+
+ @Override
+ public void onNetworkUnneeded(NetworkRequest request) {
+ // TODO: implement
+ }
+
+ /**
+ * Called when the reservation goes away and the reserved offer must be torn down.
+ *
+ * This method can be called multiple times.
+ */
+ public void tearDown() {
+ // TODO: implement.
+ // This method can be called multiple times.
+ }
+ }
+
+ @VisibleForTesting
+ public static class Dependencies {
+ /** Get NetworkProvider */
+ public NetworkProvider getNetworkProvider(Context context, Looper looper) {
+ return new NetworkProvider(context, looper, TAG);
+ }
+
+ /** Get the HandlerThread for L2capNetworkProvider to run on */
+ public HandlerThread getHandlerThread() {
+ final HandlerThread thread = new HandlerThread("L2capNetworkProviderThread");
+ thread.start();
+ return thread;
+ }
+ }
+
+ public L2capNetworkProvider(Context context) {
+ this(new Dependencies(), context);
+ }
+
+ @VisibleForTesting
+ public L2capNetworkProvider(Dependencies deps, Context context) {
+ mDeps = deps;
+ mContext = context;
+ mHandlerThread = mDeps.getHandlerThread();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ mProvider = mDeps.getNetworkProvider(context, mHandlerThread.getLooper());
+ mBlanketOffer = new BlanketReservationOffer();
+ }
+
+ /**
+ * Start L2capNetworkProvider.
+ *
+ * Called on CS Handler thread.
+ */
+ public void start() {
+ final PackageManager pm = mContext.getPackageManager();
+ if (pm.hasSystemFeature(FEATURE_BLUETOOTH_LE)) {
+ mContext.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider);
+ mProvider.registerNetworkOffer(BlanketReservationOffer.SCORE,
+ BlanketReservationOffer.CAPABILITIES, mHandler::post, mBlanketOffer);
+ }
+ }
+}
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index 4d39d7d..96f4e20 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -48,6 +48,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.ServiceConnectivityJni;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -75,15 +76,6 @@
@NonNull private final ConnectivityManager mCm;
@NonNull private final NetworkProvider mNetworkProvider;
- // Native method stubs
- private static native int nativeCreateTunTap(boolean isTun, boolean hasCarrier,
- boolean setIffMulticast, @NonNull String iface);
-
- private static native void nativeSetTunTapCarrierEnabled(@NonNull String iface, int tunFd,
- boolean enabled);
-
- private static native void nativeBringUpInterface(String iface);
-
@VisibleForTesting
protected TestNetworkService(@NonNull Context context) {
mHandlerThread = new HandlerThread("TestNetworkServiceThread");
@@ -143,7 +135,8 @@
// flags atomically.
final boolean setIffMulticast = bringUp;
ParcelFileDescriptor tunIntf = ParcelFileDescriptor.adoptFd(
- nativeCreateTunTap(isTun, hasCarrier, setIffMulticast, interfaceName));
+ ServiceConnectivityJni.createTunTap(
+ isTun, hasCarrier, setIffMulticast, interfaceName));
// Disable DAD and remove router_solicitation_delay before assigning link addresses.
if (disableIpv6ProvisioningDelay) {
@@ -160,7 +153,7 @@
}
if (bringUp) {
- nativeBringUpInterface(interfaceName);
+ ServiceConnectivityJni.bringUpInterface(interfaceName);
}
return new TestNetworkInterface(tunIntf, interfaceName);
@@ -403,11 +396,11 @@
@Override
public void setCarrierEnabled(@NonNull TestNetworkInterface iface, boolean enabled) {
enforceTestNetworkPermissions(mContext);
- nativeSetTunTapCarrierEnabled(iface.getInterfaceName(), iface.getFileDescriptor().getFd(),
- enabled);
+ ServiceConnectivityJni.setTunTapCarrierEnabled(iface.getInterfaceName(),
+ iface.getFileDescriptor().getFd(), enabled);
// Explicitly close fd after use to prevent StrictMode from complaining.
// Also, explicitly referencing iface guarantees that the object is not garbage collected
- // before nativeSetTunTapCarrierEnabled() executes.
+ // before setTunTapCarrierEnabled() executes.
try {
iface.getFileDescriptor().close();
} catch (IOException e) {
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 2b00386..2686e4a 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -29,6 +29,9 @@
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;
import android.content.Context;
@@ -73,6 +76,7 @@
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;
@@ -1604,6 +1608,12 @@
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;
}
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 71e09fe..0eab6e7 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -350,7 +350,7 @@
// TODO: remove "apex_available:platform".
apex_available: [
"//apex_available:platform",
- "com.android.btservices",
+ "com.android.bt",
"com.android.tethering",
"com.android.wifi",
],
@@ -438,10 +438,7 @@
srcs: [
"device/com/android/net/module/util/FdEventsReader.java",
"device/com/android/net/module/util/HandlerUtils.java",
- "device/com/android/net/module/util/JniUtil.java",
"device/com/android/net/module/util/SharedLog.java",
- "device/com/android/net/module/util/TimerFdUtils.java",
- "device/com/android/net/module/util/TimerFileDescriptor.java",
"framework/com/android/net/module/util/ByteUtils.java",
"framework/com/android/net/module/util/CollectionUtils.java",
"framework/com/android/net/module/util/DnsUtils.java",
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 553a24b..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
@@ -161,9 +161,9 @@
netd.tetherInterfaceAdd(iface);
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));
}
@@ -194,12 +194,12 @@
}
/** 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);
}
}
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 ab90a50..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, LOCAL_NET_ID, 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, LOCAL_NET_ID, 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, LOCAL_NET_ID, 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/RealtimeScheduler.java b/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
new file mode 100644
index 0000000..c8fdf72
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/RealtimeScheduler.java
@@ -0,0 +1,345 @@
+/*
+ * 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.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.CloseGuard;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.io.IOException;
+import java.util.PriorityQueue;
+
+/**
+ * Represents a realtime scheduler 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 RealtimeScheduler **
+ *
+ * ```java
+ * // Create a RealtimeScheduler
+ * final RealtimeScheduler scheduler = new RealtimeScheduler(handler);
+ *
+ * // Schedule a new task with a delay.
+ * scheduler.postDelayed(() -> taskToExecute(), delayTime);
+ *
+ * // Once the delay has elapsed, and the task is running, schedule another task.
+ * scheduler.postDelayed(() -> anotherTaskToExecute(), anotherDelayTime);
+ *
+ * // Remember to close the RealtimeScheduler after all tasks have finished running.
+ * scheduler.close();
+ * ```
+ */
+public class RealtimeScheduler {
+ private static final String TAG = RealtimeScheduler.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;
+
+ private final PriorityQueue<Task> mTaskQueue;
+
+ /**
+ * An abstract class for defining tasks that can be executed using a {@link Handler}.
+ */
+ private abstract static class Task implements Comparable<Task> {
+ private final long mRunTimeMs;
+ private final long mCreatedTimeNs = SystemClock.elapsedRealtimeNanos();
+
+ /**
+ * create a task with a run time
+ */
+ Task(long runTimeMs) {
+ mRunTimeMs = runTimeMs;
+ }
+
+ /**
+ * Executes the task using the provided {@link Handler}.
+ *
+ * @param handler The {@link Handler} to use for executing the task.
+ */
+ abstract void post(Handler handler);
+
+ @Override
+ public int compareTo(@NonNull Task o) {
+ if (mRunTimeMs != o.mRunTimeMs) {
+ return Long.compare(mRunTimeMs, o.mRunTimeMs);
+ }
+ return Long.compare(mCreatedTimeNs, o.mCreatedTimeNs);
+ }
+
+ /**
+ * Returns the run time of the task.
+ */
+ public long getRunTimeMs() {
+ return mRunTimeMs;
+ }
+ }
+
+ /**
+ * A task that sends a {@link Message} using a {@link Handler}.
+ */
+ private static class MessageTask extends Task {
+ private final Message mMessage;
+
+ MessageTask(Message message, long runTimeMs) {
+ super(runTimeMs);
+ 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}.
+ */
+ private static class RunnableTask extends Task {
+ private final Runnable mRunnable;
+
+ RunnableTask(Runnable runnable, long runTimeMs) {
+ super(runTimeMs);
+ 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);
+ }
+ }
+
+ /**
+ * The RealtimeScheduler 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 RealtimeScheduler(@NonNull Handler handler) {
+ mFdInt = TimerFdUtils.createTimerFileDescriptor();
+ mParcelFileDescriptor = ParcelFileDescriptor.adoptFd(mFdInt);
+ mHandler = handler;
+ mQueue = handler.getLooper().getQueue();
+ mTaskQueue = new PriorityQueue<>();
+ registerFdEventListener();
+
+ mGuard.open("close");
+ }
+
+ private boolean enqueueTask(@NonNull Task task, long delayMs) {
+ ensureRunningOnCorrectThread();
+ if (delayMs <= 0L) {
+ task.post(mHandler);
+ return true;
+ }
+ if (mTaskQueue.isEmpty() || task.compareTo(mTaskQueue.peek()) < 0) {
+ if (!TimerFdUtils.setExpirationTime(mFdInt, delayMs)) {
+ return false;
+ }
+ }
+ mTaskQueue.add(task);
+ return true;
+ }
+
+ /**
+ * Set a runnable to be executed after a specified delay.
+ *
+ * If delayMs is less than or equal to 0, the runnable will be executed immediately.
+ *
+ * @param runnable the runnable to be executed
+ * @param delayMs the delay time in milliseconds
+ * @return true if the task is scheduled successfully, false otherwise.
+ */
+ public boolean postDelayed(@NonNull Runnable runnable, long delayMs) {
+ return enqueueTask(new RunnableTask(runnable, SystemClock.elapsedRealtime() + delayMs),
+ delayMs);
+ }
+
+ /**
+ * Remove a scheduled runnable.
+ *
+ * @param runnable the runnable to be removed
+ */
+ public void removeDelayedRunnable(@NonNull Runnable runnable) {
+ ensureRunningOnCorrectThread();
+ mTaskQueue.removeIf(task -> task instanceof RunnableTask
+ && ((RunnableTask) task).mRunnable == runnable);
+ }
+
+ /**
+ * Set a message to be sent after a specified delay.
+ *
+ * If delayMs is less than or equal to 0, the message will be sent immediately.
+ *
+ * @param msg the message to be sent
+ * @param delayMs the delay time in milliseconds
+ * @return true if the message is scheduled successfully, false otherwise.
+ */
+ public boolean sendDelayedMessage(Message msg, long delayMs) {
+
+ return enqueueTask(new MessageTask(msg, SystemClock.elapsedRealtime() + delayMs), delayMs);
+ }
+
+ /**
+ * Remove a scheduled message.
+ *
+ * @param what the message to be removed
+ */
+ public void removeDelayedMessage(int what) {
+ ensureRunningOnCorrectThread();
+ mTaskQueue.removeIf(task -> task instanceof MessageTask
+ && ((MessageTask) task).mMessage.what == what);
+ }
+
+ /**
+ * Close the RealtimeScheduler. 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_ERROR) != 0) {
+ Log.wtf(TAG, "Got EVENT_ERROR from FileDescriptorEventListener.");
+ return 0;
+ }
+ if ((events & EVENT_INPUT) != 0) {
+ handleExpiration();
+ }
+ return FD_EVENTS;
+ });
+ }
+
+ private boolean isRunning() {
+ return mParcelFileDescriptor.getFileDescriptor().valid();
+ }
+
+ private void handleExpiration() {
+ // The data from the FileDescriptor must be read after the timer expires. Otherwise,
+ // expiration callbacks will continue to be sent, notifying of unread data. The content(the
+ // number of expirations) can be ignored, as the callback is the only item of interest.
+ // Refer to https://man7.org/linux/man-pages/man2/timerfd_create.2.html
+ // read(2)
+ // If the timer has already expired one or more times since
+ // its settings were last modified using timerfd_settime(),
+ // or since the last successful read(2), then the buffer
+ // given to read(2) returns an unsigned 8-byte integer
+ // (uint64_t) containing the number of expirations that have
+ // occurred. (The returned value is in host byte order—that
+ // is, the native byte order for integers on the host
+ // machine.)
+ final byte[] readBuffer = new byte[8];
+ try {
+ Os.read(mParcelFileDescriptor.getFileDescriptor(), readBuffer, 0, readBuffer.length);
+ } catch (IOException | ErrnoException exception) {
+ Log.wtf(TAG, "Read FileDescriptor failed. ", exception);
+ }
+
+ long currentTimeMs = SystemClock.elapsedRealtime();
+ while (!mTaskQueue.isEmpty()) {
+ final Task task = mTaskQueue.peek();
+ currentTimeMs = SystemClock.elapsedRealtime();
+ if (currentTimeMs < task.getRunTimeMs()) {
+ break;
+ }
+ task.post(mHandler);
+ mTaskQueue.poll();
+ }
+
+ if (!mTaskQueue.isEmpty()) {
+ // Using currentTimeMs ensures that the calculated expiration time
+ // is always positive.
+ if (!TimerFdUtils.setExpirationTime(mFdInt,
+ mTaskQueue.peek().getRunTimeMs() - currentTimeMs)) {
+ // If setting the expiration time fails, clear the task queue.
+ Log.wtf(TAG, "Failed to set expiration time");
+ mTaskQueue.clear();
+ }
+ }
+ }
+
+ 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/ServiceConnectivityJni.java b/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
new file mode 100644
index 0000000..4a5dd4f
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 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 java.io.IOException;
+
+/**
+ * Contains JNI functions for use in service-connectivity
+ */
+public class ServiceConnectivityJni {
+ static {
+ final String libName = JniUtil.getJniLibraryName(ServiceConnectivityJni.class.getPackage());
+ if (libName.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(libName);
+ }
+ }
+
+ /**
+ * Create a timerfd.
+ *
+ * @throws IOException if the timerfd creation is failed.
+ */
+ public static native int createTimerFd() throws IOException;
+
+ /**
+ * Set given time to the timerfd.
+ *
+ * @param timeMs target time
+ * @throws IOException if setting expiration time is failed.
+ */
+ public static native void setTimerFdTime(int fd, long timeMs) throws IOException;
+
+ /** Create tun/tap interface */
+ public static native int createTunTap(boolean isTun, boolean hasCarrier,
+ boolean setIffMulticast, @NonNull String iface);
+
+ /** Enable carrier on tun/tap interface */
+ public static native void setTunTapCarrierEnabled(@NonNull String iface, int tunFd,
+ boolean enabled);
+
+ /** Bring up tun/tap interface */
+ public static native void bringUpInterface(String iface);
+}
diff --git a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
index c7ed911..10bc595 100644
--- a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
+++ b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
@@ -25,40 +25,14 @@
* 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();
+ return ServiceConnectivityJni.createTimerFd();
} catch (IOException e) {
Log.e(TAG, "createTimerFd failed", e);
return -1;
@@ -68,9 +42,9 @@
/**
* Set expiration time to timerfd
*/
- static boolean setExpirationTime(int id, long expirationTimeMs) {
+ static boolean setExpirationTime(int fd, long expirationTimeMs) {
try {
- setTime(id, expirationTimeMs);
+ ServiceConnectivityJni.setTimerFdTime(fd, expirationTimeMs);
} catch (IOException e) {
Log.e(TAG, "setExpirationTime failed", e);
return false;
diff --git a/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java b/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java
deleted file mode 100644
index a8c0f17..0000000
--- a/staticlibs/device/com/android/net/module/util/TimerFileDescriptor.java
+++ /dev/null
@@ -1,261 +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.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);
- }
-
- /**
- * Get scheduled message
- */
- public Message getMessage() {
- return 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/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/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/timerfdutils/Android.bp b/staticlibs/native/serviceconnectivityjni/Android.bp
similarity index 86%
rename from staticlibs/native/timerfdutils/Android.bp
rename to staticlibs/native/serviceconnectivityjni/Android.bp
index 939a2d2..18246dd 100644
--- a/staticlibs/native/timerfdutils/Android.bp
+++ b/staticlibs/native/serviceconnectivityjni/Android.bp
@@ -18,17 +18,20 @@
}
cc_library_static {
- name: "libnet_utils_device_common_timerfdjni",
+ name: "libserviceconnectivityjni",
srcs: [
- "com_android_net_module_util_TimerFdUtils.cpp",
+ "com_android_net_module_util_ServiceConnectivityJni.cpp",
],
header_libs: [
+ "bpf_headers",
"jni_headers",
+ "libbase_headers",
],
shared_libs: [
"liblog",
"libnativehelper_compat_libc++",
],
+ stl: "libc++_static",
cflags: [
"-Wall",
"-Werror",
diff --git a/staticlibs/native/serviceconnectivityjni/com_android_net_module_util_ServiceConnectivityJni.cpp b/staticlibs/native/serviceconnectivityjni/com_android_net_module_util_ServiceConnectivityJni.cpp
new file mode 100644
index 0000000..8767589
--- /dev/null
+++ b/staticlibs/native/serviceconnectivityjni/com_android_net_module_util_ServiceConnectivityJni.cpp
@@ -0,0 +1,209 @@
+/*
+ * 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 <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <jni.h>
+#include <linux/if.h>
+#include <linux/if_tun.h>
+#include <linux/ipv6_route.h>
+#include <linux/route.h>
+#include <netinet/in.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <string>
+#include <sys/epoll.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/timerfd.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <android-base/unique_fd.h>
+#include <bpf/KernelUtils.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/scoped_utf_chars.h>
+
+#define MSEC_PER_SEC 1000
+#define NSEC_PER_MSEC 1000000
+
+#ifndef IFF_NO_CARRIER
+#define IFF_NO_CARRIER 0x0040
+#endif
+
+namespace android {
+
+static jint createTimerFd(JNIEnv *env, jclass clazz) {
+ int tfd;
+ // For safety, the file descriptor should have O_NONBLOCK(TFD_NONBLOCK) set
+ // using fcntl during creation. This ensures that, in the worst-case scenario,
+ // an EAGAIN error is returned when reading.
+ tfd = timerfd_create(CLOCK_BOOTTIME, TFD_NONBLOCK);
+ if (tfd == -1) {
+ jniThrowErrnoException(env, "createTimerFd", tfd);
+ }
+ return tfd;
+}
+
+static void setTimerFdTime(JNIEnv *env, jclass clazz, jint tfd,
+ jlong milliseconds) {
+ struct itimerspec new_value;
+ new_value.it_value.tv_sec = milliseconds / MSEC_PER_SEC;
+ new_value.it_value.tv_nsec = (milliseconds % MSEC_PER_SEC) * NSEC_PER_MSEC;
+ // Set the interval time to 0 because it's designed for repeated timer
+ // expirations after the initial expiration, which doesn't fit the current
+ // usage.
+ new_value.it_interval.tv_sec = 0;
+ new_value.it_interval.tv_nsec = 0;
+
+ int ret = timerfd_settime(tfd, 0, &new_value, NULL);
+ if (ret == -1) {
+ jniThrowErrnoException(env, "setTimerFdTime", ret);
+ }
+}
+
+static void throwException(JNIEnv *env, int error, const char *action,
+ const char *iface) {
+ const std::string &msg = "Error: " + std::string(action) + " " +
+ std::string(iface) + ": " +
+ std::string(strerror(error));
+ jniThrowException(env, "java/lang/IllegalStateException", msg.c_str());
+}
+
+// enable or disable carrier on tun / tap interface.
+static void setTunTapCarrierEnabledImpl(JNIEnv *env, const char *iface,
+ int tunFd, bool enabled) {
+ uint32_t carrierOn = enabled;
+ if (ioctl(tunFd, TUNSETCARRIER, &carrierOn)) {
+ throwException(env, errno, "set carrier", iface);
+ }
+}
+
+static int createTunTapImpl(JNIEnv *env, bool isTun, bool hasCarrier,
+ bool setIffMulticast, const char *iface) {
+ base::unique_fd tun(open("/dev/tun", O_RDWR | O_NONBLOCK));
+ ifreq ifr{};
+
+ // Allocate interface.
+ ifr.ifr_flags = (isTun ? IFF_TUN : IFF_TAP) | IFF_NO_PI;
+ if (!hasCarrier) {
+ // Using IFF_NO_CARRIER is supported starting in kernel version >= 6.0
+ // Up until then, unsupported flags are ignored.
+ if (!bpf::isAtLeastKernelVersion(6, 0, 0)) {
+ throwException(env, EOPNOTSUPP, "IFF_NO_CARRIER not supported",
+ ifr.ifr_name);
+ return -1;
+ }
+ ifr.ifr_flags |= IFF_NO_CARRIER;
+ }
+ strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
+ if (ioctl(tun.get(), TUNSETIFF, &ifr)) {
+ throwException(env, errno, "allocating", ifr.ifr_name);
+ return -1;
+ }
+
+ // Mark some TAP interfaces as supporting multicast
+ if (setIffMulticast && !isTun) {
+ base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
+ ifr.ifr_flags = IFF_MULTICAST;
+
+ if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
+ throwException(env, errno, "set IFF_MULTICAST", ifr.ifr_name);
+ return -1;
+ }
+ }
+
+ return tun.release();
+}
+
+static void bringUpInterfaceImpl(JNIEnv *env, const char *iface) {
+ // Activate interface using an unconnected datagram socket.
+ base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
+
+ ifreq ifr{};
+ strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
+ if (ioctl(inet6CtrlSock.get(), SIOCGIFFLAGS, &ifr)) {
+ throwException(env, errno, "read flags", iface);
+ return;
+ }
+ ifr.ifr_flags |= IFF_UP;
+ if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
+ throwException(env, errno, "set IFF_UP", iface);
+ return;
+ }
+}
+
+//------------------------------------------------------------------------------
+
+static void setTunTapCarrierEnabled(JNIEnv *env, jclass /* clazz */,
+ jstring jIface, jint tunFd,
+ jboolean enabled) {
+ ScopedUtfChars iface(env, jIface);
+ if (!iface.c_str()) {
+ jniThrowNullPointerException(env, "iface");
+ return;
+ }
+ setTunTapCarrierEnabledImpl(env, iface.c_str(), tunFd, enabled);
+}
+
+static jint createTunTap(JNIEnv *env, jclass /* clazz */, jboolean isTun,
+ jboolean hasCarrier, jboolean setIffMulticast,
+ jstring jIface) {
+ ScopedUtfChars iface(env, jIface);
+ if (!iface.c_str()) {
+ jniThrowNullPointerException(env, "iface");
+ return -1;
+ }
+
+ return createTunTapImpl(env, isTun, hasCarrier, setIffMulticast,
+ iface.c_str());
+}
+
+static void bringUpInterface(JNIEnv *env, jclass /* clazz */, jstring jIface) {
+ ScopedUtfChars iface(env, jIface);
+ if (!iface.c_str()) {
+ jniThrowNullPointerException(env, "iface");
+ return;
+ }
+ bringUpInterfaceImpl(env, iface.c_str());
+}
+
+//------------------------------------------------------------------------------
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"createTimerFd", "()I", (void *)createTimerFd},
+ {"setTimerFdTime", "(IJ)V", (void *)setTimerFdTime},
+ {"setTunTapCarrierEnabled", "(Ljava/lang/String;IZ)V",
+ (void *)setTunTapCarrierEnabled},
+ {"createTunTap", "(ZZZLjava/lang/String;)I", (void *)createTunTap},
+ {"bringUpInterface", "(Ljava/lang/String;)V", (void *)bringUpInterface},
+};
+
+int register_com_android_net_module_util_ServiceConnectivityJni(
+ JNIEnv *env, char const *class_name) {
+ return jniRegisterNativeMethods(env, class_name, gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp b/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp
deleted file mode 100644
index c4c960d..0000000
--- a/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp
+++ /dev/null
@@ -1,79 +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.
- */
-
-#include <errno.h>
-#include <jni.h>
-#include <nativehelper/JNIHelp.h>
-#include <nativehelper/scoped_utf_chars.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/epoll.h>
-#include <sys/timerfd.h>
-#include <time.h>
-#include <unistd.h>
-
-#define MSEC_PER_SEC 1000
-#define NSEC_PER_MSEC 1000000
-
-namespace android {
-
-static jint
-com_android_net_module_util_TimerFdUtils_createTimerFd(JNIEnv *env,
- jclass clazz) {
- int tfd;
- tfd = timerfd_create(CLOCK_BOOTTIME, 0);
- if (tfd == -1) {
- jniThrowErrnoException(env, "createTimerFd", tfd);
- }
- return tfd;
-}
-
-static void
-com_android_net_module_util_TimerFdUtils_setTime(JNIEnv *env, jclass clazz,
- jint tfd, jlong milliseconds) {
- struct itimerspec new_value;
- new_value.it_value.tv_sec = milliseconds / MSEC_PER_SEC;
- new_value.it_value.tv_nsec = (milliseconds % MSEC_PER_SEC) * NSEC_PER_MSEC;
- // Set the interval time to 0 because it's designed for repeated timer expirations after the
- // initial expiration, which doesn't fit the current usage.
- new_value.it_interval.tv_sec = 0;
- new_value.it_interval.tv_nsec = 0;
-
- int ret = timerfd_settime(tfd, 0, &new_value, NULL);
- if (ret == -1) {
- jniThrowErrnoException(env, "setTime", ret);
- }
-}
-
-/*
- * JNI registration.
- */
-static const JNINativeMethod gMethods[] = {
- /* name, signature, funcPtr */
- {"createTimerFd", "()I",
- (void *)com_android_net_module_util_TimerFdUtils_createTimerFd},
- {"setTime", "(IJ)V",
- (void *)com_android_net_module_util_TimerFdUtils_setTime},
-};
-
-int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
- char const *class_name) {
- return jniRegisterNativeMethods(env, class_name, gMethods, NELEM(gMethods));
-}
-
-}; // namespace android
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 9d1d291..f4f1ea9 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -28,6 +28,7 @@
"net-utils-device-common-struct-base",
"net-utils-device-common-wear",
"net-utils-service-connectivity",
+ "truth",
],
libs: [
"android.test.runner.stubs",
diff --git a/staticlibs/tests/unit/jni/Android.bp b/staticlibs/tests/unit/jni/Android.bp
index e456471..c444159 100644
--- a/staticlibs/tests/unit/jni/Android.bp
+++ b/staticlibs/tests/unit/jni/Android.bp
@@ -30,7 +30,7 @@
"com_android_net_moduletests_util/onload.cpp",
],
static_libs: [
- "libnet_utils_device_common_timerfdjni",
+ "libserviceconnectivityjni",
],
shared_libs: [
"liblog",
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
index a035540..af4810f 100644
--- a/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp
+++ b/staticlibs/tests/unit/jni/com_android_net_moduletests_util/onload.cpp
@@ -22,7 +22,7 @@
namespace android {
-int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
+int register_com_android_net_module_util_ServiceConnectivityJni(JNIEnv *env,
char const *class_name);
extern "C" jint JNI_OnLoad(JavaVM *vm, void *) {
@@ -32,8 +32,8 @@
return JNI_ERR;
}
- if (register_com_android_net_module_util_TimerFdUtils(
- env, "com/android/net/moduletests/util/TimerFdUtils") < 0)
+ if (register_com_android_net_module_util_ServiceConnectivityJni(
+ env, "com/android/net/moduletests/util/ServiceConnectivityJni") < 0)
return JNI_ERR;
return JNI_VERSION_1_6;
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/RealtimeSchedulerTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/RealtimeSchedulerTest.kt
new file mode 100644
index 0000000..30b530f
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/RealtimeSchedulerTest.kt
@@ -0,0 +1,140 @@
+/*
+ * 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 android.os.SystemClock
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.tryTest
+import com.android.testutils.visibleOnHandlerThread
+import com.google.common.collect.Range
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertEquals
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class RealtimeSchedulerTest {
+
+ private val TIMEOUT_MS = 1000L
+ private val TOLERANCE_MS = 50L
+ private class TestHandler(looper: Looper) : Handler(looper) {
+ override fun handleMessage(msg: Message) {
+ val pair = msg.obj as Pair<ConditionVariable, MutableList<Long>>
+ val cv = pair.first
+ cv.open()
+ val executionTimes = pair.second
+ executionTimes.add(SystemClock.elapsedRealtime())
+ }
+ }
+ private val thread = HandlerThread(RealtimeSchedulerTest::class.simpleName).apply { start() }
+ private val handler by lazy { TestHandler(thread.looper) }
+
+ @After
+ fun tearDown() {
+ thread.quitSafely()
+ thread.join()
+ }
+
+ @Test
+ fun testMultiplePostDelayedTasks() {
+ val scheduler = RealtimeScheduler(handler)
+ tryTest {
+ val initialTimeMs = SystemClock.elapsedRealtime()
+ val executionTimes = mutableListOf<Long>()
+ val cv = ConditionVariable()
+ handler.post {
+ scheduler.postDelayed(
+ { executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 0)
+ scheduler.postDelayed(
+ { executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 200)
+ val toBeRemoved = Runnable {
+ executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs)
+ }
+ scheduler.postDelayed(toBeRemoved, 250)
+ scheduler.removeDelayedRunnable(toBeRemoved)
+ scheduler.postDelayed(
+ { executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 100)
+ scheduler.postDelayed({
+ executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs)
+ cv.open() }, 300)
+ }
+ cv.block(TIMEOUT_MS)
+ assertEquals(4, executionTimes.size)
+ assertThat(executionTimes[0]).isIn(Range.closed(0L, TOLERANCE_MS))
+ assertThat(executionTimes[1]).isIn(Range.closed(100L, 100 + TOLERANCE_MS))
+ assertThat(executionTimes[2]).isIn(Range.closed(200L, 200 + TOLERANCE_MS))
+ assertThat(executionTimes[3]).isIn(Range.closed(300L, 300 + TOLERANCE_MS))
+ } cleanup {
+ visibleOnHandlerThread(handler) { scheduler.close() }
+ }
+ }
+
+ @Test
+ fun testMultipleSendDelayedMessages() {
+ val scheduler = RealtimeScheduler(handler)
+ tryTest {
+ val MSG_ID_0 = 0
+ val MSG_ID_1 = 1
+ val MSG_ID_2 = 2
+ val MSG_ID_3 = 3
+ val MSG_ID_4 = 4
+ val initialTimeMs = SystemClock.elapsedRealtime()
+ val executionTimes = mutableListOf<Long>()
+ val cv = ConditionVariable()
+ handler.post {
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_0, Pair(ConditionVariable(), executionTimes)), 0)
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_1, Pair(ConditionVariable(), executionTimes)),
+ 200)
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_4, Pair(ConditionVariable(), executionTimes)),
+ 250)
+ scheduler.removeDelayedMessage(MSG_ID_4)
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_2, Pair(ConditionVariable(), executionTimes)),
+ 100)
+ scheduler.sendDelayedMessage(
+ Message.obtain(handler, MSG_ID_3, Pair(cv, executionTimes)),
+ 300)
+ }
+ cv.block(TIMEOUT_MS)
+ assertEquals(4, executionTimes.size)
+ assertThat(executionTimes[0] - initialTimeMs).isIn(Range.closed(0L, TOLERANCE_MS))
+ assertThat(executionTimes[1] - initialTimeMs)
+ .isIn(Range.closed(100L, 100 + TOLERANCE_MS))
+ assertThat(executionTimes[2] - initialTimeMs)
+ .isIn(Range.closed(200L, 200 + TOLERANCE_MS))
+ assertThat(executionTimes[3] - initialTimeMs)
+ .isIn(Range.closed(300L, 300 + TOLERANCE_MS))
+ } cleanup {
+ visibleOnHandlerThread(handler) { scheduler.close() }
+ }
+ }
+}
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
deleted file mode 100644
index f5e47c9..0000000
--- a/staticlibs/tests/unit/src/com/android/net/module/util/TimerFileDescriptorTest.kt
+++ /dev/null
@@ -1,114 +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.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/testutils/Android.bp b/staticlibs/testutils/Android.bp
index 86aa8f1..ec486fb 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -93,6 +93,7 @@
libs: ["tradefed"],
test_suites: [
"ats",
+ "automotive-general-tests",
"device-tests",
"general-tests",
"cts",
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
index 8dc1bc4..bfbbc34 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkAgent.kt
@@ -14,19 +14,34 @@
* limitations under the License.
*/
-package com.android.testutils;
+package com.android.testutils
import android.content.Context
+import android.net.InetAddresses.parseNumericAddress
import android.net.KeepalivePacketData
+import android.net.LinkAddress
import android.net.LinkProperties
import android.net.NetworkAgent
import android.net.NetworkAgentConfig
import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_TEST
import android.net.NetworkProvider
+import android.net.NetworkRequest
import android.net.QosFilter
import android.net.Uri
import android.os.Looper
+import android.system.ErrnoException
+import android.system.Os
+import android.system.OsConstants
+import android.system.OsConstants.EADDRNOTAVAIL
+import android.system.OsConstants.ENETUNREACH
+import android.system.OsConstants.ENONET
+import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.SOCK_DGRAM
+import com.android.modules.utils.build.SdkLevel.isAtLeastS
import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.CompatUtil.makeTestNetworkSpecifier
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnBandwidthUpdateRequested
@@ -42,6 +57,8 @@
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnStopSocketKeepalive
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnUnregisterQosCallback
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
+import java.net.NetworkInterface
+import java.net.SocketException
import java.time.Duration
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@@ -65,6 +82,92 @@
conf: NetworkAgentConfig
) : NetworkAgent(context, looper, TestableNetworkAgent::class.java.simpleName /* tag */,
nc, lp, TEST_NETWORK_SCORE, conf, Provider(context, looper)) {
+ companion object {
+
+ /**
+ * Convenience method to create a [NetworkRequest] matching [TestableNetworkAgent]s from
+ * [createOnInterface].
+ */
+ fun makeNetworkRequestForInterface(ifaceName: String) = NetworkRequest.Builder()
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_TEST)
+ .setNetworkSpecifier(makeTestNetworkSpecifier(ifaceName))
+ .build()
+
+ /**
+ * Convenience method to initialize a [TestableNetworkAgent] on a given interface.
+ *
+ * This waits for link-local addresses to be setup and ensures LinkProperties are updated
+ * with the addresses.
+ */
+ fun createOnInterface(
+ context: Context,
+ looper: Looper,
+ ifaceName: String,
+ timeoutMs: Long
+ ): TestableNetworkAgent {
+ val lp = LinkProperties().apply {
+ interfaceName = ifaceName
+ }
+ val agent = TestableNetworkAgent(
+ context,
+ looper,
+ NetworkCapabilities().apply {
+ removeCapability(NET_CAPABILITY_TRUSTED)
+ addTransportType(TRANSPORT_TEST)
+ setNetworkSpecifier(makeTestNetworkSpecifier(ifaceName))
+ },
+ lp,
+ NetworkAgentConfig.Builder().build()
+ )
+ val network = agent.register()
+ agent.markConnected()
+ if (isAtLeastS()) {
+ // OnNetworkCreated was added in S
+ agent.eventuallyExpect<OnNetworkCreated>()
+ }
+
+ // Wait until the link-local address can be used. Address flags are not available
+ // without elevated permissions, so check that bindSocket works.
+ assertEventuallyTrue("No usable v6 address after $timeoutMs ms", timeoutMs) {
+ // To avoid race condition between socket connection succeeding and interface
+ // returning a non-empty address list. Verify that interface returns a non-empty
+ // list, before trying the socket connection.
+ if (NetworkInterface.getByName(ifaceName).interfaceAddresses.isEmpty()) {
+ return@assertEventuallyTrue false
+ }
+
+ val sock = Os.socket(OsConstants.AF_INET6, SOCK_DGRAM, IPPROTO_UDP)
+ tryTest {
+ network.bindSocket(sock)
+ Os.connect(sock, parseNumericAddress("ff02::fb%$ifaceName"), 12345)
+ true
+ }.catch<ErrnoException> {
+ if (it.errno != ENETUNREACH && it.errno != EADDRNOTAVAIL) {
+ throw it
+ }
+ false
+ }.catch<SocketException> {
+ // OnNetworkCreated does not exist on R, so a SocketException caused by ENONET
+ // may be seen before the network is created
+ if (isAtLeastS()) throw it
+ val cause = it.cause as? ErrnoException ?: throw it
+ if (cause.errno != ENONET) {
+ throw it
+ }
+ false
+ } cleanup {
+ Os.close(sock)
+ }
+ }
+
+ agent.lp.setLinkAddresses(NetworkInterface.getByName(ifaceName).interfaceAddresses.map {
+ LinkAddress(it.address, it.networkPrefixLength.toInt())
+ })
+ agent.sendLinkProperties(agent.lp)
+ return agent
+ }
+ }
val DEFAULT_TIMEOUT_MS = 5000L
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/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/Android.bp b/tests/cts/multidevices/Android.bp
index 949be85..a082a95 100644
--- a/tests/cts/multidevices/Android.bp
+++ b/tests/cts/multidevices/Android.bp
@@ -22,6 +22,7 @@
main: "run_tests.py",
srcs: [
"apfv4_test.py",
+ "apfv6_test.py",
"connectivity_multi_devices_test.py",
"run_tests.py",
],
diff --git a/tests/cts/multidevices/apfv4_test.py b/tests/cts/multidevices/apfv4_test.py
index 7795be5..aa535fd 100644
--- a/tests/cts/multidevices/apfv4_test.py
+++ b/tests/cts/multidevices/apfv4_test.py
@@ -53,7 +53,7 @@
) # Declare inputs for state_str and expected_result.
def test_apf_drop_ethertype_not_allowed(self, blocked_ether_type):
# Ethernet header (14 bytes).
- packet = ETHER_BROADCAST_ADDR # Destination MAC (broadcast)
+ packet = self.client_mac_address.replace(":", "") # Destination MAC
packet += self.server_mac_address.replace(":", "") # Source MAC
packet += blocked_ether_type
diff --git a/tests/cts/multidevices/apfv6_test.py b/tests/cts/multidevices/apfv6_test.py
new file mode 100644
index 0000000..fc732d2
--- /dev/null
+++ b/tests/cts/multidevices/apfv6_test.py
@@ -0,0 +1,84 @@
+# 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.
+
+from mobly import asserts
+from net_tests_utils.host.python import apf_test_base, apf_utils, adb_utils, assert_utils, packet_utils
+
+APFV6_VERSION = 6000
+ARP_OFFLOAD_REPLY_LEN = 60
+
+class ApfV6Test(apf_test_base.ApfTestBase):
+ def setup_class(self):
+ super().setup_class()
+
+ # Skip tests for APF version < 6000
+ apf_utils.assume_apf_version_support_at_least(
+ self.clientDevice, self.client_iface_name, APFV6_VERSION
+ )
+
+ def teardown_class(self):
+ # force to stop capture on the server device if any test case failed
+ try:
+ apf_utils.stop_capture_packets(self.serverDevice, self.server_iface_name)
+ except assert_utils.UnexpectedBehaviorError:
+ pass
+ super().teardown_class()
+
+ def test_unicast_arp_request_offload(self):
+ arp_request = packet_utils.construct_arp_packet(
+ src_mac=self.server_mac_address,
+ dst_mac=self.client_mac_address,
+ src_ip=self.server_ipv4_addresses[0],
+ dst_ip=self.client_ipv4_addresses[0],
+ op=packet_utils.ARP_REQUEST_OP
+ )
+
+ arp_reply = packet_utils.construct_arp_packet(
+ src_mac=self.client_mac_address,
+ dst_mac=self.server_mac_address,
+ src_ip=self.client_ipv4_addresses[0],
+ dst_ip=self.server_ipv4_addresses[0],
+ op=packet_utils.ARP_REPLY_OP
+ )
+
+ # Add zero padding up to 60 bytes, since APFv6 ARP offload always sent out 60 bytes reply
+ arp_reply = arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
+
+ self.send_packet_and_expect_reply_received(
+ arp_request, "DROPPED_ARP_REQUEST_REPLIED", arp_reply
+ )
+
+ def test_broadcast_arp_request_offload(self):
+ arp_request = packet_utils.construct_arp_packet(
+ src_mac=self.server_mac_address,
+ dst_mac=packet_utils.ETHER_BROADCAST_MAC_ADDRESS,
+ src_ip=self.server_ipv4_addresses[0],
+ dst_ip=self.client_ipv4_addresses[0],
+ op=packet_utils.ARP_REQUEST_OP
+ )
+
+ arp_reply = packet_utils.construct_arp_packet(
+ src_mac=self.client_mac_address,
+ dst_mac=self.server_mac_address,
+ src_ip=self.client_ipv4_addresses[0],
+ dst_ip=self.server_ipv4_addresses[0],
+ op=packet_utils.ARP_REPLY_OP
+ )
+
+ # Add zero padding up to 60 bytes, since APFv6 ARP offload always sent out 60 bytes reply
+ arp_reply = arp_reply.ljust(ARP_OFFLOAD_REPLY_LEN * 2, "0")
+
+ self.send_packet_and_expect_reply_received(
+ arp_request, "DROPPED_ARP_REQUEST_REPLIED", arp_reply
+ )
diff --git a/tests/cts/multidevices/run_tests.py b/tests/cts/multidevices/run_tests.py
index 1391d13..a0d0bec 100644
--- a/tests/cts/multidevices/run_tests.py
+++ b/tests/cts/multidevices/run_tests.py
@@ -16,6 +16,7 @@
import sys
from apfv4_test import ApfV4Test
+from apfv6_test import ApfV6Test
from connectivity_multi_devices_test import ConnectivityMultiDevicesTest
from mobly import suite_runner
@@ -35,4 +36,4 @@
index = sys.argv.index("--")
sys.argv = sys.argv[:1] + sys.argv[index + 1 :]
# TODO: make the tests can be executed without manually list classes.
- suite_runner.run_suite([ConnectivityMultiDevicesTest, ApfV4Test], sys.argv)
+ suite_runner.run_suite([ConnectivityMultiDevicesTest, ApfV4Test, ApfV6Test], sys.argv)
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index a9ac29c..1ba581a 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -95,6 +95,7 @@
"NetworkStackApiCurrentShims",
],
test_suites: [
+ "automotive-general-tests",
"cts",
"mts-tethering",
"mcts-tethering",
@@ -160,6 +161,7 @@
min_sdk_version: "30",
// Tag this module as a cts test artifact
test_suites: [
+ "automotive-general-tests",
"cts",
"general-tests",
],
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/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 320622b..3ab6c0d 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -21,8 +21,8 @@
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_LEANBACK
import android.content.pm.PackageManager.FEATURE_WIFI
import android.net.ConnectivityManager
import android.net.Network
@@ -38,7 +38,7 @@
import android.net.apf.ApfConstants.IPV6_NEXT_HEADER_OFFSET
import android.net.apf.ApfConstants.IPV6_SRC_ADDR_OFFSET
import android.net.apf.ApfCounterTracker
-import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MULTICAST_PING
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_REPLIED_NON_DAD
import android.net.apf.ApfCounterTracker.Counter.FILTER_AGE_16384THS
import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_ICMP
import android.net.apf.ApfV4Generator
@@ -60,6 +60,8 @@
import android.system.OsConstants
import android.system.OsConstants.AF_INET6
import android.system.OsConstants.ETH_P_IPV6
+import android.system.OsConstants.ICMP6_ECHO_REPLY
+import android.system.OsConstants.ICMP6_ECHO_REQUEST
import android.system.OsConstants.IPPROTO_ICMPV6
import android.system.OsConstants.SOCK_DGRAM
import android.system.OsConstants.SOCK_NONBLOCK
@@ -104,6 +106,7 @@
import kotlin.test.assertNotNull
import org.junit.After
import org.junit.AfterClass
+import org.junit.Assume.assumeFalse
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule
@@ -170,8 +173,8 @@
private fun isAutomotiveWithVisibleBackgroundUser(): Boolean {
val packageManager = context.getPackageManager()
val userManager = context.getSystemService(UserManager::class.java)!!
- return (packageManager.hasSystemFeature(FEATURE_AUTOMOTIVE)
- && userManager.isVisibleBackgroundUsersSupported)
+ return (packageManager.hasSystemFeature(FEATURE_AUTOMOTIVE) &&
+ userManager.isVisibleBackgroundUsersSupported)
}
@BeforeClass
@@ -211,8 +214,13 @@
handler: Handler,
private val network: Network
) : PacketReader(handler, RCV_BUFFER_SIZE) {
+ private data class PingContext(
+ val futureReply: CompletableFuture<List<ByteArray>>,
+ val expectReplyCount: Int,
+ val replyPayloads: MutableList<ByteArray> = mutableListOf()
+ )
private var sockFd: FileDescriptor? = null
- private var futureReply: CompletableFuture<ByteArray>? = null
+ private var pingContext: PingContext? = null
override fun createFd(): FileDescriptor {
// sockFd is closed by calling super.stop()
@@ -224,6 +232,8 @@
}
override fun handlePacket(recvbuf: ByteArray, length: Int) {
+ val context = pingContext ?: return
+
// If zero-length or Type is not echo reply: ignore.
if (length == 0 || recvbuf[0] != 0x81.toByte()) {
return
@@ -231,10 +241,14 @@
// Only copy the ping data and complete the future.
val result = recvbuf.sliceArray(8..<length)
Log.i(TAG, "Received ping reply: ${result.toHexString()}")
- futureReply!!.complete(recvbuf.sliceArray(8..<length))
+ context.replyPayloads.add(recvbuf.sliceArray(8..<length))
+ if (context.replyPayloads.size == context.expectReplyCount) {
+ context.futureReply.complete(context.replyPayloads)
+ pingContext = null
+ }
}
- fun sendPing(data: ByteArray, payloadSize: Int) {
+ fun sendPing(data: ByteArray, payloadSize: Int, expectReplyCount: Int = 1) {
require(data.size == payloadSize)
// rfc4443#section-4.1: Echo Request Message
@@ -250,17 +264,20 @@
val icmp6Header = byteArrayOf(0x80.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
val packet = icmp6Header + data
Log.i(TAG, "Sent ping: ${packet.toHexString()}")
- futureReply = CompletableFuture<ByteArray>()
+ pingContext = PingContext(
+ futureReply = CompletableFuture<List<ByteArray>>(),
+ expectReplyCount = expectReplyCount
+ )
Os.sendto(sockFd!!, packet, 0, packet.size, 0, PING_DESTINATION)
}
- fun expectPingReply(timeoutMs: Long = TIMEOUT_MS): ByteArray {
- return futureReply!!.get(timeoutMs, TimeUnit.MILLISECONDS)
+ fun expectPingReply(timeoutMs: Long = TIMEOUT_MS): List<ByteArray> {
+ return pingContext!!.futureReply.get(timeoutMs, TimeUnit.MILLISECONDS)
}
fun expectPingDropped() {
assertFailsWith(TimeoutException::class) {
- futureReply!!.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ pingContext!!.futureReply.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
}
}
@@ -299,10 +316,23 @@
return ApfCapabilities(version, maxLen, packetFormat)
}
+ private fun isTvDeviceSupportFullNetworkingUnder2w(): Boolean {
+ return (pm.hasSystemFeature(FEATURE_LEANBACK) &&
+ pm.hasSystemFeature("com.google.android.tv.full_networking_under_2w"))
+ }
+
@Before
fun setUp() {
assume().that(pm.hasSystemFeature(FEATURE_WIFI)).isTrue()
+ // Based on GTVS-16, Android Packet Filtering (APF) is OPTIONAL for devices that fully
+ // process all network packets on CPU at all times, even in standby, while meeting
+ // the <= 2W standby power demand requirement.
+ assumeFalse(
+ "Skipping test: TV device process full networking on CPU under 2W",
+ isTvDeviceSupportFullNetworkingUnder2w()
+ )
+
networkCallback = TestableNetworkCallback()
cm.requestNetwork(
NetworkRequest.Builder()
@@ -349,10 +379,8 @@
@Test
fun testApfCapabilities() {
// APF became mandatory in Android 14 VSR.
- assume().that(getVsrApiLevel()).isAtLeast(34)
-
- // ApfFilter does not support anything but ARPHRD_ETHER.
- assertThat(caps.apfPacketFormat).isEqualTo(OsConstants.ARPHRD_ETHER)
+ val vsrApiLevel = getVsrApiLevel()
+ assume().that(vsrApiLevel).isAtLeast(34)
// DEVICEs launching with Android 14 with CHIPSETs that set ro.board.first_api_level to 34:
// - [GMS-VSR-5.3.12-003] MUST return 4 or higher as the APF version number from calls to
@@ -372,9 +400,22 @@
// ro.board.first_api_level or ro.board.api_level to 202404 or higher:
// - [GMS-VSR-5.3.12-009] MUST indicate at least 2048 bytes of usable memory from calls to
// the getApfPacketFilterCapabilities HAL method.
- if (getVsrApiLevel() >= 202404) {
+ if (vsrApiLevel >= 202404) {
assertThat(caps.maximumApfProgramSize).isAtLeast(2048)
}
+
+ // CHIPSETs (or DEVICES with CHIPSETs) that set ro.board.first_api_level or
+ // ro.board.api_level to 202504 or higher:
+ // - [VSR-5.3.12-018] MUST implement version 6 of the Android Packet Filtering (APF)
+ // interpreter in the Wi-Fi firmware.
+ // - [VSR-5.3.12-019] MUST provide at least 4000 bytes of APF RAM.
+ if (vsrApiLevel >= 202504) {
+ assertThat(caps.apfVersionSupported).isEqualTo(6000)
+ assertThat(caps.maximumApfProgramSize).isAtLeast(4000)
+ }
+
+ // ApfFilter does not support anything but ARPHRD_ETHER.
+ assertThat(caps.apfPacketFormat).isEqualTo(OsConstants.ARPHRD_ETHER)
}
// APF is backwards compatible, i.e. a v6 interpreter supports both v2 and v4 functionality.
@@ -478,7 +519,7 @@
}
val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
packetReader.sendPing(data, payloadSize)
- assertThat(packetReader.expectPingReply()).isEqualTo(data)
+ assertThat(packetReader.expectPingReply()[0]).isEqualTo(data)
// Generate an APF program that drops the next ping
val gen = ApfV4Generator(
@@ -690,69 +731,76 @@
// increase PASSED_IPV6_ICMP counter
// pass
// else
- // transmit a ICMPv6 echo request packet with the first byte of the payload in the reply
- // increase DROPPED_IPV6_MULTICAST_PING counter
+ // transmit 3 ICMPv6 echo requests with random first byte
+ // increase DROPPED_IPV6_NS_REPLIED_NON_DAD counter
// drop
- val program = gen
- .addLoad16(R0, ETH_ETHERTYPE_OFFSET)
+ gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET)
.addJumpIfR0NotEquals(ETH_P_IPV6.toLong(), skipPacketLabel)
.addLoad8(R0, IPV6_NEXT_HEADER_OFFSET)
.addJumpIfR0NotEquals(IPPROTO_ICMPV6.toLong(), skipPacketLabel)
.addLoad8(R0, ICMP6_TYPE_OFFSET)
- .addJumpIfR0NotEquals(0x81, skipPacketLabel) // Echo reply type
+ .addJumpIfR0NotEquals(ICMP6_ECHO_REPLY.toLong(), skipPacketLabel)
.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
.addCountAndPassIfR0Equals(
- (ETHER_HEADER_LEN + IPV6_HEADER_LEN + PING_HEADER_LENGTH + firstByte.size)
- .toLong(),
- PASSED_IPV6_ICMP
+ (ETHER_HEADER_LEN + IPV6_HEADER_LEN + PING_HEADER_LENGTH + firstByte.size)
+ .toLong(),
+ PASSED_IPV6_ICMP
)
- // Ping Packet Generation
- .addAllocate(pingRequestPktLen)
- // Eth header
- .addPacketCopy(ETHER_SRC_ADDR_OFFSET, ETHER_ADDR_LEN) // dst MAC address
- .addPacketCopy(ETHER_DST_ADDR_OFFSET, ETHER_ADDR_LEN) // src MAC address
- .addWriteU16(ETH_P_IPV6) // IPv6 type
- // IPv6 Header
- .addWrite32(0x60000000) // IPv6 Header: version, traffic class, flowlabel
- // payload length (2 bytes) | next header: ICMPv6 (1 byte) | hop limit (1 byte)
- .addWrite32(pingRequestIpv6PayloadLen shl 16 or (IPPROTO_ICMPV6 shl 8 or 64))
- .addPacketCopy(IPV6_DEST_ADDR_OFFSET, IPV6_ADDR_LEN) // src ip
- .addPacketCopy(IPV6_SRC_ADDR_OFFSET, IPV6_ADDR_LEN) // dst ip
- // ICMPv6
- .addWriteU8(0x80) // type: echo request
- .addWriteU8(0) // code
- .addWriteU16(pingRequestIpv6PayloadLen) // checksum
- // identifier
- .addPacketCopy(ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_HEADER_MIN_LEN, 2)
- .addWriteU16(0) // sequence number
- .addDataCopy(firstByte) // data
- .addTransmitL4(
+
+ val numOfPacketToTransmit = 3
+ val expectReplyPayloads = (0 until numOfPacketToTransmit).map { Random.nextBytes(1) }
+ expectReplyPayloads.forEach { replyPingPayload ->
+ // Ping Packet Generation
+ gen.addAllocate(pingRequestPktLen)
+ // Eth header
+ .addPacketCopy(ETHER_SRC_ADDR_OFFSET, ETHER_ADDR_LEN) // dst MAC address
+ .addPacketCopy(ETHER_DST_ADDR_OFFSET, ETHER_ADDR_LEN) // src MAC address
+ .addWriteU16(ETH_P_IPV6) // IPv6 type
+ // IPv6 Header
+ .addWrite32(0x60000000) // IPv6 Header: version, traffic class, flowlabel
+ // payload length (2 bytes) | next header: ICMPv6 (1 byte) | hop limit (1 byte)
+ .addWrite32(pingRequestIpv6PayloadLen shl 16 or (IPPROTO_ICMPV6 shl 8 or 64))
+ .addPacketCopy(IPV6_DEST_ADDR_OFFSET, IPV6_ADDR_LEN) // src ip
+ .addPacketCopy(IPV6_SRC_ADDR_OFFSET, IPV6_ADDR_LEN) // dst ip
+ // ICMPv6
+ .addWriteU8(ICMP6_ECHO_REQUEST)
+ .addWriteU8(0) // code
+ .addWriteU16(pingRequestIpv6PayloadLen) // checksum
+ // identifier
+ .addPacketCopy(ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_HEADER_MIN_LEN, 2)
+ .addWriteU16(0) // sequence number
+ .addDataCopy(replyPingPayload) // data
+ .addTransmitL4(
ETHER_HEADER_LEN, // ip_ofs
ICMP6_CHECKSUM_OFFSET, // csum_ofs
IPV6_SRC_ADDR_OFFSET, // csum_start
IPPROTO_ICMPV6, // partial_sum
false // udp
- )
- // Warning: the program abuse DROPPED_IPV6_MULTICAST_PING for debugging purpose
- .addCountAndDrop(DROPPED_IPV6_MULTICAST_PING)
- .defineLabel(skipPacketLabel)
- .addPass()
- .generate()
+ )
+ }
+ // Warning: the program abuse DROPPED_IPV6_NS_REPLIED_NON_DAD for debugging purpose
+ gen.addCountAndDrop(DROPPED_IPV6_NS_REPLIED_NON_DAD)
+ .defineLabel(skipPacketLabel)
+ .addPass()
+
+ val program = gen.generate()
installAndVerifyProgram(program)
- packetReader.sendPing(payload, payloadSize)
-
- val replyPayload = try {
+ packetReader.sendPing(payload, payloadSize, expectReplyCount = numOfPacketToTransmit)
+ val replyPayloads = try {
packetReader.expectPingReply(TIMEOUT_MS * 2)
} catch (e: TimeoutException) {
- byteArrayOf() // Empty payload if timeout occurs
+ emptyList()
}
val apfCounterTracker = ApfCounterTracker()
apfCounterTracker.updateCountersFromData(readProgram())
Log.i(TAG, "counter map: ${apfCounterTracker.counters}")
- assertThat(replyPayload).isEqualTo(firstByte)
+ assertThat(replyPayloads.size).isEqualTo(expectReplyPayloads.size)
+ for (i in replyPayloads.indices) {
+ assertThat(replyPayloads[i]).isEqualTo(expectReplyPayloads[i])
+ }
}
}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index feb4621..9457a42 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -1615,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);
@@ -1628,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,
@@ -2429,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.
@@ -2940,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/DnsResolverTapTest.kt b/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt
new file mode 100644
index 0000000..ff608f2
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTapTest.kt
@@ -0,0 +1,185 @@
+/*
+ * 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.cts
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.net.DnsResolver
+import android.net.InetAddresses.parseNumericAddress
+import android.net.IpPrefix
+import android.net.MacAddress
+import android.net.RouteInfo
+import android.os.CancellationSignal
+import android.os.HandlerThread
+import android.os.SystemClock
+import android.platform.test.annotations.AppModeFull
+import android.provider.DeviceConfig
+import android.provider.DeviceConfig.NAMESPACE_NETD_NATIVE
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN
+import com.android.testutils.AutoReleaseNetworkCallbackRule
+import com.android.testutils.DeviceConfigRule
+import com.android.testutils.DnsResolverModuleTest
+import com.android.testutils.IPv6UdpFilter
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RouterAdvertisementResponder
+import com.android.testutils.TapPacketReaderRule
+import com.android.testutils.TestableNetworkAgent
+import com.android.testutils.TestDnsPacket
+import com.android.testutils.com.android.testutils.SetFeatureFlagsRule
+import com.android.testutils.runAsShell
+import java.net.Inet6Address
+import java.net.InetAddress
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private val TEST_DNSSERVER_MAC = MacAddress.fromString("00:11:22:33:44:55")
+private val TAG = DnsResolverTapTest::class.java.simpleName
+private const val TEST_TIMEOUT_MS = 10_000L
+
+@AppModeFull(reason = "Test networks cannot be created in instant app mode")
+@DnsResolverModuleTest
+@RunWith(AndroidJUnit4::class)
+class DnsResolverTapTest {
+ private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+ private val handlerThread = HandlerThread(TAG)
+
+ @get:Rule(order = 1)
+ val deviceConfigRule = DeviceConfigRule()
+
+ @get:Rule(order = 2)
+ val featureFlagsRule = SetFeatureFlagsRule(
+ setFlagsMethod = { name, enabled ->
+ val value = when (enabled) {
+ null -> null
+ true -> "1"
+ false -> "0"
+ }
+ deviceConfigRule.setConfig(NAMESPACE_NETD_NATIVE, name, value)
+ },
+ getFlagsMethod = {
+ runAsShell(READ_DEVICE_CONFIG) {
+ DeviceConfig.getInt(NAMESPACE_NETD_NATIVE, it, 0) == 1
+ }
+ }
+ )
+
+ @get:Rule(order = 3)
+ val packetReaderRule = TapPacketReaderRule()
+
+ @get:Rule(order = 4)
+ val cbRule = AutoReleaseNetworkCallbackRule()
+
+ private val ndResponder by lazy { RouterAdvertisementResponder(packetReaderRule.reader) }
+ private val dnsServerAddr by lazy {
+ parseNumericAddress("fe80::124%${packetReaderRule.iface.interfaceName}") as Inet6Address
+ }
+ private lateinit var agent: TestableNetworkAgent
+
+ @Before
+ fun setUp() {
+ handlerThread.start()
+ val interfaceName = packetReaderRule.iface.interfaceName
+ val cb = cbRule.requestNetwork(TestableNetworkAgent.makeNetworkRequestForInterface(
+ interfaceName))
+ agent = runAsShell(MANAGE_TEST_NETWORKS) {
+ TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+ interfaceName, TEST_TIMEOUT_MS)
+ }
+ ndResponder.addNeighborEntry(TEST_DNSSERVER_MAC, dnsServerAddr)
+ ndResponder.start()
+ agent.lp.apply {
+ addDnsServer(dnsServerAddr)
+ // A default route is needed for DnsResolver.java to send queries over IPv6
+ // (see usage of DnsUtils.haveIpv6).
+ addRoute(RouteInfo(IpPrefix("::/0"), null, null))
+ }
+ agent.sendLinkProperties(agent.lp)
+ cb.eventuallyExpect<LinkPropertiesChanged> { it.lp.dnsServers.isNotEmpty() }
+ }
+
+ @After
+ fun tearDown() {
+ ndResponder.stop()
+ if (::agent.isInitialized) {
+ agent.unregister()
+ }
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ private class DnsCallback : DnsResolver.Callback<List<InetAddress>> {
+ override fun onAnswer(answer: List<InetAddress>, rcode: Int) = Unit
+ override fun onError(error: DnsResolver.DnsException) = Unit
+ }
+
+ /**
+ * Run a cancellation test.
+ *
+ * @param domain Domain name to query
+ * @param waitTimeForNoRetryAfterCancellationMs If positive, cancel the query and wait for that
+ * delay to check no retry is sent.
+ * @return The duration it took to receive all expected replies.
+ */
+ fun doCancellationTest(domain: String, waitTimeForNoRetryAfterCancellationMs: Long): Long {
+ val cancellationSignal = CancellationSignal()
+ val dnsCb = DnsCallback()
+ val queryStart = SystemClock.elapsedRealtime()
+ DnsResolver.getInstance().query(
+ agent.network, domain, 0 /* flags */,
+ Runnable::run /* executor */, cancellationSignal, dnsCb
+ )
+
+ if (waitTimeForNoRetryAfterCancellationMs > 0) {
+ cancellationSignal.cancel()
+ }
+ // Filter for queries on UDP port 53 for the specified domain
+ val filter = IPv6UdpFilter(dstPort = 53).and {
+ TestDnsPacket(
+ it.copyOfRange(ETHER_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN, it.size),
+ dstAddr = dnsServerAddr
+ ).isQueryFor(domain, DnsResolver.TYPE_AAAA)
+ }
+
+ val reader = packetReaderRule.reader
+ assertNotNull(reader.poll(TEST_TIMEOUT_MS, filter), "Original query not found")
+ if (waitTimeForNoRetryAfterCancellationMs > 0) {
+ assertNull(reader.poll(waitTimeForNoRetryAfterCancellationMs, filter),
+ "Expected no retry query")
+ } else {
+ assertNotNull(reader.poll(TEST_TIMEOUT_MS, filter), "Retry query not found")
+ }
+ return SystemClock.elapsedRealtime() - queryStart
+ }
+
+ @SetFeatureFlagsRule.FeatureFlag("no_retry_after_cancel", true)
+ @Test
+ fun testCancellation() {
+ val timeWithRetryWhenNotCancelled = doCancellationTest("test1.example.com",
+ waitTimeForNoRetryAfterCancellationMs = 0L)
+ doCancellationTest("test2.example.com",
+ waitTimeForNoRetryAfterCancellationMs = timeWithRetryWhenNotCancelled + 50L)
+ }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt
new file mode 100644
index 0000000..484cce8
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2025 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.cts
+
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_ANY
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_NONE
+import android.net.L2capNetworkSpecifier.PSM_ANY
+import android.net.L2capNetworkSpecifier.ROLE_CLIENT
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
+import android.net.MacAddress
+import android.os.Build
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertParcelingIsLossless
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class L2capNetworkSpecifierTest {
+ @Test
+ fun testParcelUnparcel() {
+ val remoteMac = MacAddress.fromString("01:02:03:04:05:06")
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .setPsm(42)
+ .setRemoteAddress(remoteMac)
+ .build()
+ assertParcelingIsLossless(specifier)
+ }
+
+ @Test
+ fun testGetters() {
+ val remoteMac = MacAddress.fromString("11:22:33:44:55:66")
+ val specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setPsm(123)
+ .setRemoteAddress(remoteMac)
+ .build()
+ assertEquals(ROLE_CLIENT, specifier.getRole())
+ assertEquals(HEADER_COMPRESSION_NONE, specifier.getHeaderCompression())
+ assertEquals(123, specifier.getPsm())
+ assertEquals(remoteMac, specifier.getRemoteAddress())
+ }
+
+ @Test
+ fun testCanBeSatisfiedBy() {
+ val blanketOffer = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_ANY)
+ .setPsm(PSM_ANY)
+ .build()
+
+ val reservedOffer = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .setPsm(42)
+ .build()
+
+ val clientOffer = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_ANY)
+ .build()
+
+ val serverReservation = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+
+ assertTrue(serverReservation.canBeSatisfiedBy(blanketOffer))
+ assertTrue(serverReservation.canBeSatisfiedBy(reservedOffer))
+ // Note: serverReservation can be filed using reserveNetwork, or it could be a regular
+ // request filed using requestNetwork.
+ assertFalse(serverReservation.canBeSatisfiedBy(clientOffer))
+
+ val clientRequest = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .setRemoteAddress(MacAddress.fromString("00:01:02:03:04:05"))
+ .setPsm(42)
+ .build()
+
+ assertTrue(clientRequest.canBeSatisfiedBy(clientOffer))
+ // Note: the BlanketOffer also includes a RES_ID_MATCH_ALL_RESERVATIONS. Since the
+ // clientRequest is not a reservation, it won't match that request to begin with.
+ assertFalse(clientRequest.canBeSatisfiedBy(blanketOffer))
+ assertFalse(clientRequest.canBeSatisfiedBy(reservedOffer))
+
+ val matchAny = L2capNetworkSpecifier.Builder().build()
+ assertTrue(matchAny.canBeSatisfiedBy(blanketOffer))
+ assertTrue(matchAny.canBeSatisfiedBy(reservedOffer))
+ assertTrue(matchAny.canBeSatisfiedBy(clientOffer))
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 2226f4c..2fb140a 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -33,6 +33,7 @@
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.fail;
@@ -42,6 +43,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;
@@ -102,6 +104,16 @@
}
}
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
+ public void testParceling() {
+ NetworkCapabilities nc = new NetworkCapabilities.Builder().build();
+ NetworkRequest request = new NetworkRequest(nc, TYPE_NONE, 42 /* rId */,
+ NetworkRequest.Type.RESERVATION);
+
+ assertParcelingIsLossless(request);
+ }
+
@Test
public void testCapabilities() {
assertTrue(new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build()
@@ -583,4 +595,15 @@
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/NetworkReservationTest.kt b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
new file mode 100644
index 0000000..f05bf15
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkReservationTest.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2025 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.cts
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.net.ConnectivityManager
+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.RES_ID_MATCH_ALL_RESERVATIONS
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkProvider
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.platform.test.annotations.AppModeFull
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+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.runAsShell
+import kotlin.test.assertEquals
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TAG = "NetworkReservationTest"
+
+private val NETWORK_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
+
+// TODO: integrate with CSNetworkReservationTest and move to common tests.
+@AppModeFull(reason = "CHANGE_NETWORK_STATE, MANAGE_TEST_NETWORKS not grantable to instant apps")
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class NetworkReservationTest {
+ private val context = InstrumentationRegistry.getInstrumentation().context
+ private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+ private val handlerThread = HandlerThread("$TAG handler thread").apply { start() }
+ private val handler = Handler(handlerThread.looper)
+ private val provider = NetworkProvider(context, handlerThread.looper, TAG)
+
+ @Before
+ fun setUp() {
+ runAsShell(NETWORK_SETTINGS) {
+ cm.registerNetworkProvider(provider)
+ }
+ }
+
+ @After
+ fun tearDown() {
+ runAsShell(NETWORK_SETTINGS) {
+ // unregisterNetworkProvider unregisters all associated NetworkOffers.
+ cm.unregisterNetworkProvider(provider)
+ }
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ fun NetworkCapabilities.copyWithReservationId(resId: Int) = NetworkCapabilities(this).also {
+ it.reservationId = resId
+ }
+
+ @Test
+ fun testReserveNetwork() {
+ // register blanket offer
+ val blanketOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ provider.registerNetworkOffer(NETWORK_SCORE, BLANKET_CAPS, handler::post, blanketOffer)
+ }
+
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, handler, 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 reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+ val reservedOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ provider.registerNetworkOffer(NETWORK_SCORE, reservedCaps, handler::post, reservedOffer)
+ }
+
+ // validate onReserved was sent to the app
+ val appObservedCaps = cb.expect<Reserved>().caps
+ assertEquals(reservedCaps, appObservedCaps)
+
+ // validate the reservation matches the reserved offer.
+ reservedOffer.expectOnNetworkNeeded(reservedCaps)
+
+ // reserved offer goes away
+ provider.unregisterNetworkOffer(reservedOffer)
+ cb.expect<Unavailable>()
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index 005f6ad..eb2dbf7 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -96,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;
@@ -712,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) {
@@ -743,21 +775,12 @@
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
@@ -767,69 +790,88 @@
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.");
@@ -902,7 +944,7 @@
}
}
- private String tagToString(Integer tag) {
+ private static String tagToString(Integer tag) {
if (tag == null) return "null";
switch (tag) {
case TAG_NONE:
@@ -912,7 +954,7 @@
}
}
- private String stateToString(Integer state) {
+ private static String stateToString(Integer state) {
if (state == null) return "null";
switch (state) {
case STATE_ALL:
@@ -925,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;
@@ -935,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);
@@ -951,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/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 7fc8863..ee31f1a 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -22,14 +22,10 @@
import android.net.ConnectivityManager.NetworkCallback
import android.net.DnsResolver
import android.net.InetAddresses.parseNumericAddress
-import android.net.LinkAddress
-import android.net.LinkProperties
import android.net.LocalSocket
import android.net.LocalSocketAddress
import android.net.MacAddress
import android.net.Network
-import android.net.NetworkAgentConfig
-import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
@@ -40,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
@@ -50,16 +49,10 @@
import android.os.HandlerThread
import android.platform.test.annotations.AppModeFull
import android.provider.DeviceConfig.NAMESPACE_TETHERING
-import android.system.ErrnoException
-import android.system.Os
-import android.system.OsConstants.AF_INET6
-import android.system.OsConstants.EADDRNOTAVAIL
-import android.system.OsConstants.ENETUNREACH
import android.system.OsConstants.ETH_P_IPV6
import android.system.OsConstants.IPPROTO_IPV6
import android.system.OsConstants.IPPROTO_UDP
import android.system.OsConstants.RT_SCOPE_LINK
-import android.system.OsConstants.SOCK_DGRAM
import android.util.Log
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
@@ -98,12 +91,11 @@
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.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.assertEmpty
import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
@@ -244,16 +236,12 @@
val tnm = context.getSystemService(TestNetworkManager::class.java)!!
val iface = tnm.createTapInterface()
val cb = TestableNetworkCallback()
- val testNetworkSpecifier = TestNetworkSpecifier(iface.interfaceName)
cm.requestNetwork(
- NetworkRequest.Builder()
- .removeCapability(NET_CAPABILITY_TRUSTED)
- .addTransportType(TRANSPORT_TEST)
- .setNetworkSpecifier(testNetworkSpecifier)
- .build(),
+ TestableNetworkAgent.makeNetworkRequestForInterface(iface.interfaceName),
cb
)
- val agent = registerTestNetworkAgent(iface.interfaceName)
+ val agent = TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+ iface.interfaceName, TIMEOUT_MS)
val network = agent.network ?: fail("Registered agent should have a network")
cb.eventuallyExpect<LinkPropertiesChanged>(TIMEOUT_MS) {
@@ -268,57 +256,6 @@
return TestTapNetwork(iface, cb, agent, network)
}
- private fun registerTestNetworkAgent(ifaceName: String): TestableNetworkAgent {
- val lp = LinkProperties().apply {
- interfaceName = ifaceName
- }
- val agent = TestableNetworkAgent(
- context,
- handlerThread.looper,
- NetworkCapabilities().apply {
- removeCapability(NET_CAPABILITY_TRUSTED)
- addTransportType(TRANSPORT_TEST)
- setNetworkSpecifier(TestNetworkSpecifier(ifaceName))
- },
- lp,
- NetworkAgentConfig.Builder().build()
- )
- val network = agent.register()
- agent.markConnected()
- agent.expectCallback<OnNetworkCreated>()
-
- // Wait until the link-local address can be used. Address flags are not available without
- // elevated permissions, so check that bindSocket works.
- PollingCheck.check("No usable v6 address on interface after $TIMEOUT_MS ms", TIMEOUT_MS) {
- // To avoid race condition between socket connection succeeding and interface returning
- // a non-empty address list. Verify that interface returns a non-empty list, before
- // trying the socket connection.
- if (NetworkInterface.getByName(ifaceName).interfaceAddresses.isEmpty()) {
- return@check false
- }
-
- val sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP)
- tryTest {
- network.bindSocket(sock)
- Os.connect(sock, parseNumericAddress("ff02::fb%$ifaceName"), 12345)
- true
- }.catch<ErrnoException> {
- if (it.errno != ENETUNREACH && it.errno != EADDRNOTAVAIL) {
- throw it
- }
- false
- } cleanup {
- Os.close(sock)
- }
- }
-
- lp.setLinkAddresses(NetworkInterface.getByName(ifaceName).interfaceAddresses.map {
- LinkAddress(it.address, it.networkPrefixLength.toInt())
- })
- agent.sendLinkProperties(lp)
- return agent
- }
-
private fun makeTestServiceInfo(network: Network? = null) = NsdServiceInfo().also {
it.serviceType = serviceType
it.serviceName = serviceName
@@ -573,7 +510,9 @@
assertEquals(testNetwork1.network, serviceLost.serviceInfo.network)
val newAgent = runAsShell(MANAGE_TEST_NETWORKS) {
- registerTestNetworkAgent(testNetwork1.iface.interfaceName)
+ TestableNetworkAgent.createOnInterface(context, handlerThread.looper,
+ testNetwork1.iface.interfaceName,
+ TIMEOUT_MS)
}
val newNetwork = newAgent.network ?: fail("Registered agent should have a network")
val serviceDiscovered3 = discoveryRecord.expectCallback<ServiceFound>()
@@ -2629,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/netpermission/updatestatspermission/Android.bp b/tests/cts/netpermission/updatestatspermission/Android.bp
index b324dc8..0ff98e7 100644
--- a/tests/cts/netpermission/updatestatspermission/Android.bp
+++ b/tests/cts/netpermission/updatestatspermission/Android.bp
@@ -33,6 +33,7 @@
// Tag this module as a cts test artifact
test_suites: [
+ "automotive-general-tests",
"cts",
"general-tests",
],
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 6d53ddf..d103f75 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;
@@ -44,6 +45,7 @@
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
@@ -71,6 +73,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;
@@ -83,6 +86,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
import com.android.testutils.ParcelUtils;
import com.android.testutils.com.android.testutils.CarrierConfigRule;
@@ -388,18 +392,21 @@
mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
- try {
- final int ret = runAsShell(TETHER_PRIVILEGED, () -> mTM.tether(wifiTetheringIface));
- // There is no guarantee that the wifi interface will be available after disabling
- // the hotspot, so don't fail the test if the call to tether() fails.
- if (ret == TETHER_ERROR_NO_ERROR) {
- // If calling #tether successful, there is a callback to tell the result of
- // tethering setup.
- tetherEventCallback.expectErrorOrTethered(
- new TetheringInterface(TETHERING_WIFI, wifiTetheringIface));
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ try {
+ final int ret = runAsShell(TETHER_PRIVILEGED,
+ () -> mTM.tether(wifiTetheringIface));
+ // There is no guarantee that the wifi interface will be available after
+ // disabling the hotspot, so don't fail the test if the call to tether() fails.
+ if (ret == TETHER_ERROR_NO_ERROR) {
+ // If calling #tether successful, there is a callback to tell the result of
+ // tethering setup.
+ tetherEventCallback.expectErrorOrTethered(
+ new TetheringInterface(TETHERING_WIFI, wifiTetheringIface));
+ }
+ } finally {
+ runAsShell(TETHER_PRIVILEGED, () -> mTM.untether(wifiTetheringIface));
}
- } finally {
- runAsShell(TETHER_PRIVILEGED, () -> mTM.untether(wifiTetheringIface));
}
} finally {
mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
@@ -461,12 +468,29 @@
} catch (UnsupportedOperationException expect) { }
}
+ private boolean isTetheringWithSoftApConfigEnabled() {
+ return Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM
+ && Flags.tetheringWithSoftApConfig();
+ }
+
@Test
- public void testEnableTetheringPermission() throws Exception {
+ 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 {
@@ -598,4 +622,11 @@
}
}
}
+
+ @Test
+ public void testLegacyTetherApisThrowUnsupportedOperationExceptionAfterV() {
+ assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM);
+ assertThrows(UnsupportedOperationException.class, () -> mTM.tether("iface"));
+ assertThrows(UnsupportedOperationException.class, () -> mTM.untether("iface"));
+ }
}
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index 9a77c89..b415382 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -44,6 +44,7 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
@@ -65,6 +66,7 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.net.ConnectivityManager.NetworkCallback;
+import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Handler;
@@ -669,4 +671,12 @@
// No callbacks overridden -> do not use the optimization
eq(~0));
}
+
+ @Test
+ public void testLegacyTetherApisThrowUnsupportedOperationExceptionAfterV() {
+ assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM);
+ final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+ assertThrows(UnsupportedOperationException.class, () -> manager.tether("iface"));
+ assertThrows(UnsupportedOperationException.class, () -> manager.untether("iface"));
+ }
}
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/L2capNetworkProviderTest.kt b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
new file mode 100644
index 0000000..5a7515e
--- /dev/null
+++ b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
@@ -0,0 +1,216 @@
+/*
+ * 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.content.Context
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.TYPE_NONE
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_NONE
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
+import android.net.NetworkProvider
+import android.net.NetworkProvider.NetworkOfferCallback
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertTrue
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+const val TAG = "L2capNetworkProviderTest"
+
+val RESERVATION_CAPS = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .build()
+
+val RESERVATION = NetworkRequest(
+ NetworkCapabilities(RESERVATION_CAPS),
+ TYPE_NONE,
+ 42 /* rId */,
+ NetworkRequest.Type.RESERVATION
+)
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class L2capNetworkProviderTest {
+ @Mock private lateinit var context: Context
+ @Mock private lateinit var deps: L2capNetworkProvider.Dependencies
+ @Mock private lateinit var provider: NetworkProvider
+ @Mock private lateinit var cm: ConnectivityManager
+ @Mock private lateinit var pm: PackageManager
+
+ private val handlerThread = HandlerThread("$TAG handler thread").apply { start() }
+ private val handler = Handler(handlerThread.looper)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ doReturn(provider).`when`(deps).getNetworkProvider(any(), any())
+ doReturn(handlerThread).`when`(deps).getHandlerThread()
+ doReturn(cm).`when`(context).getSystemService(eq(ConnectivityManager::class.java))
+ doReturn(pm).`when`(context).getPackageManager()
+ doReturn(true).`when`(pm).hasSystemFeature(FEATURE_BLUETOOTH_LE)
+ }
+
+ @After
+ fun tearDown() {
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ @Test
+ fun testNetworkProvider_registeredWhenSupported() {
+ L2capNetworkProvider(deps, context).start()
+ verify(cm).registerNetworkProvider(eq(provider))
+ verify(provider).registerNetworkOffer(any(), any(), any(), any())
+ }
+
+ @Test
+ fun testNetworkProvider_notRegisteredWhenNotSupported() {
+ doReturn(false).`when`(pm).hasSystemFeature(FEATURE_BLUETOOTH_LE)
+ L2capNetworkProvider(deps, context).start()
+ verify(cm, never()).registerNetworkProvider(eq(provider))
+ }
+
+ fun doTestBlanketOfferIgnoresRequest(request: NetworkRequest) {
+ clearInvocations(provider)
+ L2capNetworkProvider(deps, context).start()
+
+ val blanketOfferCaptor = ArgumentCaptor.forClass(NetworkOfferCallback::class.java)
+ verify(provider).registerNetworkOffer(any(), any(), any(), blanketOfferCaptor.capture())
+
+ blanketOfferCaptor.value.onNetworkNeeded(request)
+ verify(provider).registerNetworkOffer(any(), any(), any(), any())
+ }
+
+ fun doTestBlanketOfferCreatesReservation(
+ request: NetworkRequest,
+ reservation: NetworkCapabilities
+ ) {
+ clearInvocations(provider)
+ L2capNetworkProvider(deps, context).start()
+
+ val blanketOfferCaptor = ArgumentCaptor.forClass(NetworkOfferCallback::class.java)
+ verify(provider).registerNetworkOffer(any(), any(), any(), blanketOfferCaptor.capture())
+
+ blanketOfferCaptor.value.onNetworkNeeded(request)
+
+ val capsCaptor = ArgumentCaptor.forClass(NetworkCapabilities::class.java)
+ verify(provider, times(2)).registerNetworkOffer(any(), capsCaptor.capture(), any(), any())
+
+ assertTrue(reservation.satisfiedByNetworkCapabilities(capsCaptor.value))
+ }
+
+ @Test
+ fun testBlanketOffer_reservationWithoutSpecifier() {
+ val caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .build()
+ val nr = NetworkRequest(caps, TYPE_NONE, 42 /* rId */, NetworkRequest.Type.RESERVATION)
+
+ doTestBlanketOfferIgnoresRequest(nr)
+ }
+
+ @Test
+ fun testBlanketOffer_reservationWithCorrectSpecifier() {
+ var specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_6LOWPAN)
+ .build()
+ var caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .setNetworkSpecifier(specifier)
+ .build()
+ var nr = NetworkRequest(caps, TYPE_NONE, 42 /* rId */, NetworkRequest.Type.RESERVATION)
+ doTestBlanketOfferCreatesReservation(nr, caps)
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .build()
+ caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .setNetworkSpecifier(specifier)
+ .build()
+ nr = NetworkRequest(caps, TYPE_NONE, 43 /* rId */, NetworkRequest.Type.RESERVATION)
+ doTestBlanketOfferCreatesReservation(nr, caps)
+ }
+
+ @Test
+ fun testBlanketOffer_reservationWithIncorrectSpecifier() {
+ var specifier = L2capNetworkSpecifier.Builder().build()
+ var caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .setNetworkSpecifier(specifier)
+ .build()
+ var nr = NetworkRequest(caps, TYPE_NONE, 42 /* rId */, NetworkRequest.Type.RESERVATION)
+ doTestBlanketOfferIgnoresRequest(nr)
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .build()
+ caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .setNetworkSpecifier(specifier)
+ .build()
+ nr = NetworkRequest(caps, TYPE_NONE, 44 /* rId */, NetworkRequest.Type.RESERVATION)
+ doTestBlanketOfferIgnoresRequest(nr)
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .setPsm(0x81)
+ .build()
+ caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .setNetworkSpecifier(specifier)
+ .build()
+ nr = NetworkRequest(caps, TYPE_NONE, 45 /* rId */, NetworkRequest.Type.RESERVATION)
+ doTestBlanketOfferIgnoresRequest(nr)
+
+ specifier = L2capNetworkSpecifier.Builder()
+ .setHeaderCompression(HEADER_COMPRESSION_NONE)
+ .build()
+ caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ .setNetworkSpecifier(specifier)
+ .build()
+ nr = NetworkRequest(caps, TYPE_NONE, 47 /* rId */, NetworkRequest.Type.RESERVATION)
+ doTestBlanketOfferIgnoresRequest(nr)
+ }
+}
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 ed95e4b..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;
@@ -60,7 +60,6 @@
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.SharedLog;
-import com.android.net.module.util.TimerFileDescriptor;
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
import com.android.server.connectivity.mdns.util.MdnsUtils;
import com.android.testutils.DevSdkIgnoreRule;
@@ -128,8 +127,6 @@
private SharedLog mockSharedLog;
@Mock
private MdnsServiceTypeClient.Dependencies mockDeps;
- @Mock
- private TimerFileDescriptor mockTimerFd;
@Captor
private ArgumentCaptor<MdnsServiceInfo> serviceInfoCaptor;
@@ -148,7 +145,6 @@
private Message delayMessage = null;
private Handler realHandler = null;
private MdnsFeatureFlags featureFlags = MdnsFeatureFlags.newBuilder().build();
- private TimerFileDescriptor.MessageTask task = null;
@Before
@SuppressWarnings("DoNotMock")
@@ -248,21 +244,10 @@
return true;
}).when(mockDeps).sendMessage(any(Handler.class), any(Message.class));
- doAnswer(inv -> {
- realHandler = (Handler) inv.getArguments()[0];
- return mockTimerFd;
- }).when(mockDeps).createTimerFd(any(Handler.class));
-
- doAnswer(inv -> {
- task = (TimerFileDescriptor.MessageTask) inv.getArguments()[0];
- latestDelayMs = (long) inv.getArguments()[1];
- return null;
- }).when(mockTimerFd).setDelayedTask(any(), anyLong());
-
- client = makeMdnsServiceTypeClient(featureFlags);
+ client = makeMdnsServiceTypeClient();
}
- private MdnsServiceTypeClient makeMdnsServiceTypeClient(MdnsFeatureFlags featureFlags) {
+ private MdnsServiceTypeClient makeMdnsServiceTypeClient() {
return new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
serviceCache, featureFlags);
@@ -581,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
@@ -606,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
@@ -1941,7 +1926,9 @@
@Test
public void testSendQueryWithKnownAnswers() throws Exception {
- client = makeMdnsServiceTypeClient(
+ client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
+ mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
+ serviceCache,
MdnsFeatureFlags.newBuilder().setIsQueryWithKnownAnswerEnabled(true).build());
doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
@@ -2003,7 +1990,9 @@
@Test
public void testSendQueryWithSubTypeWithKnownAnswers() throws Exception {
- client = makeMdnsServiceTypeClient(
+ client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
+ mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps,
+ serviceCache,
MdnsFeatureFlags.newBuilder().setIsQueryWithKnownAnswerEnabled(true).build());
doCallRealMethod().when(mockDeps).getDatagramPacketsFromMdnsPacket(
@@ -2125,66 +2114,6 @@
assertEquals(9680L, latestDelayMs);
}
- @Test
- public void sendQueries_AccurateDelayCallback() {
- client = makeMdnsServiceTypeClient(
- MdnsFeatureFlags.newBuilder().setIsAccurateDelayCallbackEnabled(true).build());
-
- final int numOfQueriesBeforeBackoff = 2;
- final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
- .addSubtype(SUBTYPE)
- .setQueryMode(AGGRESSIVE_QUERY_MODE)
- .setNumOfQueriesBeforeBackoff(numOfQueriesBeforeBackoff)
- .build();
- startSendAndReceive(mockListenerOne, searchOptions);
- verify(mockTimerFd, times(1)).cancelTask();
-
- // Verify that the first query has been sent.
- verifyAndSendQuery(0 /* index */, 0 /* timeInMs */, true /* expectsUnicastResponse */,
- true /* multipleSocketDiscovery */, 1 /* scheduledCount */,
- 1 /* sendMessageCount */, true /* useAccurateDelayCallback */);
- // Verify that the task cancellation occurred before scheduling another query.
- verify(mockTimerFd, times(2)).cancelTask();
-
- // Verify that the second query has been sent
- verifyAndSendQuery(1 /* index */, 0 /* timeInMs */, false /* expectsUnicastResponse */,
- true /* multipleSocketDiscovery */, 2 /* scheduledCount */,
- 2 /* sendMessageCount */, true /* useAccurateDelayCallback */);
- // Verify that the task cancellation occurred before scheduling another query.
- verify(mockTimerFd, times(3)).cancelTask();
-
- // Verify that the third query has been sent
- verifyAndSendQuery(2 /* index */, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
- false /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
- 3 /* scheduledCount */, 3 /* sendMessageCount */,
- true /* useAccurateDelayCallback */);
- // Verify that the task cancellation occurred before scheduling another query.
- verify(mockTimerFd, times(4)).cancelTask();
-
- // In backoff mode, the current scheduled task will be canceled and reschedule if the
- // 0.8 * smallestRemainingTtl is larger than time to next run.
- long currentTime = TEST_TTL / 2 + TEST_ELAPSED_REALTIME;
- doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
- doReturn(true).when(mockTimerFd).hasDelayedTask();
- processResponse(createResponse(
- "service-instance-1", "192.0.2.123", 5353,
- SERVICE_TYPE_LABELS,
- Collections.emptyMap(), TEST_TTL), socketKey);
- // Verify that the task cancellation occurred twice.
- verify(mockTimerFd, times(6)).cancelTask();
- assertNotNull(task);
- verifyAndSendQuery(3 /* index */, (long) (TEST_TTL / 2 * 0.8) /* timeInMs */,
- true /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
- 5 /* scheduledCount */, 4 /* sendMessageCount */,
- true /* useAccurateDelayCallback */);
- // Verify that the task cancellation occurred before scheduling another query.
- verify(mockTimerFd, times(7)).cancelTask();
-
- // Stop sending packets.
- stopSendAndReceive(mockListenerOne);
- verify(mockTimerFd, times(8)).cancelTask();
- }
-
private static MdnsServiceInfo matchServiceName(String name) {
return argThat(info -> info.getServiceInstanceName().equals(name));
}
@@ -2198,22 +2127,9 @@
private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse,
boolean multipleSocketDiscovery, int scheduledCount) {
- verifyAndSendQuery(index, timeInMs, expectsUnicastResponse,
- multipleSocketDiscovery, scheduledCount, index + 1 /* sendMessageCount */,
- false /* useAccurateDelayCallback */);
- }
-
- private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse,
- boolean multipleSocketDiscovery, int scheduledCount, int sendMessageCount,
- boolean useAccurateDelayCallback) {
- if (useAccurateDelayCallback && task != null && realHandler != null) {
- runOnHandler(() -> realHandler.dispatchMessage(task.getMessage()));
- task = null;
- } else {
- // Dispatch the message
- if (delayMessage != null && realHandler != null) {
- dispatchMessage();
- }
+ // Dispatch the message
+ if (delayMessage != null && realHandler != null) {
+ dispatchMessage();
}
assertEquals(timeInMs, latestDelayMs);
currentThreadExecutor.getAndClearLastScheduledRunnable().run();
@@ -2236,15 +2152,11 @@
eq(socketKey), eq(false));
}
}
- verify(mockDeps, times(sendMessageCount))
+ verify(mockDeps, times(index + 1))
.sendMessage(any(Handler.class), any(Message.class));
// Verify the task has been scheduled.
- if (useAccurateDelayCallback) {
- verify(mockTimerFd, times(scheduledCount)).setDelayedTask(any(), anyLong());
- } else {
- verify(mockDeps, times(scheduledCount))
- .sendMessageDelayed(any(Handler.class), any(Message.class), anyLong());
- }
+ verify(mockDeps, times(scheduledCount))
+ .sendMessageDelayed(any(Handler.class), any(Message.class), anyLong());
}
private static String[] getTestServiceName(String instanceName) {
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/CSNetworkReservationTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
index a159697..e698930 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
@@ -16,33 +16,49 @@
package com.android.server
-import android.net.ConnectivityManager
-import android.net.ConnectivityManager.NetworkCallback
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_INTERNET
+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 android.os.Messenger
-import android.os.Process.INVALID_UID
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
@@ -51,37 +67,227 @@
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.R)
class CSNetworkReservationTest : CSTest() {
- // TODO: remove this helper once reserveNetwork is added.
- // NetworkCallback does not currently do anything. It's just here so the API stays consistent
- // with the eventual ConnectivityManager API.
- private fun ConnectivityManager.reserveNetwork(req: NetworkRequest, cb: NetworkCallback) {
- service.requestNetwork(INVALID_UID, req.networkCapabilities,
- NetworkRequest.Type.RESERVATION.ordinal, Messenger(csHandler), 0 /* timeout */,
- null /* binder */, ConnectivityManager.TYPE_NONE, NetworkCallback.FLAG_NONE,
- context.packageName, context.attributionTag, NetworkCallback.DECLARED_METHODS_ALL)
+ 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 testReservationTriggersOnNetworkNeeded() {
- val provider = NetworkProvider(context, csHandlerThread.looper, "Ethernet provider")
- val blanketOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+ fun testReservationRequest() {
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
- cm.registerNetworkProvider(provider)
+ // validate the reservation matches the blanket offer.
+ val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+ val reservationId = reservationReq.networkCapabilities.reservationId
- val blanketCaps = ETHERNET_CAPS.copyWithReservationId(RES_ID_MATCH_ALL_RESERVATIONS)
- provider.registerNetworkOffer(ETHERNET_SCORE, blanketCaps, {r -> r.run()}, blanketOfferCb)
+ // 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)
- val req = NetworkRequest.Builder().addTransportType(TRANSPORT_ETHERNET).build()
- val cb = NetworkCallback()
- cm.reserveNetwork(req, cb)
+ // validate onReserved was sent to the app
+ val onReservedCaps = cb.expect<Reserved>().caps
+ assertEquals(reservedOfferCaps, onReservedCaps)
- blanketOfferCb.expectOnNetworkNeeded(blanketCaps)
+ // validate the reservation matches the reserved offer.
+ reservedOfferCb.expectOnNetworkNeeded(reservedOfferCaps)
- // TODO: also test onNetworkUnneeded is called once ConnectivityManager supports the
- // reserveNetwork API.
+ // 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/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
index e6aba22..b7cfaf9 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -166,9 +166,10 @@
EthernetTracker.parseStaticIpConfiguration(configAsString));
}
- private NetworkCapabilities.Builder makeEthernetCapabilitiesBuilder(boolean clearAll) {
+ private NetworkCapabilities.Builder makeEthernetCapabilitiesBuilder(boolean clearDefaults) {
final NetworkCapabilities.Builder builder =
- clearAll ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ clearDefaults
+ ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
: new NetworkCapabilities.Builder();
return builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
@@ -176,21 +177,20 @@
}
/**
- * Test: Attempt to create a capabilties with various valid sets of capabilities/transports
+ * Test: Attempt to create a capabilities with various valid sets of capabilities/transports
*/
@Test
public void createNetworkCapabilities() {
-
// Particularly common expected results
- NetworkCapabilities defaultEthernetCleared =
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ NetworkCapabilities defaultCapabilities =
+ makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.build();
NetworkCapabilities ethernetClearedWithCommonCaps =
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(true /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
@@ -200,89 +200,71 @@
.addCapability(15)
.build();
- // Empty capabilities and transports lists with a "please clear defaults" should
- // yield an empty capabilities set with TRANPORT_ETHERNET
- assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "");
+ // Empty capabilities and transports should return the default capabilities set
+ // with TRANSPORT_ETHERNET
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "");
- // Empty capabilities and transports without the clear defaults flag should return the
- // default capabilities set with TRANSPORT_ETHERNET
- assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(false /* clearAll */)
- .setLinkUpstreamBandwidthKbps(100000)
- .setLinkDownstreamBandwidthKbps(100000)
- .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
- .build(),
- false, "", "");
-
- // A list of capabilities without the clear defaults flag should return the default
- // capabilities, mixed with the desired capabilities, and TRANSPORT_ETHERNET
- assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(false /* clearAll */)
- .setLinkUpstreamBandwidthKbps(100000)
- .setLinkDownstreamBandwidthKbps(100000)
- .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
- .addCapability(11)
- .addCapability(12)
- .build(),
- false, "11,12", "");
-
- // Adding a list of capabilities with a clear defaults will leave exactly those capabilities
- // with a default TRANSPORT_ETHERNET since no overrides are specified
- assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15", "");
+ // Adding a list of capabilities will leave exactly those capabilities with a default
+ // TRANSPORT_ETHERNET since no overrides are specified
+ assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, "12,13,14,15", "");
// Adding any invalid capabilities to the list will cause them to be ignored
- assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,65,73", "");
- assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,abcdefg", "");
+ assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, "12,13,14,15,65,73", "");
+ assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, "12,13,14,15,abcdefg", "");
// Adding a valid override transport will remove the default TRANSPORT_ETHERNET transport
- // and apply only the override to the capabiltities object
+ // and apply only the override to the capabilities object
assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(0)
.build(),
- true, "", "0");
+ "",
+ "0");
assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(1)
.build(),
- true, "", "1");
+ "",
+ "1");
assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(2)
.build(),
- true, "", "2");
+ "",
+ "2");
assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(false /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addTransportType(3)
.build(),
- true, "", "3");
+ "",
+ "3");
- // "4" is TRANSPORT_VPN, which is unsupported. Should default back to TRANPORT_ETHERNET
- assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "4");
+ // "4" is TRANSPORT_VPN, which is unsupported. Should default back to TRANSPORT_ETHERNET
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "4");
// "5" is TRANSPORT_WIFI_AWARE, which is currently supported due to no legacy TYPE_NONE
// conversion. When that becomes available, this test must be updated
- assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "5");
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "5");
// "6" is TRANSPORT_LOWPAN, which is currently supported due to no legacy TYPE_NONE
// conversion. When that becomes available, this test must be updated
- assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "6");
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "6");
// Adding an invalid override transport will leave the transport as TRANSPORT_ETHERNET
- assertParsedNetworkCapabilities(defaultEthernetCleared,true, "", "100");
- assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "abcdefg");
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "100");
+ assertParsedNetworkCapabilities(defaultCapabilities, "", "abcdefg");
// Ensure the adding of both capabilities and transports work
assertParsedNetworkCapabilities(
- makeEthernetCapabilitiesBuilder(true /* clearAll */)
+ makeEthernetCapabilitiesBuilder(true /* clearDefaults */)
.setLinkUpstreamBandwidthKbps(100000)
.setLinkDownstreamBandwidthKbps(100000)
.addCapability(12)
@@ -291,17 +273,21 @@
.addCapability(15)
.addTransportType(3)
.build(),
- true, "12,13,14,15", "3");
+ "12,13,14,15",
+ "3");
// Ensure order does not matter for capability list
- assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "13,12,15,14", "");
+ assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, "13,12,15,14", "");
}
- private void assertParsedNetworkCapabilities(NetworkCapabilities expectedNetworkCapabilities,
- boolean clearCapabilties, String configCapabiltiies,String configTransports) {
- assertEquals(expectedNetworkCapabilities,
- EthernetTracker.createNetworkCapabilities(clearCapabilties, configCapabiltiies,
- configTransports).build());
+ private void assertParsedNetworkCapabilities(
+ NetworkCapabilities expectedNetworkCapabilities,
+ String configCapabiltiies,
+ String configTransports) {
+ assertEquals(
+ expectedNetworkCapabilities,
+ EthernetTracker.createNetworkCapabilities(configCapabiltiies, configTransports)
+ .build());
}
@Test
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index 50971e7..1a833e1 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -42,7 +42,7 @@
],
static_libs: [
"libnet_utils_device_common_bpfjni",
- "libnet_utils_device_common_timerfdjni",
+ "libserviceconnectivityjni",
"libtcutils",
],
shared_libs: [
diff --git a/tests/unit/jni/android_net_frameworktests_util/onload.cpp b/tests/unit/jni/android_net_frameworktests_util/onload.cpp
index a0ce4f8..f70b04b 100644
--- a/tests/unit/jni/android_net_frameworktests_util/onload.cpp
+++ b/tests/unit/jni/android_net_frameworktests_util/onload.cpp
@@ -24,7 +24,7 @@
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,
+int register_com_android_net_module_util_ServiceConnectivityJni(JNIEnv *env,
char const *class_name);
extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
@@ -40,8 +40,8 @@
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)
+ if (register_com_android_net_module_util_ServiceConnectivityJni(
+ env, "android/net/frameworktests/util/ServiceConnectivityJni") < 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 c55096b..af16d19 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -623,12 +623,17 @@
mNat64CidrController.maybeUpdateNat64Cidr();
}
- private static OtDaemonConfiguration newOtDaemonConfig(
- @NonNull ThreadConfiguration threadConfig) {
+ private OtDaemonConfiguration newOtDaemonConfig(ThreadConfiguration threadConfig) {
+ int srpServerConfig = R.bool.config_thread_srp_server_wait_for_border_routing_enabled;
+ boolean srpServerWaitEnabled = mResources.get().getBoolean(srpServerConfig);
+ int autoJoinConfig = R.bool.config_thread_border_router_auto_join_enabled;
+ boolean autoJoinEnabled = mResources.get().getBoolean(autoJoinConfig);
return new OtDaemonConfiguration.Builder()
.setBorderRouterEnabled(threadConfig.isBorderRouterEnabled())
.setNat64Enabled(threadConfig.isNat64Enabled())
.setDhcpv6PdEnabled(threadConfig.isDhcpv6PdEnabled())
+ .setSrpServerWaitForBorderRoutingEnabled(srpServerWaitEnabled)
+ .setBorderRouterAutoJoinEnabled(autoJoinEnabled)
.build();
}
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 316f570..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
@@ -483,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)
}
}
@@ -530,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 dcbb3f5..bc8da8b 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -231,6 +231,11 @@
when(mConnectivityResources.get()).thenReturn(mResources);
when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
+ when(mResources.getBoolean(
+ eq(R.bool.config_thread_srp_server_wait_for_border_routing_enabled)))
+ .thenReturn(true);
+ when(mResources.getBoolean(eq(R.bool.config_thread_border_router_auto_join_enabled)))
+ .thenReturn(true);
when(mResources.getString(eq(R.string.config_thread_vendor_name)))
.thenReturn(TEST_VENDOR_NAME);
when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
@@ -285,6 +290,11 @@
@Test
public void initialize_resourceOverlayValuesAreSetToOtDaemon() throws Exception {
+ when(mResources.getBoolean(
+ eq(R.bool.config_thread_srp_server_wait_for_border_routing_enabled)))
+ .thenReturn(false);
+ when(mResources.getBoolean(eq(R.bool.config_thread_border_router_auto_join_enabled)))
+ .thenReturn(false);
when(mResources.getString(eq(R.string.config_thread_vendor_name)))
.thenReturn(TEST_VENDOR_NAME);
when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
@@ -297,6 +307,8 @@
mService.initialize();
mTestLooper.dispatchAll();
+ assertThat(mFakeOtDaemon.getConfiguration().srpServerWaitForBorderRoutingEnabled).isFalse();
+ assertThat(mFakeOtDaemon.getConfiguration().borderRouterAutoJoinEnabled).isFalse();
MeshcopTxtAttributes meshcopTxts = mFakeOtDaemon.getOverriddenMeshcopTxtAttributes();
assertThat(meshcopTxts.vendorName).isEqualTo(TEST_VENDOR_NAME);
assertThat(meshcopTxts.vendorOui).isEqualTo(TEST_VENDOR_OUI_BYTES);