diff --git a/framework/api/current.txt b/framework/api/current.txt index a9d156961c..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,8 +254,8 @@ 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 setRemoteAddress(@NonNull android.net.MacAddress); + 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 c7067f6789..3c95dd0ec2 100644 --- a/framework/src/android/net/L2capNetworkSpecifier.java +++ b/framework/src/android/net/L2capNetworkSpecifier.java @@ -18,8 +18,10 @@ 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; @@ -30,34 +32,46 @@ 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 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 +86,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 +110,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. */ - public static final int PSM_ANY = -1; + /** + * 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). */ + /** The Bluetooth L2CAP Protocol/Service Multiplexer (PSM). */ private final int mPsm; private L2capNetworkSpecifier(Parcel in) { @@ -131,12 +153,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 +173,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 +183,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 +196,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,26 +211,28 @@ 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. + * @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; } /** - * 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) { + 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; } @@ -203,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); } } @@ -211,21 +251,41 @@ 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 */ @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; } diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java new file mode 100644 index 0000000000..34968e704c --- /dev/null +++ b/service/src/com/android/server/L2capNetworkProvider.java @@ -0,0 +1,223 @@ +/* + * 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_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.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.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 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 + * 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) + .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); + } + } + + 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()); + mBlanketOffer = new BlanketReservationOffer(); + + 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/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt index b593bafa4f..484cce8fea 100644 --- a/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt +++ b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt @@ -18,7 +18,9 @@ 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 @@ -28,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 @@ -51,14 +55,63 @@ 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()) } + + @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/unit/java/com/android/server/L2capNetworkProviderTest.kt b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt new file mode 100644 index 0000000000..ffa98289fa --- /dev/null +++ b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt @@ -0,0 +1,215 @@ +/* + * 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(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)) + verify(provider).registerNetworkOffer(any(), any(), any(), any()) + } + + @Test + fun testNetworkProvider_notRegisteredWhenNotSupported() { + doReturn(false).`when`(pm).hasSystemFeature(FEATURE_BLUETOOTH_LE) + 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) + } +}