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