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
diff --git a/framework/api/current.txt b/framework/api/current.txt
index a9d1569..323c533 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -244,7 +244,7 @@
     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 @@
     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 c7067f6..3c95dd0 100644
--- a/framework/src/android/net/L2capNetworkSpecifier.java
+++ b/framework/src/android/net/L2capNetworkSpecifier.java
@@ -18,8 +18,10 @@
 
 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.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.
+ * 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.
  *
- * 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}.
- *
+ * 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 @@
     @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 @@
     @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 @@
         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 @@
     /** 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 @@
         /**
          * 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 @@
         /**
          * 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 @@
         /**
          * 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 @@
         /** 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 @@
     /** @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 0000000..34968e7
--- /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<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);
+        }
+    }
+}
+
diff --git a/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt b/tests/cts/net/src/android/net/cts/L2capNetworkSpecifierTest.kt
index b593baf..484cce8 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 @@
 
 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.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 @@
     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 0000000..ffa9828
--- /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)
+    }
+}