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
+        }
     }
 }