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

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(