From 73eee542a11cda372ea948d9be3c6d37891742d2 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Fri, 6 Dec 2024 12:29:29 -0800 Subject: [PATCH] Add reserveNetwork API reserveNetwork allows a requestor to initiate the creation of a network that requires the system to preallocate resources which can then be advertised by the requestor on or off device in order to help create the network. For example, this API will be used to reserve a IP over L2cap server network, which requires the system to first create a BluetoothServerSocket and reserve a PSM (i.e. port). This information is passed back to the requestor via onReserved() callback and subsequently advertised via a BLE advertisement. The network is established as soon as the client connects to the advertised PSM, which will trigger a call to onAvailable(), just like a regular NetworkRequest. onReserved() callbacks are exclusive to the reserveNetwork API and are sent when an NetworkOffer is registered that matches the reservation ID of an existing request. A reserved offer always follows the reservation request as it is created in direct response. onReserved() can only be called once. If for some reason the reservation gets cancelled by the system (i.e. the NetworkOffer is retracted) or cannot be fulfilled, onUnavailable() is called. It is up to the app to file a new NetworkRequest when this happens. An onReserved() callback is not queued, because a) it cannot be called more than once for the same reservation, b) it must always be the first callback, and c) it only gets cancelled by onUnavailable(). This change does not handle NetworkOffer migrations, such as updating the score, properly. This must be addressed before the feature can ship as the yieldToBadWifi logic can update the score on all NetworkOffers. API-Coverage-Bug: 372936361 Test: Very basic CSNetworkReservationTest; more to follow. Change-Id: I9b349d469830bf28e006cf403c605574371db267 --- framework/api/current.txt | 1 + .../src/android/net/ConnectivityManager.java | 54 ++++++++- .../android/server/ConnectivityService.java | 105 +++++++++++++++++- .../CSNetworkReservationTest.kt | 47 ++++---- 4 files changed, 175 insertions(+), 32 deletions(-) diff --git a/framework/api/current.txt b/framework/api/current.txt index 32dcfd9272..797c107c92 100644 --- a/framework/api/current.txt +++ b/framework/api/current.txt @@ -103,6 +103,7 @@ package android.net { method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, int); method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler, int); method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.app.PendingIntent); + method @FlaggedApi("com.android.net.flags.ipv6_over_ble") public void reserveNetwork(@NonNull android.net.NetworkRequest, @NonNull android.os.Handler, @NonNull android.net.ConnectivityManager.NetworkCallback); method @Deprecated public void setNetworkPreference(int); method @Deprecated public static boolean setProcessDefaultNetwork(@Nullable android.net.Network); method public void unregisterNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback); diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java index 33a443e66d..9016d132ad 100644 --- a/framework/src/android/net/ConnectivityManager.java +++ b/framework/src/android/net/ConnectivityManager.java @@ -21,6 +21,7 @@ import static android.net.NetworkRequest.Type.BACKGROUND_REQUEST; import static android.net.NetworkRequest.Type.LISTEN; import static android.net.NetworkRequest.Type.LISTEN_FOR_BEST; import static android.net.NetworkRequest.Type.REQUEST; +import static android.net.NetworkRequest.Type.RESERVATION; import static android.net.NetworkRequest.Type.TRACK_DEFAULT; import static android.net.NetworkRequest.Type.TRACK_SYSTEM_DEFAULT; import static android.net.QosCallback.QosCallbackRegistrationException; @@ -4271,12 +4272,18 @@ public class ConnectivityManager { private static final int METHOD_ONLOST = 6; /** - * Called if no network is found within the timeout time specified in - * {@link #requestNetwork(NetworkRequest, NetworkCallback, int)} call or if the - * requested network request cannot be fulfilled (whether or not a timeout was - * specified). When this callback is invoked the associated - * {@link NetworkRequest} will have already been removed and released, as if - * {@link #unregisterNetworkCallback(NetworkCallback)} had been called. + * If the callback was registered with one of the {@code requestNetwork} methods, this will + * be called if no network is found within the timeout specified in {@link + * #requestNetwork(NetworkRequest, NetworkCallback, int)} call or if the requested network + * request cannot be fulfilled (whether or not a timeout was specified). + * + * If the callback was registered when reserving a network, this method indicates that the + * reservation is removed. It can be called when the reservation is requested, because the + * system could not satisfy the reservation, or after the reserved network connects. + * + * When this callback is invoked the associated {@link NetworkRequest} will have already + * been removed and released, as if {@link #unregisterNetworkCallback(NetworkCallback)} had + * been called. */ @FilteredCallback(methodId = METHOD_ONUNAVAILABLE, calledByCallbackId = CALLBACK_UNAVAIL) public void onUnavailable() {} @@ -5007,6 +5014,41 @@ public class ConnectivityManager { sendRequestForNetwork(nc, networkCallback, 0, REQUEST, TYPE_NONE, cbHandler); } + /** + * Reserve a network to satisfy a set of {@link NetworkCapabilities}. + * + * Some types of networks require the system to generate (i.e. reserve) some set of information + * before a network can be connected. For such networks, {@link #reserveNetwork} can be used + * which may lead to a call to {@link NetworkCallback#onReserved(NetworkCapabilities)} + * containing the {@link NetworkCapabilities} that were reserved. + * + * A reservation reserves at most one network. If the network connects, a reservation request + * behaves similar to a request filed using {@link #requestNetwork}. The provided {@link + * NetworkCallback} will only be called for the reserved network. + * + * If the system determines that the requested reservation can never be fulfilled, {@link + * NetworkCallback#onUnavailable} is called, the reservation is released by the system, and the + * provided callback can be reused. Otherwise, the reservation remains in place until the + * requested network connects. There is no guarantee that the reserved network will ever + * connect. + * + * @param request {@link NetworkRequest} describing this request. + * @param handler {@link Handler} to specify the thread upon which the callback will be invoked. + * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note + * the callback must not be shared - it uniquely specifies this request. + */ + // TODO: add executor overloads for all network request methods. Any method that passed an + // Executor could process the messages on the singleton ConnectivityThread Handler. + @SuppressLint("ExecutorRegistration") + @FlaggedApi(Flags.FLAG_IPV6_OVER_BLE) + public void reserveNetwork(@NonNull NetworkRequest request, + @NonNull Handler handler, + @NonNull NetworkCallback networkCallback) { + final CallbackHandler cbHandler = new CallbackHandler(handler); + final NetworkCapabilities nc = request.networkCapabilities; + sendRequestForNetwork(nc, networkCallback, 0, RESERVATION, TYPE_NONE, cbHandler); + } + /** * Request a network to satisfy a set of {@link NetworkCapabilities}, limited * by a timeout. diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index d9988eb431..fe2685825b 100644 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -109,6 +109,7 @@ import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5; import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION; import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS; import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS; +import static android.net.NetworkCapabilities.RES_ID_UNSET; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.NetworkCapabilities.TRANSPORT_TEST; import static android.net.NetworkCapabilities.TRANSPORT_VPN; @@ -7801,6 +7802,28 @@ public class ConnectivityService extends IConnectivityManager.Stub return mNetworkRequestForCallback; } + /** + * NetworkCapabilities that were created as part of a NetworkOffer in response to a + * RESERVATION request. mReservedCapabilities is null if no current offer matches the + * RESERVATION request or if the request is not a RESERVATION. Matching is based on + * reservationId. + */ + @Nullable + private NetworkCapabilities mReservedCapabilities; + @Nullable + NetworkCapabilities getReservedCapabilities() { + return mReservedCapabilities; + } + + void setReservedCapabilities(@NonNull NetworkCapabilities caps) { + // This function can only be called once. NetworkCapabilities are never reset as the + // reservation is released when the offer disappears. + if (mReservedCapabilities != null) { + logwtf("ReservedCapabilities can only be set once"); + } + mReservedCapabilities = caps; + } + /** * Get the list of UIDs this nri applies to. */ @@ -8161,6 +8184,14 @@ public class ConnectivityService extends IConnectivityManager.Stub return PREFERENCE_ORDER_NONE; } + public int getReservationId() { + // RESERVATIONs cannot be used in multilayer requests. + if (isMultilayerRequest()) return RES_ID_UNSET; + final NetworkRequest req = mRequests.get(0); + // Non-reservation types return RES_ID_UNSET. + return req.networkCapabilities.getReservationId(); + } + @Override public void binderDied() { // As an immutable collection, mRequests cannot change by the time the @@ -9381,6 +9412,18 @@ public class ConnectivityService extends IConnectivityManager.Stub return false; } + @Nullable + private NetworkRequestInfo maybeGetNriForReservedOffer(NetworkOfferInfo noi) { + final int reservationId = noi.offer.caps.getReservationId(); + if (reservationId == RES_ID_UNSET) return null; // not a reserved offer. + + for (NetworkRequestInfo nri : mNetworkRequests.values()) { + if (reservationId == nri.getReservationId()) return nri; + } + // The reservation was withdrawn or the reserving process died. + return null; + } + /** * Register or update a network offer. * @param newOffer The new offer. If the callback member is the same as an existing @@ -9398,6 +9441,10 @@ public class ConnectivityService extends IConnectivityManager.Stub } final NetworkOfferInfo existingOffer = findNetworkOfferInfoByCallback(newOffer.callback); if (null != existingOffer) { + // TODO: to support updating the score for reserved offers by calling + // ConnectivityManager#offerNetwork with the same callback object or via + // updateOfferScore, prevent handleUnregisterNetworkOffer() from sending an + // onUnavailable() callback here. handleUnregisterNetworkOffer(existingOffer); newOffer.migrateFrom(existingOffer.offer); if (DBG) { @@ -9410,6 +9457,25 @@ public class ConnectivityService extends IConnectivityManager.Stub } } final NetworkOfferInfo noi = new NetworkOfferInfo(newOffer); + final NetworkRequestInfo reservationNri = maybeGetNriForReservedOffer(noi); + if (reservationNri != null) { + // A NetworkRequest is only allowed to trigger a single reserved offer (and onReserved() + // callback). All subsequent offers are ignored. This either indicates a bug in the + // provider (e.g., responding twice to the same reservation, or updating the + // capabilities of a reserved offer), or multiple providers responding to the same offer + // (which could happen, but is not useful to the requesting app). + // TODO: add proper support for offer migration; i.e. allow the score of a reservation + // offer to be updated. + if (reservationNri.getReservedCapabilities() != null) { + loge("A reservation can only trigger a single offer; new offer is ignored."); + return; + } + // Always update the reserved offer before calling callCallbackForRequest. + reservationNri.setReservedCapabilities(noi.offer.caps); + callCallbackForRequest( + reservationNri, null /* networkAgent */, CALLBACK_RESERVED, 0 /* arg1 */); + } + try { noi.offer.callback.asBinder().linkToDeath(noi, 0 /* flags */); } catch (RemoteException e) { @@ -9430,6 +9496,19 @@ public class ConnectivityService extends IConnectivityManager.Stub // function may be called twice in a row, but the array will no longer contain // the offer. if (!mNetworkOffers.remove(noi)) return; + + // If the offer was brought up as a result of a reservation, inform the RESERVATION request + // that it has disappeared. There is no need to reset nri.mReservedCapabilities to null, as + // CALLBACK_UNAVAIL will cause the request to be torn down. In addition, leaving + // nri.mReservedOffer set prevents an additional onReserved() callback in + // handleRegisterNetworkOffer() in the case of a migration (which would be ignored as it + // follows an onUnavailable). + final NetworkRequestInfo nri = maybeGetNriForReservedOffer(noi); + if (nri != null) { + handleRemoveNetworkRequest(nri); + callCallbackForRequest(nri, null /* networkAgent */, CALLBACK_UNAVAIL, 0 /* arg1 */); + } + noi.offer.callback.asBinder().unlinkToDeath(noi, 0 /* flags */); } @@ -10648,9 +10727,9 @@ public class ConnectivityService extends IConnectivityManager.Stub return bundle; } - // networkAgent is only allowed to be null if notificationType is - // CALLBACK_UNAVAIL. This is because UNAVAIL is about no network being - // available, while all other cases are about some particular network. + // networkAgent is only allowed to be null if notificationType is CALLBACK_UNAVAIL or + // CALLBACK_RESERVED. This is because, per definition, no network is available for UNAVAIL, and + // RESERVED callbacks happen when a NetworkOffer is created in response to a reservation. private void callCallbackForRequest(@NonNull final NetworkRequestInfo nri, @Nullable final NetworkAgentInfo networkAgent, final int notificationType, final int arg1) { @@ -10662,6 +10741,10 @@ public class ConnectivityService extends IConnectivityManager.Stub } // Even if a callback ends up not being sent, it may affect other callbacks in the queue, so // queue callbacks before checking the declared methods flags. + // UNAVAIL and RESERVED callbacks are safe not to be queued, because RESERVED must always be + // the first callback. In addition, RESERVED cannot be sent more than once and is only + // cancelled by UNVAIL. + // TODO: evaluate whether it makes sense to queue RESERVED callbacks. if (networkAgent != null && nri.maybeQueueCallback(networkAgent, notificationType)) { return; } @@ -10669,14 +10752,24 @@ public class ConnectivityService extends IConnectivityManager.Stub // No need to send the notification as the recipient method is not overridden return; } - final Network bundleNetwork = notificationType == CALLBACK_UNAVAIL - ? null - : networkAgent.network; + // networkAgent is only null for UNAVAIL and RESERVED. + final Network bundleNetwork = (networkAgent != null) ? networkAgent.network : null; final Bundle bundle = makeCommonBundleForCallback(nri, bundleNetwork); final boolean includeLocationSensitiveInfo = (nri.mCallbackFlags & NetworkCallback.FLAG_INCLUDE_LOCATION_INFO) != 0; final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback(); switch (notificationType) { + case CALLBACK_RESERVED: { + final NetworkCapabilities nc = + createWithLocationInfoSanitizedIfNecessaryWhenParceled( + networkCapabilitiesRestrictedForCallerPermissions( + nri.getReservedCapabilities(), nri.mPid, nri.mUid), + includeLocationSensitiveInfo, nri.mPid, nri.mUid, + nrForCallback.getRequestorPackageName(), + nri.mCallingAttributionTag); + putParcelable(bundle, nc); + break; + } case CALLBACK_AVAILABLE: { final NetworkCapabilities nc = createWithLocationInfoSanitizedIfNecessaryWhenParceled( diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt index a15969788d..99ca0d7cef 100644 --- a/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt +++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt @@ -16,8 +16,6 @@ package com.android.server -import android.net.ConnectivityManager -import android.net.ConnectivityManager.NetworkCallback import android.net.NetworkCapabilities import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED @@ -28,11 +26,15 @@ import android.net.NetworkProvider import android.net.NetworkRequest import android.net.NetworkScore import android.os.Build -import android.os.Messenger -import android.os.Process.INVALID_UID import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.RecorderCallback.CallbackEntry.Reserved +import com.android.testutils.RecorderCallback.CallbackEntry.Unavailable +import com.android.testutils.TestableNetworkCallback import com.android.testutils.TestableNetworkOfferCallback +import com.android.testutils.TestableNetworkOfferCallback.CallbackEntry.OnNetworkNeeded +import kotlin.test.assertEquals +import kotlin.test.fail import org.junit.Test import org.junit.runner.RunWith @@ -51,22 +53,12 @@ private const val NO_CB_TIMEOUT_MS = 200L @RunWith(DevSdkIgnoreRunner::class) @IgnoreUpTo(Build.VERSION_CODES.R) class CSNetworkReservationTest : CSTest() { - // TODO: remove this helper once reserveNetwork is added. - // NetworkCallback does not currently do anything. It's just here so the API stays consistent - // with the eventual ConnectivityManager API. - private fun ConnectivityManager.reserveNetwork(req: NetworkRequest, cb: NetworkCallback) { - service.requestNetwork(INVALID_UID, req.networkCapabilities, - NetworkRequest.Type.RESERVATION.ordinal, Messenger(csHandler), 0 /* timeout */, - null /* binder */, ConnectivityManager.TYPE_NONE, NetworkCallback.FLAG_NONE, - context.packageName, context.attributionTag, NetworkCallback.DECLARED_METHODS_ALL) - } - fun NetworkCapabilities.copyWithReservationId(resId: Int) = NetworkCapabilities(this).also { it.reservationId = resId } @Test - fun testReservationTriggersOnNetworkNeeded() { + fun testReservationRequest() { val provider = NetworkProvider(context, csHandlerThread.looper, "Ethernet provider") val blanketOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS) @@ -76,12 +68,27 @@ class CSNetworkReservationTest : CSTest() { provider.registerNetworkOffer(ETHERNET_SCORE, blanketCaps, {r -> r.run()}, blanketOfferCb) val req = NetworkRequest.Builder().addTransportType(TRANSPORT_ETHERNET).build() - val cb = NetworkCallback() - cm.reserveNetwork(req, cb) + val cb = TestableNetworkCallback() + cm.reserveNetwork(req, csHandler, cb) - blanketOfferCb.expectOnNetworkNeeded(blanketCaps) + // validate the reservation matches the blanket offer. + val reservationReq = blanketOfferCb.expectOnNetworkNeeded(blanketCaps).request + val reservationId = reservationReq.networkCapabilities.reservationId - // TODO: also test onNetworkUnneeded is called once ConnectivityManager supports the - // reserveNetwork API. + // bring up specific reservation offer + val specificCaps = ETHERNET_CAPS.copyWithReservationId(reservationId) + val specificOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS) + provider.registerNetworkOffer(ETHERNET_SCORE, specificCaps, {r -> r.run()}, specificOfferCb) + + // validate onReserved was sent to the app + val reservedCaps = cb.expect().caps + assertEquals(specificCaps, reservedCaps) + + // validate the reservation matches the specific offer. + specificOfferCb.expectOnNetworkNeeded(specificCaps) + + // Specific offer goes away + provider.unregisterNetworkOffer(specificOfferCb) + cb.expect() } }