Merge "Reduce the visibility of logging to statsd to package-private" into main
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index 47e2848..6d857b1 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -1,3 +1,6 @@
+# Keep JNI registered methods
+-keepclasseswithmembers,includedescriptorclasses class * { native <methods>; }
+
 # Keep class's integer static field for MessageUtils to parsing their name.
 -keepclassmembers class com.android.server.**,android.net.**,com.android.networkstack.** {
     static final % POLICY_*;
@@ -7,18 +10,6 @@
     static final % EVENT_*;
 }
 
--keep class com.android.networkstack.tethering.util.BpfMap {
-    native <methods>;
-}
-
--keep class com.android.networkstack.tethering.util.TcUtils {
-    native <methods>;
-}
-
--keep class com.android.networkstack.tethering.util.TetheringUtils {
-    native <methods>;
-}
-
 # Ensure runtime-visible field annotations are kept when using R8 full mode.
 -keepattributes RuntimeVisibleAnnotations,AnnotationDefault
 -keep interface com.android.networkstack.tethering.util.Struct$Field {
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index fa6ce95..6229f6d 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -174,10 +174,10 @@
         /**
          * Request Tethering change.
          *
-         * @param request the TetheringRequest this IpServer was enabled with.
+         * @param tetheringType the downstream type of this IpServer.
          * @param enabled enable or disable tethering.
          */
-        public void requestEnableTethering(TetheringRequest request, boolean enabled) { }
+        public void requestEnableTethering(int tetheringType, boolean enabled) { }
     }
 
     /** Capture IpServer dependencies, for injection. */
@@ -1189,8 +1189,8 @@
                     handleNewPrefixRequest((IpPrefix) message.obj);
                     break;
                 case CMD_NOTIFY_PREFIX_CONFLICT:
-                    mLog.i("restart tethering: " + mIfaceName);
-                    mCallback.requestEnableTethering(mTetheringRequest, false /* enabled */);
+                    mLog.i("restart tethering: " + mInterfaceType);
+                    mCallback.requestEnableTethering(mInterfaceType, false /* enabled */);
                     transitionTo(mWaitingForRestartState);
                     break;
                 case CMD_SERVICE_FAILED_TO_START:
@@ -1474,12 +1474,12 @@
                 case CMD_TETHER_UNREQUESTED:
                     transitionTo(mInitialState);
                     mLog.i("Untethered (unrequested) and restarting " + mIfaceName);
-                    mCallback.requestEnableTethering(mTetheringRequest, true /* enabled */);
+                    mCallback.requestEnableTethering(mInterfaceType, true /* enabled */);
                     break;
                 case CMD_INTERFACE_DOWN:
                     transitionTo(mUnavailableState);
                     mLog.i("Untethered (interface down) and restarting " + mIfaceName);
-                    mCallback.requestEnableTethering(mTetheringRequest, true /* enabled */);
+                    mCallback.requestEnableTethering(mInterfaceType, true /* enabled */);
                     break;
                 default:
                     return false;
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 21b420a..4f07f58 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -2232,9 +2232,9 @@
                         break;
                     }
                     case EVENT_REQUEST_CHANGE_DOWNSTREAM: {
-                        final boolean enabled = message.arg1 == 1;
-                        final TetheringRequest request = (TetheringRequest) message.obj;
-                        enableTetheringInternal(request.getTetheringType(), enabled, null, null);
+                        final int tetheringType = message.arg1;
+                        final Boolean enabled = (Boolean) message.obj;
+                        enableTetheringInternal(tetheringType, enabled, null, null);
                         break;
                     }
                     default:
@@ -2812,9 +2812,9 @@
         }
 
         @Override
