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:
commit
62928ee6e1
5 changed files with 601 additions and 50 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
223
service/src/com/android/server/L2capNetworkProvider.java
Normal file
223
service/src/com/android/server/L2capNetworkProvider.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
215
tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
Normal file
215
tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue