Merge changes I963a1c84,I515d6b60,Iae99f0bc,I99d1240d,I49d078fb into main
* changes:
Check whether a NetworkRequest is already tracked
Check whether client NetworkRequest includes a valid specifier
Add empty ClientOffer
Delete L2capNetworkProviderTest
Add support for an L2CAP server network
diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java
index 15c860b..32ee397 100644
--- a/service/src/com/android/server/L2capNetworkProvider.java
+++ b/service/src/com/android/server/L2capNetworkProvider.java
@@ -19,6 +19,7 @@
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_CLIENT;
import static android.net.L2capNetworkSpecifier.ROLE_SERVER;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
@@ -54,6 +55,7 @@
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.system.Os;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -62,6 +64,9 @@
import com.android.server.net.L2capNetwork;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
@@ -88,6 +93,7 @@
private final NetworkProvider mProvider;
private final BlanketReservationOffer mBlanketOffer;
private final Set<ReservedServerOffer> mReservedServerOffers = new ArraySet<>();
+ private final ClientOffer mClientOffer;
// mBluetoothManager guaranteed non-null when read on handler thread after start() is called
@Nullable
private BluetoothManager mBluetoothManager;
@@ -269,16 +275,117 @@
return new L2capNetwork(mHandler, mContext, mProvider, ifname, socket, tunFd, caps, cb);
}
-
private class ReservedServerOffer implements NetworkOfferCallback {
private final NetworkCapabilities mReservedCapabilities;
- private final BluetoothServerSocket mServerSocket;
+ private final AcceptThread mAcceptThread;
+ // This set should almost always contain at most one network. This is because all L2CAP
+ // server networks created by the same reserved offer are indistinguishable from each other,
+ // so that ConnectivityService will tear down all but the first. However, temporarily, there
+ // can be more than one network.
+ private final Set<L2capNetwork> mL2capNetworks = new ArraySet<>();
+
+ private class AcceptThread extends Thread {
+ private static final int TIMEOUT_MS = 500;
+ private final BluetoothServerSocket mServerSocket;
+ private volatile boolean mIsRunning = true;
+
+ public AcceptThread(BluetoothServerSocket serverSocket) {
+ mServerSocket = serverSocket;
+ }
+
+ private void postDestroyAndUnregisterReservedOffer() {
+ mHandler.post(() -> {
+ destroyAndUnregisterReservedOffer(ReservedServerOffer.this);
+ });
+ }
+
+ private static void closeBluetoothSocket(BluetoothSocket socket) {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to close BluetoothSocket", e);
+ }
+ }
+
+ private void postCreateServerNetwork(BluetoothSocket connectedSocket) {
+ mHandler.post(() -> {
+ final boolean success = createServerNetwork(connectedSocket);
+ if (!success) closeBluetoothSocket(connectedSocket);
+ });
+ }
+
+ public void run() {
+ while (mIsRunning) {
+ final BluetoothSocket connectedSocket;
+ try {
+ connectedSocket = mServerSocket.accept();
+ } catch (IOException e) {
+ // BluetoothServerSocket was closed().
+ if (!mIsRunning) return;
+
+ // Else, BluetoothServerSocket encountered exception.
+ Log.e(TAG, "BluetoothServerSocket#accept failed", e);
+ postDestroyAndUnregisterReservedOffer();
+ return; // stop running immediately on error
+ }
+ postCreateServerNetwork(connectedSocket);
+ }
+ }
+
+ public void tearDown() {
+ mIsRunning = false;
+ try {
+ // BluetoothServerSocket.close() is thread-safe.
+ mServerSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to close BluetoothServerSocket", e);
+ }
+ try {
+ join();
+ } catch (InterruptedException e) {
+ // join() interrupted during tearDown(). Do nothing.
+ }
+ }
+ }
+
+ private boolean createServerNetwork(BluetoothSocket socket) {
+ // It is possible the offer went away.
+ if (!mReservedServerOffers.contains(this)) return false;
+
+ if (!socket.isConnected()) {
+ Log.wtf(TAG, "BluetoothSocket must be connected");
+ return false;
+ }
+
+ final L2capNetwork network = createL2capNetwork(socket, mReservedCapabilities,
+ new L2capNetwork.ICallback() {
+ @Override
+ public void onError(L2capNetwork network) {
+ destroyAndUnregisterReservedOffer(ReservedServerOffer.this);
+ }
+ @Override
+ public void onNetworkUnwanted(L2capNetwork network) {
+ // Leave reservation in place.
+ final boolean networkExists = mL2capNetworks.remove(network);
+ if (!networkExists) return; // already torn down.
+ network.tearDown();
+ }
+ });
+
+ if (network == null) {
+ Log.e(TAG, "Failed to create L2capNetwork");
+ return false;
+ }
+
+ mL2capNetworks.add(network);
+ return true;
+ }
public ReservedServerOffer(NetworkCapabilities reservedCapabilities,
BluetoothServerSocket serverSocket) {
mReservedCapabilities = reservedCapabilities;
- // TODO: ServerSocket will be managed by an AcceptThread.
- mServerSocket = serverSocket;
+ mAcceptThread = new AcceptThread(serverSocket);
+ mAcceptThread.start();
}
public NetworkCapabilities getReservedCapabilities() {
@@ -287,28 +394,137 @@
@Override
public void onNetworkNeeded(NetworkRequest request) {
- // TODO: implement
+ // UNUSED: the lifetime of the reserved network is controlled by the blanket offer.
}
@Override
public void onNetworkUnneeded(NetworkRequest request) {
- // TODO: implement
+ // UNUSED: the lifetime of the reserved network is controlled by the blanket offer.
}
- /**
- * Called when the reservation goes away and the reserved offer must be torn down.
- *
- * This method can be called multiple times.
- */
+ /** Called when the reservation goes away and the reserved offer must be torn down. */
public void tearDown() {
- try {
- mServerSocket.close();
- } catch (IOException e) {
- Log.w(TAG, "Failed to close BluetoothServerSocket", e);
+ mAcceptThread.tearDown();
+ for (L2capNetwork network : mL2capNetworks) {
+ network.tearDown();
}
}
}
+ private class ClientOffer implements NetworkOfferCallback {
+ public static final NetworkScore SCORE = new NetworkScore.Builder().build();
+ public static final NetworkCapabilities CAPABILITIES;
+ static {
+ // Below capabilities will match any request with an L2capNetworkSpecifier
+ // that specifies ROLE_CLIENT or without a NetworkSpecifier.
+ final L2capNetworkSpecifier l2capNetworkSpecifier = new L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_CLIENT)
+ .build();
+ CAPABILITIES = new NetworkCapabilities.Builder(COMMON_CAPABILITIES)
+ .setNetworkSpecifier(l2capNetworkSpecifier)
+ .build();
+ }
+
+ private final Map<L2capNetworkSpecifier, ClientRequestInfo> mClientNetworkRequests =
+ new ArrayMap<>();
+
+ /**
+ * State object to store information for client NetworkRequests.
+ */
+ private static class ClientRequestInfo {
+ public final List<NetworkRequest> requests = new ArrayList<>();
+
+ public ClientRequestInfo(NetworkRequest request) {
+ requests.add(request);
+ }
+ }
+
+
+ private boolean isValidL2capSpecifier(@Nullable NetworkSpecifier spec) {
+ if (spec == null) return false;
+
+ // If not null, guaranteed to be L2capNetworkSepcifier.
+ final L2capNetworkSpecifier l2capSpec = (L2capNetworkSpecifier) spec;
+
+ // The ROLE_CLIENT offer can be satisfied by a ROLE_ANY request.
+ if (l2capSpec.getRole() != ROLE_CLIENT) return false;
+
+ // HEADER_COMPRESSION_ANY is never valid in a request.
+ if (l2capSpec.getHeaderCompression() == HEADER_COMPRESSION_ANY) return false;
+
+ // remoteAddr must not be null for ROLE_CLIENT requests.
+ if (l2capSpec.getRemoteAddress() == null) return false;
+
+ // Client network requests require a PSM to be specified.
+ // Ensure the PSM is within the valid range of dynamic BLE L2CAP values.
+ if (l2capSpec.getPsm() < 0x80) return false;
+ if (l2capSpec.getPsm() > 0xFF) return false;
+
+ return true;
+ }
+
+ @Override
+ public void onNetworkNeeded(NetworkRequest request) {
+ Log.d(TAG, "New client network request: " + request);
+ if (!isValidL2capSpecifier(request.getNetworkSpecifier())) {
+ Log.w(TAG, "Ignoring invalid client request: " + request);
+ return;
+ }
+
+ final L2capNetworkSpecifier requestSpecifier =
+ (L2capNetworkSpecifier) request.getNetworkSpecifier();
+ // Check whether this exact request is already being tracked.
+ final ClientRequestInfo cri = mClientNetworkRequests.get(requestSpecifier);
+ if (cri != null) {
+ Log.d(TAG, "The request is already being tracked. NetworkRequest: " + request);
+ cri.requests.add(request);
+ return;
+ }
+
+ // Check whether a fuzzy match shows a mismatch in header compression by calling
+ // canBeSatisfiedBy().
+ // TODO: Add a copy constructor to L2capNetworkSpecifier.Builder.
+ final L2capNetworkSpecifier matchAnyHeaderCompressionSpecifier =
+ new L2capNetworkSpecifier.Builder()
+ .setRole(requestSpecifier.getRole())
+ .setRemoteAddress(requestSpecifier.getRemoteAddress())
+ .setPsm(requestSpecifier.getPsm())
+ .setHeaderCompression(HEADER_COMPRESSION_ANY)
+ .build();
+ for (L2capNetworkSpecifier existingSpecifier : mClientNetworkRequests.keySet()) {
+ if (existingSpecifier.canBeSatisfiedBy(matchAnyHeaderCompressionSpecifier)) {
+ // This requeset can never be serviced as this network already exists with a
+ // different header compression mechanism.
+ mProvider.declareNetworkRequestUnfulfillable(request);
+ return;
+ }
+ }
+
+ // If the code reaches here, this is a new request.
+ mClientNetworkRequests.put(requestSpecifier, new ClientRequestInfo(request));
+
+ // TODO: implement onNetworkNeeded
+ }
+
+ @Override
+ public void onNetworkUnneeded(NetworkRequest request) {
+ final L2capNetworkSpecifier specifier =
+ (L2capNetworkSpecifier) request.getNetworkSpecifier();
+
+ // Map#get() is safe to call with null key
+ final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
+ if (cri == null) return;
+
+ cri.requests.remove(request);
+ if (cri.requests.size() > 0) return;
+
+ // If the code reaches here, the network needs to be torn down.
+ mClientNetworkRequests.remove(specifier);
+
+ // TODO: implement onNetworkUnneeded
+ }
+ }
+
@VisibleForTesting
public static class Dependencies {
/** Get NetworkProvider */
@@ -336,6 +552,7 @@
mHandler = new Handler(mHandlerThread.getLooper());
mProvider = mDeps.getNetworkProvider(context, mHandlerThread.getLooper());
mBlanketOffer = new BlanketReservationOffer();
+ mClientOffer = new ClientOffer();
}
/**
@@ -358,6 +575,8 @@
mContext.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider);
mProvider.registerNetworkOffer(BlanketReservationOffer.SCORE,
BlanketReservationOffer.CAPABILITIES, mHandler::post, mBlanketOffer);
+ mProvider.registerNetworkOffer(ClientOffer.SCORE,
+ ClientOffer.CAPABILITIES, mHandler::post, mClientOffer);
});
}
}
diff --git a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
deleted file mode 100644
index 49eb476..0000000
--- a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
- * 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.bluetooth.BluetoothAdapter
-import android.bluetooth.BluetoothManager
-import android.bluetooth.BluetoothServerSocket
-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 com.android.testutils.waitForIdle
-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
-
-private const val TAG = "L2capNetworkProviderTest"
-private const val TIMEOUT_MS = 1000
-
-private val RESERVATION_CAPS = NetworkCapabilities.Builder.withoutDefaultCapabilities()
- .addTransportType(TRANSPORT_BLUETOOTH)
- .build()
-
-private 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
- @Mock private lateinit var bm: BluetoothManager
- @Mock private lateinit var adapter: BluetoothAdapter
- @Mock private lateinit var serverSocket: BluetoothServerSocket
-
- 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(handlerThread).`when`(deps).getHandlerThread()
- doReturn(cm).`when`(context).getSystemService(eq(ConnectivityManager::class.java))
- doReturn(pm).`when`(context).getPackageManager()
- doReturn(true).`when`(pm).hasSystemFeature(FEATURE_BLUETOOTH_LE)
-
- doReturn(bm).`when`(context).getSystemService(eq(BluetoothManager::class.java))
- doReturn(adapter).`when`(bm).getAdapter()
- doReturn(serverSocket).`when`(adapter).listenUsingInsecureL2capChannel()
- doReturn(0x80).`when`(serverSocket).getPsm()
- }
-
- @After
- fun tearDown() {
- handlerThread.quitSafely()
- handlerThread.join()
- }
-
- @Test
- fun testNetworkProvider_registeredWhenSupported() {
- L2capNetworkProvider(deps, context).start()
- handlerThread.waitForIdle(TIMEOUT_MS)
- 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).start()
- handlerThread.waitForIdle(TIMEOUT_MS)
- verify(cm, never()).registerNetworkProvider(eq(provider))
- }
-
- fun doTestBlanketOfferIgnoresRequest(request: NetworkRequest) {
- clearInvocations(provider)
- L2capNetworkProvider(deps, context).start()
- handlerThread.waitForIdle(TIMEOUT_MS)
-
- 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).start()
- handlerThread.waitForIdle(TIMEOUT_MS)
-
- 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)
- }
-}