-        public void requestEnableTethering(TetheringRequest request, boolean enabled) {
+        public void requestEnableTethering(int tetheringType, boolean enabled) {
             mTetherMainSM.sendMessage(TetherMainSM.EVENT_REQUEST_CHANGE_DOWNSTREAM,
-                    enabled ? 1 : 0, 0, request);
+                    tetheringType, 0, enabled ? Boolean.TRUE : Boolean.FALSE);
         }
     }
 
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index c329142..f9e3a6a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -186,9 +186,11 @@
         // - Test bluetooth prefix is reserved.
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
                 getSubAddress(mBluetoothAddress.getAddress().getAddress()));
-        final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer);
+        final LinkAddress hotspotAddress = requestStickyDownstreamAddress(mHotspotIpServer,
+                CONNECTIVITY_SCOPE_GLOBAL);
         final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress);
         assertNotEquals(asIpPrefix(mBluetoothAddress), hotspotPrefix);
+        releaseDownstream(mHotspotIpServer);
 
         // - Test previous enabled hotspot prefix(cached prefix) is reserved.
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
@@ -207,7 +209,6 @@
         assertNotEquals(asIpPrefix(mLegacyWifiP2pAddress), etherPrefix);
         assertNotEquals(asIpPrefix(mBluetoothAddress), etherPrefix);
         assertNotEquals(hotspotPrefix, etherPrefix);
-        releaseDownstream(mHotspotIpServer);
         releaseDownstream(mEthernetIpServer);
     }
 
diff --git a/framework-t/src/android/net/NetworkStatsAccess.java b/framework-t/src/android/net/NetworkStatsAccess.java
index 7c9b3ec..449588a 100644
--- a/framework-t/src/android/net/NetworkStatsAccess.java
+++ b/framework-t/src/android/net/NetworkStatsAccess.java
@@ -111,6 +111,12 @@
     /** Returns the {@link NetworkStatsAccess.Level} for the given caller. */
     public static @NetworkStatsAccess.Level int checkAccessLevel(
             Context context, int callingPid, int callingUid, @Nullable String callingPackage) {
+        final int appId = UserHandle.getAppId(callingUid);
+        if (appId == Process.SYSTEM_UID) {
+            // the system can access data usage for all apps on the device.
+            // check system uid first, to avoid possible dead lock from other APIs
+            return NetworkStatsAccess.Level.DEVICE;
+        }
         final DevicePolicyManager mDpm = context.getSystemService(DevicePolicyManager.class);
         final TelephonyManager tm = (TelephonyManager)
                 context.getSystemService(Context.TELEPHONY_SERVICE);
@@ -126,16 +132,13 @@
             Binder.restoreCallingIdentity(token);
         }
 
-        final int appId = UserHandle.getAppId(callingUid);
-
         final boolean isNetworkStack = PermissionUtils.hasAnyPermissionOf(
                 context, callingPid, callingUid, android.Manifest.permission.NETWORK_STACK,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
 
-        if (hasCarrierPrivileges || isDeviceOwner
-                || appId == Process.SYSTEM_UID || isNetworkStack) {
-            // Carrier-privileged apps and device owners, and the system (including the
-            // network stack) can access data usage for all apps on the device.
+        if (hasCarrierPrivileges || isDeviceOwner || isNetworkStack) {
+            // Carrier-privileged apps and device owners, and the network stack
+            // can access data usage for all apps on the device.
             return NetworkStatsAccess.Level.DEVICE;
         }
 
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index fe26858..18801f0 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -110,6 +110,7 @@
 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.RES_ID_MATCH_ALL_RESERVATIONS;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
@@ -6765,7 +6766,7 @@
                     final NetworkOfferInfo offer =
                             findNetworkOfferInfoByCallback((INetworkOfferCallback) msg.obj);
                     if (null != offer) {
-                        handleUnregisterNetworkOffer(offer);
+                        handleUnregisterNetworkOffer(offer, true /* releaseReservations */);
                     }
                     break;
                 }
@@ -7682,17 +7683,23 @@
         }
     }
 
