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