Register reserved offer

The reserved offer adapts the reservation requests capabilities, and
additionally populates its own capabilities (namely the PSM provided by
the BluetoothServerSocket).

Test: TH
Change-Id: Ida388b6f1e4552242726bd4d7eba6215bb0577a3
diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java
index 24d2746..34968e7 100644
--- a/service/src/com/android/server/L2capNetworkProvider.java
+++ b/service/src/com/android/server/L2capNetworkProvider.java
@@ -16,6 +16,9 @@
 
 package com.android.server;
 
+import static android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN;
+import static android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_ANY;
+import static android.net.L2capNetworkSpecifier.PSM_ANY;
 import static android.net.L2capNetworkSpecifier.ROLE_SERVER;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
@@ -27,6 +30,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 import static android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE;
 
+import android.annotation.Nullable;
 import android.content.Context;
 import android.net.ConnectivityManager;
 import android.net.L2capNetworkSpecifier;
@@ -35,11 +39,16 @@
 import android.net.NetworkProvider.NetworkOfferCallback;
 import android.net.NetworkRequest;
 import android.net.NetworkScore;
+import android.net.NetworkSpecifier;
 import android.os.Handler;
 import android.os.Looper;
+import android.util.ArrayMap;
+import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.Map;
+
 
 public class L2capNetworkProvider {
     private static final String TAG = L2capNetworkProvider.class.getSimpleName();
@@ -47,6 +56,7 @@
     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
@@ -80,6 +90,87 @@
             CAPABILITIES = caps;
         }
 
+        // TODO: consider moving this into L2capNetworkSpecifier as #isValidServerReservation().
+        private boolean isValidL2capSpecifier(@Nullable NetworkSpecifier spec) {
+            if (spec == null) return false;
+            // If spec is not null, L2capNetworkSpecifier#canBeSatisfiedBy() guarantees the
+            // specifier is of type L2capNetworkSpecifier.
+            final L2capNetworkSpecifier l2capSpec = (L2capNetworkSpecifier) spec;
+
+            // The ROLE_SERVER offer can be satisfied by a ROLE_ANY request.
+            if (l2capSpec.getRole() != ROLE_SERVER) return false;
+
+            // HEADER_COMPRESSION_ANY is never valid in a request.
+            if (l2capSpec.getHeaderCompression() == HEADER_COMPRESSION_ANY) return false;
+
+            // remoteAddr must be null for ROLE_SERVER requests.
+            if (l2capSpec.getRemoteAddress() != null) return false;
+
+            // reservation must allocate a PSM, so only PSM_ANY can be passed.
+            if (l2capSpec.getPsm() != PSM_ANY) return false;
+
+            return true;
+        }
+
+        @Override
+        public void onNetworkNeeded(NetworkRequest request) {
+            Log.d(TAG, "New reservation request: " + request);
+            if (!isValidL2capSpecifier(request.getNetworkSpecifier())) {
+                Log.w(TAG, "Ignoring invalid reservation request: " + request);
+                return;
+            }
+
+            final NetworkCapabilities reservationCaps = request.networkCapabilities;
+            final ReservedServerOffer reservedOffer = new ReservedServerOffer(reservationCaps);
+
+            final NetworkCapabilities reservedCaps = reservedOffer.getReservedCapabilities();
+            mProvider.registerNetworkOffer(SCORE, reservedCaps, mHandler::post, reservedOffer);
+            mReservedServerOffers.put(request.requestId, reservedOffer);
+        }
+
+        @Override
+        public void onNetworkUnneeded(NetworkRequest request) {
+            if (!mReservedServerOffers.containsKey(request.requestId)) {
+                return;
+            }
+
+            final ReservedServerOffer reservedOffer = mReservedServerOffers.get(request.requestId);
+            // Note that the reserved offer gets torn down when the reservation goes away, even if
+            // there are lingering requests.
+            reservedOffer.tearDown();
+            mProvider.unregisterNetworkOffer(reservedOffer);
+        }
+    }
+
+    private class ReservedServerOffer implements NetworkOfferCallback {
+        private final boolean mUseHeaderCompression;
+        private final int mPsm;
+        private final NetworkCapabilities mReservedCapabilities;
+
+        public ReservedServerOffer(NetworkCapabilities reservationCaps) {
+            // getNetworkSpecifier() is guaranteed to return a non-null L2capNetworkSpecifier.
+            final L2capNetworkSpecifier reservationSpec =
+                    (L2capNetworkSpecifier) reservationCaps.getNetworkSpecifier();
+            mUseHeaderCompression =
+                    reservationSpec.getHeaderCompression() == HEADER_COMPRESSION_6LOWPAN;
+
+            // TODO: open BluetoothServerSocket and allocate a PSM.
+            mPsm = 0x80;
+
+            final L2capNetworkSpecifier reservedSpec = new L2capNetworkSpecifier.Builder()
+                    .setRole(ROLE_SERVER)
+                    .setHeaderCompression(reservationSpec.getHeaderCompression())
+                    .setPsm(mPsm)
+                    .build();
+            mReservedCapabilities = new NetworkCapabilities.Builder(reservationCaps)
+                    .setNetworkSpecifier(reservedSpec)
+                    .build();
+        }
+
+        public NetworkCapabilities getReservedCapabilities() {
+            return mReservedCapabilities;
+        }
+
         @Override
         public void onNetworkNeeded(NetworkRequest request) {
             // TODO: implement
@@ -89,6 +180,16 @@
         public void onNetworkUnneeded(NetworkRequest request) {
             // TODO: implement
         }
+
+        /**
+         * Called when the reservation goes away and the reserved offer must be torn down.
+         *
+         * This method can be called multiple times.
+         */
+        public void tearDown() {
+            // TODO: implement.
+            // This method can be called multiple times.
+        }
     }
 
     @VisibleForTesting
diff --git a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
index bb7f21c..ffa9828 100644
--- a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
+++ b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
@@ -20,27 +20,51 @@
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE
 import android.net.ConnectivityManager
+import android.net.ConnectivityManager.TYPE_NONE
+import android.net.L2capNetworkSpecifier
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_6LOWPAN
+import android.net.L2capNetworkSpecifier.HEADER_COMPRESSION_NONE
+import android.net.L2capNetworkSpecifier.ROLE_SERVER
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
 import android.net.NetworkProvider
+import android.net.NetworkProvider.NetworkOfferCallback
+import android.net.NetworkRequest
 import android.os.Build
 import android.os.Handler
 import android.os.HandlerThread
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertTrue
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
 import org.mockito.Mockito.any
+import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.never
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 const val TAG = "L2capNetworkProviderTest"
 
+val RESERVATION_CAPS = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+    .addTransportType(TRANSPORT_BLUETOOTH)
+    .build()
+
+val RESERVATION = NetworkRequest(
+        NetworkCapabilities(RESERVATION_CAPS),
+        TYPE_NONE,
+        42 /* rId */,
+        NetworkRequest.Type.RESERVATION
+)
+
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 class L2capNetworkProviderTest {
@@ -81,4 +105,111 @@
         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)
+    }
 }