-    private void ensureAllNetworkRequestsHaveType(List<NetworkRequest> requests) {
+    private void ensureAllNetworkRequestsHaveSupportedType(List<NetworkRequest> requests) {
+        final boolean isMultilayerRequest = requests.size() > 1;
         for (int i = 0; i < requests.size(); i++) {
-            ensureNetworkRequestHasType(requests.get(i));
+            ensureNetworkRequestHasSupportedType(requests.get(i), isMultilayerRequest);
         }
     }
 
-    private void ensureNetworkRequestHasType(NetworkRequest request) {
+    private void ensureNetworkRequestHasSupportedType(NetworkRequest request,
+            boolean isMultilayerRequest) {
         if (request.type == NetworkRequest.Type.NONE) {
             throw new IllegalArgumentException(
                     "All NetworkRequests in ConnectivityService must have a type");
         }
+        if (isMultilayerRequest && request.type == NetworkRequest.Type.RESERVATION) {
+            throw new IllegalArgumentException(
+                    "Reservation requests are not supported in multilayer request");
+        }
     }
 
     /**
@@ -7844,7 +7851,7 @@
         NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r,
                 @NonNull final NetworkRequest requestForCallback, @Nullable final PendingIntent pi,
                 @Nullable String callingAttributionTag, final int preferenceOrder) {
-            ensureAllNetworkRequestsHaveType(r);
+            ensureAllNetworkRequestsHaveSupportedType(r);
             mRequests = initializeRequests(r);
             mNetworkRequestForCallback = requestForCallback;
             mPendingIntent = pi;
@@ -7878,7 +7885,7 @@
                 @NetworkCallback.Flag int callbackFlags,
                 @Nullable String callingAttributionTag, int declaredMethodsFlags) {
             super();
-            ensureAllNetworkRequestsHaveType(r);
+            ensureAllNetworkRequestsHaveSupportedType(r);
             mRequests = initializeRequests(r);
             mNetworkRequestForCallback = requestForCallback;
             mMessenger = m;
@@ -7898,7 +7905,7 @@
         NetworkRequestInfo(@NonNull final NetworkRequestInfo nri,
                 @NonNull final List<NetworkRequest> r) {
             super();
-            ensureAllNetworkRequestsHaveType(r);
+            ensureAllNetworkRequestsHaveSupportedType(r);
             mRequests = initializeRequests(r);
             mNetworkRequestForCallback = nri.getNetworkRequestForCallback();
             final NetworkAgentInfo satisfier = nri.getSatisfier();
@@ -8887,7 +8894,7 @@
 
     @Override
     public void releaseNetworkRequest(NetworkRequest networkRequest) {
-        ensureNetworkRequestHasType(networkRequest);
+        ensureNetworkRequestHasSupportedType(networkRequest, false /* isMultilayerRequest */);
         mHandler.sendMessage(mHandler.obtainMessage(
                 EVENT_RELEASE_NETWORK_REQUEST, mDeps.getCallingUid(), 0, networkRequest));
     }
@@ -8930,6 +8937,11 @@
         Objects.requireNonNull(score);
         Objects.requireNonNull(caps);
         Objects.requireNonNull(callback);
+        if (caps.hasTransport(TRANSPORT_TEST)) {
+            enforceAnyPermissionOf(mContext, Manifest.permission.MANAGE_TEST_NETWORKS);
+        } else {
+            enforceNetworkFactoryPermission();
+        }
         final boolean yieldToBadWiFi = caps.hasTransport(TRANSPORT_CELLULAR) && !avoidBadWifi();
         final NetworkOffer offer = new NetworkOffer(
                 FullScore.makeProspectiveScore(score, caps, yieldToBadWiFi),
@@ -8968,7 +8980,7 @@
             }
         }
         for (final NetworkOfferInfo noi : toRemove) {
-            handleUnregisterNetworkOffer(noi);
+            handleUnregisterNetworkOffer(noi, true /* releaseReservations */);
         }
         if (DBG) log("unregisterNetworkProvider for " + npi.name);
     }
