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
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..99ca0d7 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.fail
 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,27 @@
         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>()
     }
 }