Merge changes I5c5c4ec3,I9b349d46 into main
* changes:
Add a test to ensure reservation requests do not match regular offers
Add reserveNetwork API
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 32dcfd9..797c107 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -103,6 +103,7 @@
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 33a443e..9016d13 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.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 @@
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() {}
@@ -5008,6 +5015,41 @@
}
/**
+ * 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 d9988eb..fe26858 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.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;
@@ -7802,6 +7803,28 @@
}
/**
+ * 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.
*/
@NonNull
@@ -8161,6 +8184,14 @@
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 @@
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 @@
}
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 @@
}
}
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 @@
// 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 @@
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 @@
}
// 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 @@
// 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 a159697..7b6c995 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.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.assertNull
import org.junit.Test
import org.junit.runner.RunWith
@@ -51,22 +53,12 @@
@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,52 @@
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>()
+ }
+
+ fun TestableNetworkOfferCallback.expectNoCallbackWhere(
+ predicate: (TestableNetworkOfferCallback.CallbackEntry) -> Boolean
+ ) {
+ val event = history.poll(NO_CB_TIMEOUT_MS) { predicate(it) }
+ assertNull(event)
+ }
+
+ @Test
+ fun testReservationRequest_notDeliveredToRegularOffer() {
+ val provider = NetworkProvider(context, csHandlerThread.looper, "Ethernet provider")
+ val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+
+ cm.registerNetworkProvider(provider)
+ provider.registerNetworkOffer(ETHERNET_SCORE, ETHERNET_CAPS, {r -> r.run()}, offerCb)
+
+ val req = NetworkRequest.Builder().addTransportType(TRANSPORT_ETHERNET).build()
+ val cb = TestableNetworkCallback()
+ cm.reserveNetwork(req, csHandler, cb)
+
+ // validate the offer does not receive onNetworkNeeded for reservation request
+ offerCb.expectNoCallbackWhere {
+ it is OnNetworkNeeded && it.request.type == NetworkRequest.Type.RESERVATION
+ }
}
}