@@ -9401,7 +9413,7 @@
 
         @Override
         public void binderDied() {
-            mHandler.post(() -> handleUnregisterNetworkOffer(this));
+            mHandler.post(() -> handleUnregisterNetworkOffer(this, true /* releaseReservations */));
         }
     }
 
@@ -9440,41 +9452,61 @@
             return;
         }
         final NetworkOfferInfo existingOffer = findNetworkOfferInfoByCallback(newOffer.callback);
+
+        // If a reserved offer is updated, ensure the capabilities are not changed. This ensures
+        // that the reserved offer's capabilities match the ones passed by the onReserved callback,
+        // which is sent only once.
+        //
+        // TODO: consider letting the provider change the capabilities of an offer as long as they
+        // continue to satisfy the capabilities that were passed to onReserved. This is not needed
+        // today, but it shouldn't violate the API contract:
+        // - NetworkOffer capabilities are not promises
+        // - The app making a reservation must never assume that the capabilities of the reserved
+        // network are equal to the ones that were passed to onReserved. There will almost always be
+        // other capabilities, for example, those that change at runtime such as VALIDATED or
+        // NOT_SUSPENDED.
+        if (null != existingOffer
+                && existingOffer.offer.caps.getReservationId() != RES_ID_UNSET
+                && existingOffer.offer.caps.getReservationId() != RES_ID_MATCH_ALL_RESERVATIONS
+                && !newOffer.caps.equals(existingOffer.offer.caps)) {
+            // Reserved offers are not allowed to update their NetworkCapabilities.
+            // Doing so will immediately remove the offer from CS and send onUnavailable to the app.
+            handleUnregisterNetworkOffer(existingOffer, true /* releaseReservations */);
+            existingOffer.offer.notifyUnneeded();
+            logwtf("Reserved offers must never update their reserved NetworkCapabilities");
+            return;
+        }
+
+        final NetworkOfferInfo noi = new NetworkOfferInfo(newOffer);
         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);
+            // Do not send onUnavailable for a reserved offer when updating it.
+            handleUnregisterNetworkOffer(existingOffer, false /* releaseReservations */);
             newOffer.migrateFrom(existingOffer.offer);
             if (DBG) {
                 // handleUnregisterNetworkOffer has already logged the old offer
                 log("update offer from providerId " + newOffer.providerId + " new : " + newOffer);
             }
         } else {
+            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).
+                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*/);
+            }
             if (DBG) {
                 log("register offer from providerId " + newOffer.providerId + " : " + newOffer);
             }
         }
-        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 */);
@@ -9486,7 +9518,8 @@
         issueNetworkNeeds(noi);
     }
 
