From c7e388423af99d0cc0a9d4e924dd65c1be0657d0 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Wed, 8 Jan 2025 06:48:21 -0800 Subject: [PATCH 01/10] Have setRemoteAddress accept a Nullable remoteAddress Per API council feedback. Bug: 388438641 Test: TH Change-Id: Iba71c4944ebbd91df5fdbb3605fc9edcc2139af5 --- framework/api/current.txt | 2 +- framework/src/android/net/L2capNetworkSpecifier.java | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/framework/api/current.txt b/framework/api/current.txt index a9d156961c..a0801499ca 100644 --- a/framework/api/current.txt +++ b/framework/api/current.txt @@ -255,7 +255,7 @@ package android.net { method @NonNull public android.net.L2capNetworkSpecifier build(); method @NonNull public android.net.L2capNetworkSpecifier.Builder setHeaderCompression(int); method @NonNull public android.net.L2capNetworkSpecifier.Builder setPsm(int); - method @NonNull public android.net.L2capNetworkSpecifier.Builder setRemoteAddress(@NonNull android.net.MacAddress); + method @NonNull public android.net.L2capNetworkSpecifier.Builder setRemoteAddress(@Nullable android.net.MacAddress); method @NonNull public android.net.L2capNetworkSpecifier.Builder setRole(int); } diff --git a/framework/src/android/net/L2capNetworkSpecifier.java b/framework/src/android/net/L2capNetworkSpecifier.java index c7067f6789..82ad05dd77 100644 --- a/framework/src/android/net/L2capNetworkSpecifier.java +++ b/framework/src/android/net/L2capNetworkSpecifier.java @@ -178,11 +178,10 @@ public final class L2capNetworkSpecifier extends NetworkSpecifier implements Par * * Only valid for client networks. A null MacAddress matches *any* MacAddress. * - * @param remoteAddress the MAC address to connect to. + * @param remoteAddress the MAC address to connect to, or null to match any MAC address. */ @NonNull - public Builder setRemoteAddress(@NonNull MacAddress remoteAddress) { - Objects.requireNonNull(remoteAddress); + public Builder setRemoteAddress(@Nullable MacAddress remoteAddress) { mRemoteAddress = remoteAddress; return this; } From d10fa68a5c781592a3185ff973bb1b911ba96b20 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Tue, 7 Jan 2025 21:07:42 -0800 Subject: [PATCH 02/10] Improve documentation of L2capNetworkSpecifier Addresses comments in aosp/3429160. Test: TH Change-Id: I05588b7f0a3d22beae4ddb2237b9a6eebdd00d03 --- .../android/net/L2capNetworkSpecifier.java | 98 +++++++++++++------ 1 file changed, 66 insertions(+), 32 deletions(-) diff --git a/framework/src/android/net/L2capNetworkSpecifier.java b/framework/src/android/net/L2capNetworkSpecifier.java index 82ad05dd77..0d46a3e106 100644 --- a/framework/src/android/net/L2capNetworkSpecifier.java +++ b/framework/src/android/net/L2capNetworkSpecifier.java @@ -20,6 +20,7 @@ import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.os.Build; import android.os.Parcel; import android.os.Parcelable; @@ -33,31 +34,43 @@ import java.util.Objects; * A {@link NetworkSpecifier} used to identify an L2CAP network. * * An L2CAP network is not symmetrical, meaning there exists both a server (Bluetooth peripheral) - * and a client (Bluetooth central) node. This specifier contains information required to request or - * reserve an L2CAP network. + * 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 PSM to be advertised to the client. Therefore, the server - * network must always be reserved using {@link ConnectivityManager#reserveNetwork}. The subsequent - * {@link ConnectivityManager.NetworkCallback#onReserved(NetworkCapabilities)} includes information - * (i.e. the PSM) for the server to advertise to the client. - * Under the hood, an L2CAP server network is represented by a {@link - * android.bluetooth.BluetoothServerSocket} which can, in theory, accept many connections. However, - * before Android 15 Bluetooth APIs do not expose the channel ID, so these connections are - * indistinguishable. In practice, this means that network matching semantics in {@link - * ConnectivityService} will tear down all but the first connection. - * - * The L2cap client network can be connected using {@link ConnectivityManager#requestNetwork} - * including passing in the relevant information (i.e. PSM and destination MAC address) using the - * {@link L2capNetworkSpecifier}. + * 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 { - /** Accept any role. */ + /** + * 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. */ + /** 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. */ + /** Specifier describes a server network, i.e., the device is the Bluetooth peripheral. */ public static final int ROLE_SERVER = 2; /** @hide */ @@ -72,7 +85,12 @@ public final class L2capNetworkSpecifier extends NetworkSpecifier implements Par @Role private final int mRole; - /** Accept any form of header compression. */ + /** + * 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; @@ -91,16 +109,19 @@ public final class L2capNetworkSpecifier extends NetworkSpecifier implements Par @HeaderCompression private final int mHeaderCompression; - /** - * The MAC address of the remote. - */ + /** The MAC address of the remote. */ @Nullable private final MacAddress mRemoteAddress; - /** Match any PSM. */ + /** + * 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 = -1; - /** The Bluetooth L2CAP Protocol Service Multiplexer (PSM). */ + /** The Bluetooth L2CAP Protocol/Service Multiplexer (PSM). */ private final int mPsm; private L2capNetworkSpecifier(Parcel in) { @@ -131,12 +152,19 @@ public final class L2capNetworkSpecifier extends NetworkSpecifier implements Par return mHeaderCompression; } - /** Returns the remote MAC address for this network to connect to. */ + /** + * 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 PSM for this network to connect to. */ + /** Returns the Protocol/Service Multiplexer (PSM) for this network to connect to. */ public int getPsm() { return mPsm; } @@ -144,9 +172,9 @@ public final class L2capNetworkSpecifier extends NetworkSpecifier implements Par /** A builder class for L2capNetworkSpecifier. */ public static final class Builder { @Role - private int mRole; + private int mRole = ROLE_ANY; @HeaderCompression - private int mHeaderCompression; + private int mHeaderCompression = HEADER_COMPRESSION_ANY; @Nullable private MacAddress mRemoteAddress; private int mPsm = PSM_ANY; @@ -154,6 +182,8 @@ public final class L2capNetworkSpecifier extends NetworkSpecifier implements Par /** * Set the role to use for this network. * + * If not set, defaults to {@link ROLE_ANY}. + * * @param role the role to use. */ @NonNull @@ -165,6 +195,10 @@ public final class L2capNetworkSpecifier extends NetworkSpecifier implements Par /** * 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 @@ -176,7 +210,7 @@ public final class L2capNetworkSpecifier extends NetworkSpecifier implements Par /** * Set the remote address for the client to connect to. * - * Only valid for client networks. A null MacAddress matches *any* MacAddress. + * 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. */ @@ -187,11 +221,11 @@ public final class L2capNetworkSpecifier extends NetworkSpecifier implements Par } /** - * Set the PSM for the client to connect to. + * Set the Protocol/Service Multiplexer (PSM) for the client to connect to. * - * Can only be configured on client networks. + * If not set, defaults to {@link PSM_ANY}. * - * @param psm the Protocol Service Multiplexer (PSM) to connect to. + * @param psm the Protocol/Service Multiplexer (PSM) to connect to. */ @NonNull public Builder setPsm(int psm) { From 5b6e6b2a5a983fd524b29cbfe12f94f16b5abe4a Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Wed, 8 Jan 2025 12:24:17 -0800 Subject: [PATCH 03/10] Use IntRange for setPsm Configures an IntRange [0, 255] for the L2CAP BLE PSM. Note that L2CAP over Bluetooth Classic uses PSM values that exceed this range though is explicitly not supported. This CL also changes the value for PSM_ANY from -1 to 0 to make the IntRange continuous as 0 is not a valid PSM (in both BLE and Classic). Bug: 388438641 Test: TH Change-Id: I15a69cfb80545c4f3723d089afd3cf026eec0696 --- framework/api/current.txt | 4 ++-- framework/src/android/net/L2capNetworkSpecifier.java | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/framework/api/current.txt b/framework/api/current.txt index a0801499ca..323c533ca0 100644 --- a/framework/api/current.txt +++ b/framework/api/current.txt @@ -244,7 +244,7 @@ package android.net { 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 = -1; // 0xffffffff + 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 @@ -254,7 +254,7 @@ package android.net { 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(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); } diff --git a/framework/src/android/net/L2capNetworkSpecifier.java b/framework/src/android/net/L2capNetworkSpecifier.java index 0d46a3e106..6770add828 100644 --- a/framework/src/android/net/L2capNetworkSpecifier.java +++ b/framework/src/android/net/L2capNetworkSpecifier.java @@ -18,6 +18,7 @@ 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; @@ -31,7 +32,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.Objects; /** - * A {@link NetworkSpecifier} used to identify an L2CAP network. + * 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 @@ -119,7 +120,7 @@ public final class L2capNetworkSpecifier extends NetworkSpecifier implements Par * 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 = -1; + public static final int PSM_ANY = 0; /** The Bluetooth L2CAP Protocol/Service Multiplexer (PSM). */ private final int mPsm; @@ -228,7 +229,10 @@ public final class L2capNetworkSpecifier extends NetworkSpecifier implements Par * @param psm the Protocol/Service Multiplexer (PSM) to connect to. */ @NonNull - public Builder setPsm(int psm) { + 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; } From 7faa0b486cebc48aae36daa0e02c748eb6a87492 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Tue, 7 Jan 2025 21:08:37 -0800 Subject: [PATCH 04/10] Do not redact PSM from L2capNetworkSpecifier Per discussion on aosp/3429160. "It doesn't look like knowing the PSM of an L2CAP network would give other apps any sort of useful information. They don't have the MAC, and there are only 128 ephemeral PSM values anyway." Test: TH Change-Id: Ia81c97dd2e31611fd2d13d5899390fc0c67b6c0f --- framework/src/android/net/L2capNetworkSpecifier.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/framework/src/android/net/L2capNetworkSpecifier.java b/framework/src/android/net/L2capNetworkSpecifier.java index 6770add828..b4a3416f55 100644 --- a/framework/src/android/net/L2capNetworkSpecifier.java +++ b/framework/src/android/net/L2capNetworkSpecifier.java @@ -256,13 +256,12 @@ public final class L2capNetworkSpecifier extends NetworkSpecifier implements Par @Override @Nullable public NetworkSpecifier redact() { - // Redact the remote MAC address and the PSM (for non-server roles). final NetworkSpecifier redactedSpecifier = new Builder() .setRole(mRole) .setHeaderCompression(mHeaderCompression) - // TODO: consider not redacting the specifier in onReserved, so the redaction can be - // more strict (i.e. the PSM could always be redacted). - .setPsm(mRole == ROLE_SERVER ? mPsm : PSM_ANY) + // The remote address is redacted. + .setRemoteAddress(null) + .setPsm(mPsm) .build(); return redactedSpecifier; } From 5e61925e5170d860b2e3182b19b5779a0e55eb3f Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Tue, 7 Jan 2025 21:09:50 -0800 Subject: [PATCH 05/10] Do not allow L2CAP specifier with ROLE_SERVER and remote address set This is an invalid combination that never seems useful. Note that this still allows ROLE_ANY in combination with a remote address. This still doesn't seem particularly useful, but there is no good reason to disallow it. Test: TH Change-Id: Ib0b7284cb99ee92f14113f8184af978eeb2318e7 --- framework/src/android/net/L2capNetworkSpecifier.java | 5 ++++- .../cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt | 5 ++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/framework/src/android/net/L2capNetworkSpecifier.java b/framework/src/android/net/L2capNetworkSpecifier.java index b4a3416f55..3331123f96 100644 --- a/framework/src/android/net/L2capNetworkSpecifier.java +++ b/framework/src/android/net/L2capNetworkSpecifier.java @@ -240,7 +240,10 @@ public final class L2capNetworkSpecifier extends NetworkSpecifier implements Par /** Create the L2capNetworkSpecifier object. */ @NonNull public L2capNetworkSpecifier build() { - // TODO: throw an exception for combinations that cannot be supported. + 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); } } diff --git a/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt index b593bafa4f..51ccdd5b24 100644 --- a/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt +++ b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt @@ -20,7 +20,6 @@ import android.net.L2capNetworkSpecifier import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_NONE 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 @@ -51,12 +50,12 @@ class L2capNetworkSpecifierTest { fun testGetters() { val remoteMac = MacAddress.fromString("11:22:33:44:55:66") val specifier = L2capNetworkSpecifier.Builder() - .setRole(ROLE_SERVER) + .setRole(ROLE_CLIENT) .setHeaderCompression(HEADER_COMPRESSION_NONE) .setPsm(123) .setRemoteAddress(remoteMac) .build() - assertEquals(ROLE_SERVER, specifier.getRole()) + assertEquals(ROLE_CLIENT, specifier.getRole()) assertEquals(HEADER_COMPRESSION_NONE, specifier.getHeaderCompression()) assertEquals(123, specifier.getPsm()) assertEquals(remoteMac, specifier.getRemoteAddress()) From 74a8f475b2d9cf98e226c482f7fff5488d8b312e Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Fri, 10 Jan 2025 10:32:30 -0800 Subject: [PATCH 06/10] Implement matching semantics for L2capNetworkSpecifier A server network is created by using ConnectivityManger#reserveNetwork, in which the app passes a specifier that sets: reservationRequest = { role = server headerCompression = 6lowpan remoteAddr = any (null) psm = any } This specifier must subsequently match the blanket reservation offer, and the reserved offer that is generated as a response: blanketOffer = { role = server headerCompression = any remoteAddr = any (null) psm = any } reservedOffer = { role = server headerCompression = 6lowpan remoteAddr = any (null) psm = 0x81 } And not the client network offer or a client network: clientOffer = { role = client headerCompression = any remoteAddr = any (null) psm = any } clientNetwork = { role = client headerCompression = 6lowpan remoteAddr = 01:02:03:04:05:06 psm = 0x81 } Similarly the client request must match both the clientOffer and clientNetweork, but not the reservedOffer (and technically blanketOffer): clientRequest = { role = client headerCompression = 6lowpan remoteAddr = 01:02:03:04:05:06 psm = 0x81 } Note, a request for TRANSPORT_BLUETOOTH with no specifier matches *any* specifier per specifier matching semantics in NetworkCapabilities. Test: L2capNetworkSpecifierTest Change-Id: Ia16926b463a54530ae6e88278466e92ed82bf852 --- .../android/net/L2capNetworkSpecifier.java | 25 ++++++++- .../net/cts/L2capNetworkSpecifierTest.kt | 54 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/framework/src/android/net/L2capNetworkSpecifier.java b/framework/src/android/net/L2capNetworkSpecifier.java index 3331123f96..3c95dd0ec2 100644 --- a/framework/src/android/net/L2capNetworkSpecifier.java +++ b/framework/src/android/net/L2capNetworkSpecifier.java @@ -251,8 +251,29 @@ public final class L2capNetworkSpecifier extends NetworkSpecifier implements Par /** @hide */ @Override public boolean canBeSatisfiedBy(NetworkSpecifier other) { - // TODO: implement matching semantics. - return false; + 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 */ diff --git a/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt index 51ccdd5b24..484cce8fea 100644 --- a/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt +++ b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt @@ -18,8 +18,11 @@ 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 @@ -27,6 +30,8 @@ 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 @@ -60,4 +65,53 @@ class L2capNetworkSpecifierTest { 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)) + } } From 47425dcb525c616b7b03e8134d7a4c4be7f23c7d Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Tue, 12 Nov 2024 08:17:54 -0800 Subject: [PATCH 07/10] Add empty L2capNetworkProvider The network provider is registered when BLE is supported. Bug: 372936361 Test: L2capNetworkProviderTest Change-Id: I8163339015f4a3bb0ff8911f6f52a38ec1730f39 --- .../android/server/L2capNetworkProvider.java | 62 ++++++++++++++ .../server/L2capNetworkProviderTest.kt | 82 +++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 service/src/com/android/server/L2capNetworkProvider.java create mode 100644 tests/unit/java/com/android/server/L2capNetworkProviderTest.kt diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java new file mode 100644 index 0000000000..694a1dff3f --- /dev/null +++ b/service/src/com/android/server/L2capNetworkProvider.java @@ -0,0 +1,62 @@ +/* + * 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.content.pm.PackageManager.FEATURE_BLUETOOTH_LE; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkProvider; +import android.os.Handler; +import android.os.Looper; + +import com.android.internal.annotations.VisibleForTesting; + + +public class L2capNetworkProvider { + private static final String TAG = L2capNetworkProvider.class.getSimpleName(); + private final Dependencies mDeps; + private final Handler mHandler; + private final NetworkProvider mProvider; + private final boolean mIsSupported; + + @VisibleForTesting + public static class Dependencies { + /** Get NetworkProvider */ + public NetworkProvider getNetworkProvider(Context context, Looper looper) { + return new NetworkProvider(context, looper, TAG); + } + } + + public L2capNetworkProvider(Context context, Handler handler) { + this(new Dependencies(), context, handler); + } + + @VisibleForTesting + public L2capNetworkProvider(Dependencies deps, Context context, Handler handler) { + mDeps = deps; + mHandler = handler; + mProvider = mDeps.getNetworkProvider(context, handler.getLooper()); + + mIsSupported = context.getPackageManager().hasSystemFeature(FEATURE_BLUETOOTH_LE); + if (mIsSupported) { + context.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider); + } + } + +} + 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 0000000000..fc0b8157aa --- /dev/null +++ b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt @@ -0,0 +1,82 @@ +/* + * 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.NetworkProvider +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +const val TAG = "L2capNetworkProviderTest" + +@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(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, handler) + verify(cm).registerNetworkProvider(eq(provider)) + } + + @Test + fun testNetworkProvider_notRegisteredWhenNotSupported() { + doReturn(false).`when`(pm).hasSystemFeature(FEATURE_BLUETOOTH_LE) + L2capNetworkProvider(deps, context, handler) + verify(cm, never()).registerNetworkProvider(eq(provider)) + } +} From 77df9d34839057406de49c5d1a731b36af87a569 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Thu, 9 Jan 2025 15:50:35 -0800 Subject: [PATCH 08/10] Add blanket offer to L2capNetworkProvider Registers a blanket reservation offer which will receive reservation requests filed using ConnectivityManager#reserveNetwork. Test: TH Change-Id: I2d37fe564a6c40f2c6dee1f6f7aa655c47628b85 --- .../android/server/L2capNetworkProvider.java | 62 +++++++++++++++++-- .../server/L2capNetworkProviderTest.kt | 2 + 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java index 694a1dff3f..463a76fbeb 100644 --- a/service/src/com/android/server/L2capNetworkProvider.java +++ b/service/src/com/android/server/L2capNetworkProvider.java @@ -16,11 +16,23 @@ package com.android.server; +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.content.Context; import android.net.ConnectivityManager; +import android.net.NetworkCapabilities; import android.net.NetworkProvider; +import android.net.NetworkProvider.NetworkOfferCallback; +import android.net.NetworkRequest; +import android.net.NetworkScore; import android.os.Handler; import android.os.Looper; @@ -32,7 +44,46 @@ public class L2capNetworkProvider { private final Dependencies mDeps; private final Handler mHandler; private final NetworkProvider mProvider; - private final boolean mIsSupported; + private final BlanketReservationOffer mBlanketOffer; + + /** + * 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 { + NetworkCapabilities caps = NetworkCapabilities.Builder.withoutDefaultCapabilities() + .addTransportType(TRANSPORT_BLUETOOTH) + .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) + .build(); + caps.setReservationId(RES_ID_MATCH_ALL_RESERVATIONS); + CAPABILITIES = caps; + } + + @Override + public void onNetworkNeeded(NetworkRequest request) { + // TODO: implement + } + + @Override + public void onNetworkUnneeded(NetworkRequest request) { + // TODO: implement + } + } @VisibleForTesting public static class Dependencies { @@ -51,12 +102,15 @@ public class L2capNetworkProvider { mDeps = deps; mHandler = handler; mProvider = mDeps.getNetworkProvider(context, handler.getLooper()); + mBlanketOffer = new BlanketReservationOffer(); - mIsSupported = context.getPackageManager().hasSystemFeature(FEATURE_BLUETOOTH_LE); - if (mIsSupported) { + final boolean isBleSupported = + context.getPackageManager().hasSystemFeature(FEATURE_BLUETOOTH_LE); + if (isBleSupported) { context.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider); + mProvider.registerNetworkOffer(BlanketReservationOffer.SCORE, + BlanketReservationOffer.CAPABILITIES, mHandler::post, mBlanketOffer); } } - } diff --git a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt index fc0b8157aa..bb7f21cc56 100644 --- a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt +++ b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt @@ -33,6 +33,7 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mock +import org.mockito.Mockito.any import org.mockito.Mockito.doReturn import org.mockito.Mockito.never import org.mockito.Mockito.verify @@ -71,6 +72,7 @@ class L2capNetworkProviderTest { fun testNetworkProvider_registeredWhenSupported() { L2capNetworkProvider(deps, context, handler) verify(cm).registerNetworkProvider(eq(provider)) + verify(provider).registerNetworkOffer(any(), any(), any(), any()) } @Test From 56ced95d2a8c3c2428d957c0bc1803823233e002 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Fri, 10 Jan 2025 10:55:50 -0800 Subject: [PATCH 09/10] Add L2capNetworkSpecifier to blanket offer The blanket offer is used to respond to reservation requests and only serves ROLE_SERVER networks, as ROLE_CLIENT networks are not reserved. Test: TH Change-Id: I9ed00a76fa0d61caf27e321ea17260fb12b69600 --- service/src/com/android/server/L2capNetworkProvider.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java index 463a76fbeb..24d27462c5 100644 --- a/service/src/com/android/server/L2capNetworkProvider.java +++ b/service/src/com/android/server/L2capNetworkProvider.java @@ -16,6 +16,7 @@ package com.android.server; +import static android.net.L2capNetworkSpecifier.ROLE_SERVER; 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; @@ -28,6 +29,7 @@ import static android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE; import android.content.Context; import android.net.ConnectivityManager; +import android.net.L2capNetworkSpecifier; import android.net.NetworkCapabilities; import android.net.NetworkProvider; import android.net.NetworkProvider.NetworkOfferCallback; @@ -61,6 +63,9 @@ public class L2capNetworkProvider { // 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) .addCapability(NET_CAPABILITY_NOT_CONGESTED) @@ -69,6 +74,7 @@ public class L2capNetworkProvider { .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; From f1ae5ab86c3f0210d8937f40bab2e7a57b2a943b Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Thu, 9 Jan 2025 15:57:10 -0800 Subject: [PATCH 10/10] Register reserved offer The reserved offer adapts the reservation requests capabilities, and additionally populates its own capabilities (namely the PSM provided by the BluetoothServerSocket). Test: TH Change-Id: Ida388b6f1e4552242726bd4d7eba6215bb0577a3 --- .../android/server/L2capNetworkProvider.java | 101 ++++++++++++++ .../server/L2capNetworkProviderTest.kt | 131 ++++++++++++++++++ 2 files changed, 232 insertions(+) diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java index 24d27462c5..34968e704c 100644 --- a/service/src/com/android/server/L2capNetworkProvider.java +++ b/service/src/com/android/server/L2capNetworkProvider.java @@ -16,6 +16,9 @@ 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_CONGESTED; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; @@ -27,6 +30,7 @@ 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.net.ConnectivityManager; import android.net.L2capNetworkSpecifier; @@ -35,11 +39,16 @@ 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.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(); @@ -47,6 +56,7 @@ public class L2capNetworkProvider { private final Handler mHandler; private final NetworkProvider mProvider; private final BlanketReservationOffer mBlanketOffer; + private final Map mReservedServerOffers = new ArrayMap<>(); /** * The blanket reservation offer is used to create an L2CAP server network, i.e. a network @@ -80,6 +90,87 @@ public class L2capNetworkProvider { 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 @@ -89,6 +180,16 @@ public class L2capNetworkProvider { 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 diff --git a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt index bb7f21cc56..ffa98289fa 100644 --- a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt +++ b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt @@ -20,27 +20,51 @@ 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 { @@ -81,4 +105,111 @@ class L2capNetworkProviderTest { L2capNetworkProvider(deps, context, handler) verify(cm, never()).registerNetworkProvider(eq(provider)) } + + fun doTestBlanketOfferIgnoresRequest(request: NetworkRequest) { + clearInvocations(provider) + L2capNetworkProvider(deps, context, handler) + + 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, handler) + + 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) + } }