Merge changes Ida388b6f,I9ed00a76,I2d37fe56,I81633390,Ia16926b4, ... into main

* changes:
  Register reserved offer
  Add L2capNetworkSpecifier to blanket offer
  Add blanket offer to L2capNetworkProvider
  Add empty L2capNetworkProvider
  Implement matching semantics for L2capNetworkSpecifier
  Do not allow L2CAP specifier with ROLE_SERVER and remote address set
  Do not redact PSM from L2capNetworkSpecifier
  Use IntRange for setPsm
  Improve documentation of L2capNetworkSpecifier
  Have setRemoteAddress accept a Nullable remoteAddress
This commit is contained in:
Patrick Rohr 2025-01-13 20:26:09 -08:00 • committed by Gerrit Code Review
commit 62928ee6e1
5 changed files with 601 additions and 50 deletions

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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<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)
.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);
}
}
}

View file

@ -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))
}
}

View file

@ -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)
}
}