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
This commit is contained in:
Patrick Rohr 2024-12-06 12:29:29 -08:00
commit 73eee542a1
4 changed files with 175 additions and 32 deletions

View file

@ -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);

View file

@ -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.

View file

@ -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(

View file

@ -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<Reserved>().caps
assertEquals(specificCaps, reservedCaps)
// validate the reservation matches the specific offer.
specificOfferCb.expectOnNetworkNeeded(specificCaps)
// Specific offer goes away
provider.unregisterNetworkOffer(specificOfferCb)
cb.expect<Unavailable>()
}
}