-    private void handleUnregisterNetworkOffer(@NonNull final NetworkOfferInfo noi) {
+    private void handleUnregisterNetworkOffer(@NonNull final NetworkOfferInfo noi,
+                    boolean releaseReservations) {
         ensureRunningOnConnectivityServiceThread();
         if (DBG) {
             log("unregister offer from providerId " + noi.offer.providerId + " : " + noi.offer);
@@ -9504,11 +9537,10 @@
         // handleRegisterNetworkOffer() in the case of a migration (which would be ignored as it
         // follows an onUnavailable).
         final NetworkRequestInfo nri = maybeGetNriForReservedOffer(noi);
-        if (nri != null) {
+        if (releaseReservations && nri != null) {
             handleRemoveNetworkRequest(nri);
             callCallbackForRequest(nri, null /* networkAgent */, CALLBACK_UNAVAIL, 0 /* arg1 */);
         }
-
         noi.offer.callback.asBinder().unlinkToDeath(noi, 0 /* flags */);
     }
 
diff --git a/service/src/com/android/server/connectivity/NetworkOffer.java b/service/src/com/android/server/connectivity/NetworkOffer.java
index eea382e..d294046 100644
--- a/service/src/com/android/server/connectivity/NetworkOffer.java
+++ b/service/src/com/android/server/connectivity/NetworkOffer.java
@@ -42,6 +42,7 @@
  * @hide
  */
 public class NetworkOffer implements NetworkRanker.Scoreable {
+    private static final String TAG = NetworkOffer.class.getSimpleName();
     @NonNull public final FullScore score;
     @NonNull public final NetworkCapabilities caps;
     @NonNull public final INetworkOfferCallback callback;
@@ -126,6 +127,23 @@
     }
 
     /**
+     * Sends onNetworkUnneeded for any remaining NetworkRequests.
+     *
+     * Used after a NetworkOffer migration failed to let the provider know that its networks should
+     * be torn down (as the offer is no longer registered).
+     */
+    public void notifyUnneeded() {
+        try {
+            for (NetworkRequest request : mCurrentlyNeeded) {
+                callback.onNetworkUnneeded(request);
+            }
+        } catch (RemoteException e) {
+            // The remote is dead; nothing to do.
+        }
+        mCurrentlyNeeded.clear();
+    }
+
+    /**
      * Migrate from, and take over, a previous offer.
      *
      * When an updated offer is sent from a provider, call this method on the new offer, passing
diff --git a/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java b/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java
new file mode 100644
index 0000000..b4f7642
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/TerribleErrorLog.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.NonNull;
+import android.util.Log;
+
+import java.util.function.BiConsumer;
+
+/**
+ * Utility class for logging terrible errors and reporting them for tracking.
+ *
+ * @hide
+ */
+public class TerribleErrorLog {
+
+    private static final String TAG = TerribleErrorLog.class.getSimpleName();
+
+    /**
+     * Logs a terrible error and reports metrics through a provided statsLog.
+     */
+    public static void logTerribleError(@NonNull BiConsumer<Integer, Integer> statsLog,
+            @NonNull String message, int protoType, int errorType) {
+        statsLog.accept(protoType, errorType);
+        Log.wtf(TAG, message);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt
new file mode 100644
index 0000000..5fd634e
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/TerribleErrorLogTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util
+
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.testutils.tryTest
+import kotlin.test.assertContentEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class TerribleErrorLogTest {
+    @Test
+    fun testLogTerribleError() {
+        val wtfCaptures = mutableListOf<String>()
+        val prevHandler = Log.setWtfHandler { tag, what, system ->
+            wtfCaptures.add("$tag,${what.message}")
+        }
+        val statsLogCapture = mutableListOf<Pair<Int, Int>>()
+        val testStatsLog = object {
+            fun write(protoType: Int, errorType: Int) {
+                statsLogCapture.add(protoType to errorType)
+            }
+        }
+        tryTest {
+            TerribleErrorLog.logTerribleError(testStatsLog::write, "error", 1, 2)
+            assertContentEquals(listOf(1 to 2), statsLogCapture)
+            assertContentEquals(listOf("TerribleErrorLog,error"), wtfCaptures)
+        } cleanup {
+            Log.setWtfHandler(prevHandler)
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
index 7b6c995..e698930 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkReservationTest.kt
@@ -17,12 +17,16 @@
 package com.android.server
 
 import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
-import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P
 import android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS
 import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
 import android.net.NetworkProvider
+import android.net.NetworkProvider.NetworkOfferCallback
 import android.net.NetworkRequest
 import android.net.NetworkScore
 import android.os.Build
@@ -35,16 +39,26 @@
 import com.android.testutils.TestableNetworkOfferCallback.CallbackEntry.OnNetworkNeeded
 import kotlin.test.assertEquals
 import kotlin.test.assertNull
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
-
 private val ETHERNET_SCORE = NetworkScore.Builder().build()
 private val ETHERNET_CAPS = NetworkCapabilities.Builder()
         .addTransportType(TRANSPORT_ETHERNET)
+        .addTransportType(TRANSPORT_TEST)
         .addCapability(NET_CAPABILITY_INTERNET)
         .addCapability(NET_CAPABILITY_NOT_CONGESTED)
         .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        .removeCapability(NET_CAPABILITY_TRUSTED)
+        .build()
+private val BLANKET_CAPS = NetworkCapabilities(ETHERNET_CAPS).apply {
+    reservationId = RES_ID_MATCH_ALL_RESERVATIONS
+}
+private val ETHERNET_REQUEST = NetworkRequest.Builder()
+        .addTransportType(TRANSPORT_ETHERNET)
+        .addTransportType(TRANSPORT_TEST)
+        .removeCapability(NET_CAPABILITY_TRUSTED)
         .build()
 
 private const val TIMEOUT_MS = 5_000L
@@ -53,42 +67,53 @@
 @RunWith(DevSdkIgnoreRunner::class)
 @IgnoreUpTo(Build.VERSION_CODES.R)
 class CSNetworkReservationTest : CSTest() {
+    private lateinit var provider: NetworkProvider
+    private val blanketOffer = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+
+    @Before
+    fun subclassSetUp() {
+        provider = NetworkProvider(context, csHandlerThread.looper, "Ethernet provider")
+        cm.registerNetworkProvider(provider)
+
+        // register a blanket offer for use in tests.
+        provider.registerNetworkOffer(ETHERNET_SCORE, BLANKET_CAPS, blanketOffer)
+    }
+
     fun NetworkCapabilities.copyWithReservationId(resId: Int) = NetworkCapabilities(this).also {
         it.reservationId = resId
     }
 
+    fun NetworkProvider.registerNetworkOffer(
+            score: NetworkScore,
+            caps: NetworkCapabilities,
+            cb: NetworkOfferCallback
+    ) {
+        registerNetworkOffer(score, caps, {r -> r.run()}, cb)
+    }
+
     @Test
     fun testReservationRequest() {
-        val provider = NetworkProvider(context, csHandlerThread.looper, "Ethernet provider")
-        val blanketOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
-
-        cm.registerNetworkProvider(provider)
-
-        val blanketCaps = ETHERNET_CAPS.copyWithReservationId(RES_ID_MATCH_ALL_RESERVATIONS)
-        provider.registerNetworkOffer(ETHERNET_SCORE, blanketCaps, {r -> r.run()}, blanketOfferCb)
-
-        val req = NetworkRequest.Builder().addTransportType(TRANSPORT_ETHERNET).build()
         val cb = TestableNetworkCallback()
-        cm.reserveNetwork(req, csHandler, cb)
+        cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
 
         // validate the reservation matches the blanket offer.
-        val reservationReq = blanketOfferCb.expectOnNetworkNeeded(blanketCaps).request
+        val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
         val reservationId = reservationReq.networkCapabilities.reservationId
 
-        // 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)
+        // bring up reserved reservation offer
+        val reservedOfferCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+        val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        provider.registerNetworkOffer(ETHERNET_SCORE, reservedOfferCaps, reservedOfferCb)
 
         // validate onReserved was sent to the app
-        val reservedCaps = cb.expect<Reserved>().caps
-        assertEquals(specificCaps, reservedCaps)
+        val onReservedCaps = cb.expect<Reserved>().caps
+        assertEquals(reservedOfferCaps, onReservedCaps)
 
-        // validate the reservation matches the specific offer.
-        specificOfferCb.expectOnNetworkNeeded(specificCaps)
+        // validate the reservation matches the reserved offer.
+        reservedOfferCb.expectOnNetworkNeeded(reservedOfferCaps)
 
-        // Specific offer goes away
-        provider.unregisterNetworkOffer(specificOfferCb)
+        // reserved offer goes away
+        provider.unregisterNetworkOffer(reservedOfferCb)
         cb.expect<Unavailable>()
     }
 
@@ -101,19 +126,168 @@
 
     @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)
+        cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
 
         // validate the offer does not receive onNetworkNeeded for reservation request
         offerCb.expectNoCallbackWhere {
             it is OnNetworkNeeded && it.request.type == NetworkRequest.Type.RESERVATION
         }
     }
+
+    @Test
+    fun testReservedOffer_preventReservationIdUpdate() {
+        val cb = TestableNetworkCallback()
+        cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+        // validate the reservation matches the blanket offer.
+        val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+        val reservationId = reservationReq.networkCapabilities.reservationId
+
+        // bring up reserved offer
+        val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+        val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+
+        cb.expect<Reserved>()
+        reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+
+        // try to update the offer's reservationId by reusing the same callback object.
+        // first file a new request to try and match the offer later.
+        val cb2 = TestableNetworkCallback()
+        cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb2)
+
+        val reservationReq2 = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+        val reservationId2 = reservationReq2.networkCapabilities.reservationId
+
+        // try to update the offer's reservationId to an existing reservationId.
+        val updatedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId2)
+        provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, reservedOfferCb)
+
+        // validate the original offer disappeared.
+        cb.expect<Unavailable>()
+        // validate the new offer was rejected by CS.
+        reservedOfferCb.expectOnNetworkUnneeded(reservedCaps)
+        // validate cb2 never sees onReserved().
+        cb2.assertNoCallback()
+    }
+
+    @Test
+    fun testReservedOffer_capabilitiesCannotBeUpdated() {
+        val cb = TestableNetworkCallback()
+        cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+        val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+        val reservationId = reservationReq.networkCapabilities.reservationId
+
+        val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+        val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+
+        cb.expect<Reserved>()
+        reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+
+        // update reserved offer capabilities
+        val updatedCaps = NetworkCapabilities(reservedCaps).addCapability(NET_CAPABILITY_WIFI_P2P)
+        provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, reservedOfferCb)
+
+        cb.expect<Unavailable>()
+        reservedOfferCb.expectOnNetworkUnneeded(reservedCaps)
+        reservedOfferCb.assertNoCallback()
+    }
+
+    @Test
+    fun testBlanketOffer_updateAllowed() {
+        val cb = TestableNetworkCallback()
+        cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+        blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS)
+
+        val updatedCaps = NetworkCapabilities(BLANKET_CAPS).addCapability(NET_CAPABILITY_WIFI_P2P)
+        provider.registerNetworkOffer(ETHERNET_SCORE, updatedCaps, blanketOffer)
+        blanketOffer.assertNoCallback()
+
+        // Note: NetworkRequest.Builder(NetworkRequest) *does not* perform a defensive copy but
+        // changes the underlying request.
+        val p2pRequest = NetworkRequest.Builder(NetworkRequest(ETHERNET_REQUEST))
+                .addCapability(NET_CAPABILITY_WIFI_P2P)
+                .build()
+        cm.reserveNetwork(p2pRequest, csHandler, cb)
+        blanketOffer.expectOnNetworkNeeded(updatedCaps)
+    }
+
+    @Test
+    fun testReservationOffer_onlyAllowSingleOffer() {
+        val cb = TestableNetworkCallback()
+        cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+        val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+        val reservationId = reservationReq.networkCapabilities.reservationId
+
+        val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        val caps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+        provider.registerNetworkOffer(ETHERNET_SCORE, caps, offerCb)
+        offerCb.expectOnNetworkNeeded(caps)
+        cb.expect<Reserved>()
+
+        val newOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        provider.registerNetworkOffer(ETHERNET_SCORE, caps, newOfferCb)
+        newOfferCb.assertNoCallback()
+        cb.assertNoCallback()
+
+        // File a regular request and validate only the old offer gets onNetworkNeeded.
+        val cb2 = TestableNetworkCallback()
+        cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+        offerCb.expectOnNetworkNeeded(caps)
+        newOfferCb.assertNoCallback()
+    }
+
+    @Test
+    fun testReservationOffer_updateScore() {
+        val cb = TestableNetworkCallback()
+        cm.reserveNetwork(ETHERNET_REQUEST, csHandler, cb)
+
+        val reservationReq = blanketOffer.expectOnNetworkNeeded(BLANKET_CAPS).request
+        val reservationId = reservationReq.networkCapabilities.reservationId
+
+        val reservedCaps = ETHERNET_CAPS.copyWithReservationId(reservationId)
+        val reservedOfferCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        provider.registerNetworkOffer(ETHERNET_SCORE, reservedCaps, reservedOfferCb)
+        reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+        reservedOfferCb.assertNoCallback()
+        cb.expect<Reserved>()
+
+        // update reserved offer capabilities
+        val newScore = NetworkScore.Builder().setShouldYieldToBadWifi(true).build()
+        provider.registerNetworkOffer(newScore, reservedCaps, reservedOfferCb)
+        cb.assertNoCallback()
+
+        val cb2 = TestableNetworkCallback()
+        cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+        reservedOfferCb.expectOnNetworkNeeded(reservedCaps)
+        reservedOfferCb.assertNoCallback()
+    }
+
+    @Test
+    fun testReservationOffer_regularOfferCanBeUpdated() {
+        val offerCb = TestableNetworkOfferCallback(TIMEOUT_MS, NO_CB_TIMEOUT_MS)
+        provider.registerNetworkOffer(ETHERNET_SCORE, ETHERNET_CAPS, offerCb)
+
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(ETHERNET_REQUEST, cb, csHandler)
+        offerCb.expectOnNetworkNeeded(ETHERNET_CAPS)
+        offerCb.assertNoCallback()
+
+        val updatedCaps = NetworkCapabilities(ETHERNET_CAPS).addCapability(NET_CAPABILITY_WIFI_P2P)
+        val newScore = NetworkScore.Builder().setShouldYieldToBadWifi(true).build()
+        provider.registerNetworkOffer(newScore, updatedCaps, offerCb)
+        offerCb.assertNoCallback()
+
+        val cb2 = TestableNetworkCallback()
+        cm.requestNetwork(ETHERNET_REQUEST, cb2, csHandler)
+        offerCb.expectOnNetworkNeeded(ETHERNET_CAPS)
+        offerCb.assertNoCallback()
+    }
 }
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
index 316f570..801e21e 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -39,6 +39,7 @@
 import android.os.SystemClock
 import android.system.OsConstants
 import android.system.OsConstants.IPPROTO_ICMP
+import android.util.Log
 import androidx.test.core.app.ApplicationProvider
 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
 import com.android.net.module.util.IpUtils
@@ -84,6 +85,8 @@
 
 /** Utilities for Thread integration tests. */
 object IntegrationTestUtils {
+    private val TAG = IntegrationTestUtils::class.simpleName
+
     // The timeout of join() after restarting ot-daemon. The device needs to send 6 Link Request
     // every 5 seconds, followed by 4 Parent Request every second. So this value needs to be 40
     // seconds to be safe
@@ -483,6 +486,7 @@
         val serviceInfoFuture = CompletableFuture<NsdServiceInfo>()
         val listener: NsdManager.DiscoveryListener = object : DefaultDiscoveryListener() {
             override fun onServiceFound(serviceInfo: NsdServiceInfo) {
+                Log.d(TAG, "onServiceFound: $serviceInfo")
                 serviceInfoFuture.complete(serviceInfo)
             }
         }
@@ -530,6 +534,7 @@
         val resolvedServiceInfoFuture = CompletableFuture<NsdServiceInfo>()
         val callback: NsdManager.ServiceInfoCallback = object : DefaultServiceInfoCallback() {
             override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
+                Log.d(TAG, "onServiceUpdated: $serviceInfo")
                 if (predicate.test(serviceInfo)) {
                     resolvedServiceInfoFuture.complete(serviceInfo)
                 }