Merge "Remove DEFAULT_FORBIDDEN_CAPABILITIES from NetworkRequest" into main
diff --git a/Cronet/tests/mts/AndroidTest.xml b/Cronet/tests/mts/AndroidTest.xml
index 513b988..bccbe29 100644
--- a/Cronet/tests/mts/AndroidTest.xml
+++ b/Cronet/tests/mts/AndroidTest.xml
@@ -53,8 +53,6 @@
         <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" />
         <!-- b/316550794 -->
         <option name="exclude-filter" value="org.chromium.net.impl.CronetLoggerTest#testEngineCreation" />
-        <!-- b/327182569 -->
-        <option name="exclude-filter" value="org.chromium.net.urlconnection.CronetURLStreamHandlerFactoryTest#testSetUrlStreamFactoryUsesCronetForNative" />
         <option name="hidden-api-checks" value="false"/>
         <option name="isolated-storage" value="false"/>
     </test>
@@ -64,4 +62,4 @@
             class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
         <option name="mainline-module-package-name" value="com.google.android.tethering" />
     </object>
-</configuration>
+</configuration>
\ No newline at end of file
diff --git a/OWNERS_core_networking_xts b/OWNERS_core_networking_xts
index 7612210..b24e3ac 100644
--- a/OWNERS_core_networking_xts
+++ b/OWNERS_core_networking_xts
@@ -3,6 +3,8 @@
 
 # For cherry-picks of CLs that are already merged in aosp/master, or flaky test fixes.
 jchalard@google.com #{LAST_RESORT_SUGGESTION}
+# In addition to cherry-picks and flaky test fixes, also for APF firmware tests
+# (to verify correct behaviour of the wifi APF interpreter)
 maze@google.com #{LAST_RESORT_SUGGESTION}
 # In addition to cherry-picks and flaky test fixes, also for incremental changes on NsdManager tests
 # to increase coverage for existing behavior, and testing of bug fixes in NsdManager
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 30bdf37..4bae221 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -103,9 +103,9 @@
         "dscpPolicy.o",
         "netd.o",
         "offload.o",
-        "offload@btf.o",
+        "offload@mainline.o",
         "test.o",
-        "test@btf.o",
+        "test@mainline.o",
     ],
     apps: [
         "ServiceConnectivityResources",
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 674cd98..1958aa8 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -94,13 +94,13 @@
 }
 
 bpf {
-    name: "offload@btf.o",
-    srcs: ["offload@btf.c"],
+    name: "offload@mainline.o",
+    srcs: ["offload@mainline.c"],
     btf: true,
     cflags: [
         "-Wall",
         "-Werror",
-        "-DBTF",
+        "-DMAINLINE",
     ],
 }
 
@@ -114,13 +114,13 @@
 }
 
 bpf {
-    name: "test@btf.o",
-    srcs: ["test@btf.c"],
+    name: "test@mainline.o",
+    srcs: ["test@mainline.c"],
     btf: true,
     cflags: [
         "-Wall",
         "-Werror",
-        "-DBTF",
+        "-DMAINLINE",
     ],
 }
 
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index 90f96a1..dd59dca 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -24,16 +24,16 @@
 #define __kernel_udphdr udphdr
 #include <linux/udp.h>
 
-#ifdef BTF
+#ifdef MAINLINE
 // BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
 // ship a different file than for later versions, but we need bpfloader v0.25+
 // for obj@ver.o support
 #define BPFLOADER_MIN_VER BPFLOADER_OBJ_AT_VER_VERSION
-#else /* BTF */
+#else /* MAINLINE */
 // The resulting .o needs to load on the Android S bpfloader
 #define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
 #define BPFLOADER_MAX_VER BPFLOADER_OBJ_AT_VER_VERSION
-#endif /* BTF */
+#endif /* MAINLINE */
 
 // Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
 #define TETHERING_UID AID_ROOT
diff --git a/bpf_progs/offload@btf.c b/bpf_progs/offload@mainline.c
similarity index 100%
rename from bpf_progs/offload@btf.c
rename to bpf_progs/offload@mainline.c
diff --git a/bpf_progs/test.c b/bpf_progs/test.c
index 70b08b7..e2b8ea5 100644
--- a/bpf_progs/test.c
+++ b/bpf_progs/test.c
@@ -18,16 +18,16 @@
 #include <linux/in.h>
 #include <linux/ip.h>
 
-#ifdef BTF
+#ifdef MAINLINE
 // BTF is incompatible with bpfloaders < v0.10, hence for S (v0.2) we must
 // ship a different file than for later versions, but we need bpfloader v0.25+
 // for obj@ver.o support
 #define BPFLOADER_MIN_VER BPFLOADER_OBJ_AT_VER_VERSION
-#else /* BTF */
+#else /* MAINLINE */
 // The resulting .o needs to load on the Android S bpfloader
 #define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
 #define BPFLOADER_MAX_VER BPFLOADER_OBJ_AT_VER_VERSION
-#endif /* BTF */
+#endif /* MAINLINE */
 
 // Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
 #define TETHERING_UID AID_ROOT
diff --git a/bpf_progs/test@btf.c b/bpf_progs/test@mainline.c
similarity index 100%
rename from bpf_progs/test@btf.c
rename to bpf_progs/test@mainline.c
diff --git a/framework-t/src/android/net/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java
index 2895b0c..6afb2d5 100644
--- a/framework-t/src/android/net/nsd/AdvertisingRequest.java
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -110,8 +110,9 @@
     }
 
     /**
-     * Returns the time interval that the resource records may be cached on a DNS resolver or
-     * {@code null} if not specified.
+     * Returns the time interval that the resource records may be cached on a DNS resolver.
+     *
+     * The value will be {@code null} if it's not specified with the {@link #Builder}.
      *
      * @hide
      */
@@ -161,7 +162,7 @@
         dest.writeParcelable(mServiceInfo, flags);
         dest.writeInt(mProtocolType);
         dest.writeLong(mAdvertisingConfig);
-        dest.writeLong(mTtl == null ? -1 : mTtl.getSeconds());
+        dest.writeLong(mTtl == null ? -1L : mTtl.getSeconds());
     }
 
 //    @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
@@ -205,7 +206,9 @@
          * When registering a service, {@link NsdManager#FAILURE_BAD_PARAMETERS} will be returned
          * if {@code ttl} is smaller than 30 seconds.
          *
-         * Note: only number of seconds of {@code ttl} is used.
+         * Note: the value after the decimal point (in unit of seconds) will be discarded. For
+         * example, {@code 30} seconds will be used when {@code Duration.ofSeconds(30L, 50_000L)}
+         * is provided.
          *
          * @param ttl the maximum duration that the DNS resource records will be cached
          *
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index f4cc2ac..dba08a1 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -70,7 +70,8 @@
 
     private int mInterfaceIndex;
 
-    // The timestamp that all resource records associated with this service are considered invalid.
+    // The timestamp that one or more resource records associated with this service are considered
+    // invalid.
     @Nullable
     private Instant mExpirationTime;
 
@@ -497,7 +498,9 @@
     /**
      * Sets the timestamp after when this service is expired.
      *
-     * Note: only number of seconds of {@code expirationTime} is used.
+     * Note: the value after the decimal point (in unit of seconds) will be discarded. For
+     * example, {@code 30} seconds will be used when {@code Duration.ofSeconds(30L, 50_000L)}
+     * is provided.
      *
      * @hide
      */
diff --git a/framework/Android.bp b/framework/Android.bp
index aef0f74..52f2c7c 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -116,6 +116,7 @@
     static_libs: [
         "httpclient_api",
         "httpclient_impl",
+        "http_client_logging",
         // Framework-connectivity-pre-jarjar is identical to framework-connectivity
         // implementation, but without the jarjar rules. However, framework-connectivity
         // is not based on framework-connectivity-pre-jarjar, it's rebuilt from source
@@ -144,6 +145,7 @@
     ],
     impl_only_static_libs: [
         "httpclient_impl",
+        "http_client_logging",
     ],
 }
 
diff --git a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
index dfe5867..a80db85 100644
--- a/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
+++ b/framework/src/android/net/connectivity/ConnectivityCompatChanges.java
@@ -84,6 +84,21 @@
     @ChangeId
     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
     public static final long ENABLE_PLATFORM_MDNS_BACKEND = 270306772L;
+
+    /**
+     * Apps targeting Android V or higher receive network callbacks from local networks as default
+     *
+     * Apps targeting lower than {@link android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM} need
+     * to add {@link android.net.NetworkCapabilities#NET_CAPABILITY_LOCAL_NETWORK} to the
+     * {@link android.net.NetworkCapabilities} of the {@link android.net.NetworkRequest} to receive
+     * {@link android.net.ConnectivityManager.NetworkCallback} from local networks.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public static final long ENABLE_MATCH_LOCAL_NETWORK = 319212206L;
+
     private ConnectivityCompatChanges() {
     }
 }
diff --git a/netbpfload/loader.cpp b/netbpfload/loader.cpp
index c5077fb..3026655 100644
--- a/netbpfload/loader.cpp
+++ b/netbpfload/loader.cpp
@@ -571,6 +571,14 @@
 
 static bool mapMatchesExpectations(const unique_fd& fd, const string& mapName,
                                    const struct bpf_map_def& mapDef, const enum bpf_map_type type) {
+    // bpfGetFd... family of functions require at minimum a 4.14 kernel,
+    // so on 4.9-T kernels just pretend the map matches our expectations.
+    // Additionally we'll get almost equivalent test coverage on newer devices/kernels.
+    // This is because the primary failure mode we're trying to detect here
+    // is either a source code misconfiguration (which is likely kernel independent)
+    // or a newly introduced kernel feature/bug (which is unlikely to get backported to 4.9).
+    if (!isAtLeastKernelVersion(4, 14, 0)) return true;
+
     // Assuming fd is a valid Bpf Map file descriptor then
     // all the following should always succeed on a 4.14+ kernel.
     // If they somehow do fail, they'll return -1 (and set errno),
@@ -708,6 +716,16 @@
         }
 
         enum bpf_map_type type = md[i].type;
+        if (type == BPF_MAP_TYPE_DEVMAP && !isAtLeastKernelVersion(4, 14, 0)) {
+            // On Linux Kernels older than 4.14 this map type doesn't exist, but it can kind
+            // of be approximated: ARRAY has the same userspace api, though it is not usable
+            // by the same ebpf programs.  However, that's okay because the bpf_redirect_map()
+            // helper doesn't exist on 4.9-T anyway (so the bpf program would fail to load,
+            // and thus needs to be tagged as 4.14+ either way), so there's nothing useful you
+            // could do with a DEVMAP anyway (that isn't already provided by an ARRAY)...
+            // Hence using an ARRAY instead of a DEVMAP simply makes life easier for userspace.
+            type = BPF_MAP_TYPE_ARRAY;
+        }
         if (type == BPF_MAP_TYPE_DEVMAP_HASH && !isAtLeastKernelVersion(5, 4, 0)) {
             // On Linux Kernels older than 5.4 this map type doesn't exist, but it can kind
             // of be approximated: HASH has the same userspace visible api.
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index c162bcc..98c2d86 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -241,13 +241,10 @@
         }
 
         @Override
-        public void onDestroyed(@NonNull MdnsInterfaceSocket socket) {
-            for (int i = mAdvertiserRequests.size() - 1; i >= 0; i--) {
-                if (mAdvertiserRequests.valueAt(i).onAdvertiserDestroyed(socket)) {
-                    mAdvertiserRequests.removeAt(i);
-                }
-            }
-            mAllAdvertisers.remove(socket);
+        public void onAllServicesRemoved(@NonNull MdnsInterfaceSocket socket) {
+            if (DBG) { mSharedLog.i("onAllServicesRemoved: " + socket); }
+            // Try destroying the advertiser if all services has been removed
+            destroyAdvertiser(socket, false /* interfaceDestroyed */);
         }
     };
 
@@ -318,6 +315,30 @@
     }
 
     /**
+     * Destroys the advertiser for the interface indicated by {@code socket}.
+     *
+     * {@code interfaceDestroyed} should be set to {@code true} if this method is called because
+     * the associated interface has been destroyed.
+     */
+    private void destroyAdvertiser(MdnsInterfaceSocket socket, boolean interfaceDestroyed) {
+        InterfaceAdvertiserRequest advertiserRequest;
+
+        MdnsInterfaceAdvertiser advertiser = mAllAdvertisers.remove(socket);
+        if (advertiser != null) {
+            advertiser.destroyNow();
+            if (DBG) { mSharedLog.i("MdnsInterfaceAdvertiser is destroyed: " + advertiser); }
+        }
+
+        for (int i = mAdvertiserRequests.size() - 1; i >= 0; i--) {
+            advertiserRequest = mAdvertiserRequests.valueAt(i);
+            if (advertiserRequest.onAdvertiserDestroyed(socket, interfaceDestroyed)) {
+                if (DBG) { mSharedLog.i("AdvertiserRequest is removed: " + advertiserRequest); }
+                mAdvertiserRequests.removeAt(i);
+            }
+        }
+    }
+
+    /**
      * A request for a {@link MdnsInterfaceAdvertiser}.
      *
      * This class tracks services to be advertised on all sockets provided via a registered
@@ -336,13 +357,22 @@
         }
 
         /**
-         * Called when an advertiser was destroyed, after all services were unregistered and it sent
-         * exit announcements, or the interface is gone.
+         * Called when the interface advertiser associated with {@code socket} has been destroyed.
          *
-         * @return true if this {@link InterfaceAdvertiserRequest} should now be deleted.
+         * {@code interfaceDestroyed} should be set to {@code true} if this method is called because
+         * the associated interface has been destroyed.
+         *
+         * @return true if the {@link InterfaceAdvertiserRequest} should now be deleted
          */
-        boolean onAdvertiserDestroyed(@NonNull MdnsInterfaceSocket socket) {
+        boolean onAdvertiserDestroyed(
+                @NonNull MdnsInterfaceSocket socket, boolean interfaceDestroyed) {
             final MdnsInterfaceAdvertiser removedAdvertiser = mAdvertisers.remove(socket);
+            if (removedAdvertiser != null
+                    && !interfaceDestroyed && mPendingRegistrations.size() > 0) {
+                mSharedLog.wtf(
+                        "unexpected onAdvertiserDestroyed() when there are pending registrations");
+            }
+
             if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled && removedAdvertiser != null) {
                 final String interfaceName = removedAdvertiser.getSocketInterfaceName();
                 // If the interface is destroyed, stop all hardware offloading on that
@@ -528,7 +558,7 @@
         public void onInterfaceDestroyed(@NonNull SocketKey socketKey,
                 @NonNull MdnsInterfaceSocket socket) {
             final MdnsInterfaceAdvertiser advertiser = mAdvertisers.get(socket);
-            if (advertiser != null) advertiser.destroyNow();
+            if (advertiser != null) destroyAdvertiser(socket, true /* interfaceDestroyed */);
         }
 
         @Override
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index c2363c0..c1c7d5f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -102,12 +102,15 @@
                 @NonNull MdnsInterfaceAdvertiser advertiser, int serviceId, int conflictType);
 
         /**
-         * Called by the advertiser when it destroyed itself.
+         * Called when all services on this interface advertiser has already been removed and exit
+         * announcements have been sent.
          *
-         * This can happen after a call to {@link #destroyNow()}, or after all services were
-         * unregistered and the advertiser finished sending exit announcements.
+         * <p>It's guaranteed that there are no service registrations in the
+         * MdnsInterfaceAdvertiser when this callback is invoked.
+         *
+         * <p>This is typically listened by the {@link MdnsAdvertiser} to release the resources
          */
-        void onDestroyed(@NonNull MdnsInterfaceSocket socket);
+        void onAllServicesRemoved(@NonNull MdnsInterfaceSocket socket);
     }
 
     /**
@@ -149,10 +152,11 @@
         public void onFinished(@NonNull BaseAnnouncementInfo info) {
             if (info instanceof MdnsAnnouncer.ExitAnnouncementInfo) {
                 mRecordRepository.removeService(info.getServiceId());
-
-                if (mRecordRepository.getServicesCount() == 0) {
-                    destroyNow();
-                }
+                mCbHandler.post(() -> {
+                    if (mRecordRepository.getServicesCount() == 0) {
+                        mCb.onAllServicesRemoved(mSocket);
+                    }
+                });
             }
         }
     }
@@ -234,8 +238,7 @@
      * Start the advertiser.
      *
      * The advertiser will stop itself when all services are removed and exit announcements sent,
-     * notifying via {@link Callback#onDestroyed}. This can also be triggered manually via
-     * {@link #destroyNow()}.
+     * notifying via {@link Callback#onAllServicesRemoved}.
      */
     public void start() {
         mSocket.addPacketHandler(this);
@@ -283,8 +286,8 @@
         mAnnouncer.stop(id);
         final MdnsAnnouncer.ExitAnnouncementInfo exitInfo = mRecordRepository.exitService(id);
         if (exitInfo != null) {
-            // This effectively schedules destroyNow(), as it is to be called when the exit
-            // announcement finishes if there is no service left.
+            // This effectively schedules onAllServicesRemoved(), as it is to be called when the
+            // exit announcement finishes if there is no service left.
             // A non-zero exit announcement delay follows legacy mdnsresponder behavior, and is
             // also useful to ensure that when a host receives the exit announcement, the service
             // has been unregistered on all interfaces; so an announcement sent from interface A
@@ -294,9 +297,11 @@
         } else {
             // No exit announcement necessary: remove the service immediately.
             mRecordRepository.removeService(id);
-            if (mRecordRepository.getServicesCount() == 0) {
-                destroyNow();
-            }
+            mCbHandler.post(() -> {
+                if (mRecordRepository.getServicesCount() == 0) {
+                    mCb.onAllServicesRemoved(mSocket);
+                }
+            });
         }
     }
 
@@ -330,7 +335,8 @@
     /**
      * Destroy the advertiser immediately, not sending any exit announcement.
      *
-     * <p>Useful when the underlying network went away. This will trigger an onDestroyed callback.
+     * <p>This is typically called when all services on the interface are removed or when the
+     * underlying network went away.
      */
     public void destroyNow() {
         for (int serviceId : mRecordRepository.clearServices()) {
@@ -339,7 +345,6 @@
         }
         mReplySender.cancelAll();
         mSocket.removePacketHandler(this);
-        mCbHandler.post(() -> mCb.onDestroyed(mSocket));
     }
 
     /**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
index f60a95e..1ec9e39 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -94,57 +94,6 @@
     @NonNull
     private final Instant expirationTime;
 
-    /** Constructs a {@link MdnsServiceInfo} object with default values. */
-    public MdnsServiceInfo(
-            String serviceInstanceName,
-            String[] serviceType,
-            @Nullable List<String> subtypes,
-            String[] hostName,
-            int port,
-            @Nullable String ipv4Address,
-            @Nullable String ipv6Address,
-            @Nullable List<String> textStrings) {
-        this(
-                serviceInstanceName,
-                serviceType,
-                subtypes,
-                hostName,
-                port,
-                List.of(ipv4Address),
-                List.of(ipv6Address),
-                textStrings,
-                /* textEntries= */ null,
-                /* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
-                /* network= */ null,
-                /* expirationTime= */ Instant.MAX);
-    }
-
-    /** Constructs a {@link MdnsServiceInfo} object with default values. */
-    public MdnsServiceInfo(
-            String serviceInstanceName,
-            String[] serviceType,
-            List<String> subtypes,
-            String[] hostName,
-            int port,
-            @Nullable String ipv4Address,
-            @Nullable String ipv6Address,
-            @Nullable List<String> textStrings,
-            @Nullable List<TextEntry> textEntries) {
-        this(
-                serviceInstanceName,
-                serviceType,
-                subtypes,
-                hostName,
-                port,
-                List.of(ipv4Address),
-                List.of(ipv6Address),
-                textStrings,
-                textEntries,
-                /* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
-                /* network= */ null,
-                /* expirationTime= */ Instant.MAX);
-    }
-
     /**
      * Constructs a {@link MdnsServiceInfo} object with default values.
      *
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocket.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocket.java
index c51811b..653ea6c 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocket.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocket.java
@@ -58,7 +58,6 @@
     MdnsSocket(@NonNull MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider,
             MulticastSocket multicastSocket, SharedLog sharedLog) throws IOException {
         this.multicastNetworkInterfaceProvider = multicastNetworkInterfaceProvider;
-        this.multicastNetworkInterfaceProvider.startWatchingConnectivityChanges();
         this.multicastSocket = multicastSocket;
         this.sharedLog = sharedLog;
         // RFC Spec: https://tools.ietf.org/html/rfc6762
@@ -120,7 +119,6 @@
     public void close() {
         // This is a race with the use of the file descriptor (b/27403984).
         multicastSocket.close();
-        multicastNetworkInterfaceProvider.stopWatchingConnectivityChanges();
     }
 
     /**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
index 82c8c5b..7b71e43 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
@@ -106,6 +106,7 @@
     @Nullable private Timer checkMulticastResponseTimer;
     private final SharedLog sharedLog;
     @NonNull private final MdnsFeatureFlags mdnsFeatureFlags;
+    private final MulticastNetworkInterfaceProvider interfaceProvider;
 
     public MdnsSocketClient(@NonNull Context context, @NonNull MulticastLock multicastLock,
             SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
@@ -118,6 +119,7 @@
             unicastReceiverBuffer = null;
         }
         this.mdnsFeatureFlags = mdnsFeatureFlags;
+        this.interfaceProvider = new MulticastNetworkInterfaceProvider(context, sharedLog);
     }
 
     @Override
@@ -138,6 +140,7 @@
         cannotReceiveMulticastResponse.set(false);
 
         shouldStopSocketLoop = false;
+        interfaceProvider.startWatchingConnectivityChanges();
         try {
             // TODO (changed when importing code): consider setting thread stats tag
             multicastSocket = createMdnsSocket(MdnsConstants.MDNS_PORT, sharedLog);
@@ -183,6 +186,7 @@
         }
 
         multicastLock.release();
+        interfaceProvider.stopWatchingConnectivityChanges();
 
         shouldStopSocketLoop = true;
         waitForSendThreadToStop();
@@ -482,8 +486,7 @@
 
     @VisibleForTesting
     MdnsSocket createMdnsSocket(int port, SharedLog sharedLog) throws IOException {
-        return new MdnsSocket(new MulticastNetworkInterfaceProvider(context, sharedLog), port,
-                sharedLog);
+        return new MdnsSocket(interfaceProvider, port, sharedLog);
     }
 
     private void sendPackets(List<DatagramPacket> packets, MdnsSocket socket) {
diff --git a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
index 8598ac4..ca97d07 100644
--- a/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
+++ b/service-t/src/com/android/server/net/TrafficStatsRateLimitCache.java
@@ -19,12 +19,13 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.NetworkStats;
+import android.util.LruCache;
 
 import com.android.internal.annotations.GuardedBy;
 
 import java.time.Clock;
-import java.util.HashMap;
 import java.util.Objects;
+import java.util.function.Supplier;
 
 /**
  * A thread-safe cache for storing and retrieving {@link NetworkStats.Entry} objects,
@@ -39,10 +40,12 @@
      *
      * @param clock The {@link Clock} to use for determining timestamps.
      * @param expiryDurationMs The expiry duration in milliseconds.
+     * @param maxSize Maximum number of entries.
      */
-    TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs) {
+    TrafficStatsRateLimitCache(@NonNull Clock clock, long expiryDurationMs, int maxSize) {
         mClock = clock;
         mExpiryDurationMs = expiryDurationMs;
+        mMap = new LruCache<>(maxSize);
     }
 
     private static class TrafficStatsCacheKey {
@@ -81,7 +84,7 @@
     }
 
     @GuardedBy("mMap")
-    private final HashMap<TrafficStatsCacheKey, TrafficStatsCacheValue> mMap = new HashMap<>();
+    private final LruCache<TrafficStatsCacheKey, TrafficStatsCacheValue> mMap;
 
     /**
      * Retrieves a {@link NetworkStats.Entry} from the cache, associated with the given key.
@@ -105,6 +108,36 @@
     }
 
     /**
+     * Retrieves a {@link NetworkStats.Entry} from the cache, associated with the given key.
+     * If the entry is not found in the cache or has expired, computes it using the provided
+     * {@code supplier} and stores the result in the cache.
+     *
+     * @param iface The interface name to include in the cache key. {@code IFACE_ALL}
+     *              if not applicable.
+     * @param uid The UID to include in the cache key. {@code UID_ALL} if not applicable.
+     * @param supplier The {@link Supplier} to compute the {@link NetworkStats.Entry} if not found.
+     * @return The cached or computed {@link NetworkStats.Entry}, or null if not found, expired,
+     *         or if the {@code supplier} returns null.
+     */
+    @Nullable
+    NetworkStats.Entry getOrCompute(String iface, int uid,
+            @NonNull Supplier<NetworkStats.Entry> supplier) {
+        synchronized (mMap) {
+            final NetworkStats.Entry cachedValue = get(iface, uid);
+            if (cachedValue != null) {
+                return cachedValue;
+            }
+
+            // Entry not found or expired, compute it
+            final NetworkStats.Entry computedEntry = supplier.get();
+            if (computedEntry != null && !computedEntry.isEmpty()) {
+                put(iface, uid, computedEntry);
+            }
+            return computedEntry;
+        }
+    }
+
+    /**
      * Stores a {@link NetworkStats.Entry} in the cache, associated with the given key.
      *
      * @param iface The interface name to include in the cache key. Null if not applicable.
@@ -124,7 +157,7 @@
      */
     void clear() {
         synchronized (mMap) {
-            mMap.clear();
+            mMap.evictAll();
         }
     }
 
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 30b14b2..8f09a40 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -97,6 +97,8 @@
 import static android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY;
 import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST;
 import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST_ONLY;
+import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_LOCAL_NETWORK;
+import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_SELF_CERTIFIED_CAPABILITIES_DECLARATION;
 import static android.os.Process.INVALID_UID;
 import static android.os.Process.VPN_UID;
 import static android.system.OsConstants.ETH_P_ALL;
@@ -214,7 +216,6 @@
 import android.net.Uri;
 import android.net.VpnManager;
 import android.net.VpnTransportInfo;
-import android.net.connectivity.ConnectivityCompatChanges;
 import android.net.metrics.IpConnectivityLog;
 import android.net.metrics.NetworkEvent;
 import android.net.netd.aidl.NativeUidRangeConfig;
@@ -2607,7 +2608,7 @@
                 // Not the system, so it's an app requesting on its own behalf.
                 type = RequestType.RT_APP.getNumber();
             }
-            countPerType.put(type, countPerType.get(type, 0));
+            countPerType.put(type, countPerType.get(type, 0) + 1);
         }
         for (int i = countPerType.size() - 1; i >= 0; --i) {
             final RequestCountForType.Builder r = RequestCountForType.newBuilder();
@@ -3020,6 +3021,23 @@
         }
     }
 
+    private void maybeDisableLocalNetworkMatching(NetworkCapabilities nc, int callingUid) {
+        if (mDeps.isChangeEnabled(ENABLE_MATCH_LOCAL_NETWORK, callingUid)) {
+            return;
+        }
+        // If NET_CAPABILITY_LOCAL_NETWORK is not added to capability, request should not be
+        // satisfied by local networks.
+        if (!nc.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) {
+            nc.addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK);
+        }
+    }
+
+    private void restrictRequestNetworkCapabilitiesForCaller(NetworkCapabilities nc,
+            int callingUid, String callerPackageName) {
+        restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callerPackageName);
+        maybeDisableLocalNetworkMatching(nc, callingUid);
+    }
+
     @Override
     public @RestrictBackgroundStatus int getRestrictBackgroundStatusByCaller() {
         enforceAccessPermission();
@@ -7708,10 +7726,12 @@
                 //  the state of the app when the request is filed, but we never change the
                 //  request if the app changes network state. http://b/29964605
                 enforceMeteredApnPolicy(networkCapabilities);
+                maybeDisableLocalNetworkMatching(networkCapabilities, callingUid);
                 break;
             case LISTEN_FOR_BEST:
                 enforceAccessPermission();
                 networkCapabilities = new NetworkCapabilities(networkCapabilities);
+                maybeDisableLocalNetworkMatching(networkCapabilities, callingUid);
                 break;
             default:
                 throw new IllegalArgumentException("Unsupported request type " + reqType);
@@ -7798,7 +7818,7 @@
         final UserHandle user = UserHandle.getUserHandleForUid(callingUid);
         // Only run the check if the change is enabled.
         if (!mDeps.isChangeEnabled(
-                ConnectivityCompatChanges.ENABLE_SELF_CERTIFIED_CAPABILITIES_DECLARATION,
+                ENABLE_SELF_CERTIFIED_CAPABILITIES_DECLARATION,
                 callingPackageName, user)) {
             return false;
         }
@@ -7950,8 +7970,8 @@
         ensureRequestableCapabilities(networkCapabilities);
         ensureSufficientPermissionsForRequest(networkCapabilities,
                 Binder.getCallingPid(), callingUid, callingPackageName);
-        restrictRequestUidsForCallerAndSetRequestorInfo(networkCapabilities,
-                callingUid, callingPackageName);
+        restrictRequestNetworkCapabilitiesForCaller(
+                networkCapabilities, callingUid, callingPackageName);
 
         NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, TYPE_NONE,
                 nextNetworkRequestId(), NetworkRequest.Type.REQUEST);
@@ -8011,7 +8031,7 @@
         NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
         ensureSufficientPermissionsForRequest(networkCapabilities,
                 Binder.getCallingPid(), callingUid, callingPackageName);
-        restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName);
+        restrictRequestNetworkCapabilitiesForCaller(nc, callingUid, callingPackageName);
         // Apps without the CHANGE_NETWORK_STATE permission can't use background networks, so
         // make all their listens include NET_CAPABILITY_FOREGROUND. That way, they will get
         // onLost and onAvailable callbacks when networks move in and out of the background.
@@ -8044,7 +8064,7 @@
         ensureSufficientPermissionsForRequest(networkCapabilities,
                 Binder.getCallingPid(), callingUid, callingPackageName);
         final NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
-        restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName);
+        restrictRequestNetworkCapabilitiesForCaller(nc, callingUid, callingPackageName);
 
         NetworkRequest networkRequest = new NetworkRequest(nc, TYPE_NONE, nextNetworkRequestId(),
                 NetworkRequest.Type.LISTEN);
@@ -12017,7 +12037,7 @@
         // This NetworkCapabilities is only used for matching to Networks. Clear out its owner uid
         // and administrator uids to be safe.
         final NetworkCapabilities nc = new NetworkCapabilities(request.networkCapabilities);
-        restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName);
+        restrictRequestNetworkCapabilitiesForCaller(nc, callingUid, callingPackageName);
 
         final NetworkRequest requestWithId =
                 new NetworkRequest(
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index baff09b..ac922cd 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -39,6 +39,9 @@
 // Android U / 14 (api level 34) - various new program types added
 #define BPFLOADER_U_VERSION 37u
 
+// Android V / 15 (api level 35) - this bpfloader should eventually go back to T
+#define BPFLOADER_MAINLINE_VERSION 42u
+
 /* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
  * before #include "bpf_helpers.h" to change which bpfloaders will
  * process the resulting .o file.
@@ -111,10 +114,12 @@
 #define KVER_NONE KVER_(0)
 #define KVER_4_14 KVER(4, 14, 0)
 #define KVER_4_19 KVER(4, 19, 0)
-#define KVER_5_4 KVER(5, 4, 0)
-#define KVER_5_8 KVER(5, 8, 0)
-#define KVER_5_9 KVER(5, 9, 0)
+#define KVER_5_4  KVER(5, 4, 0)
+#define KVER_5_8  KVER(5, 8, 0)
+#define KVER_5_9  KVER(5, 9, 0)
 #define KVER_5_15 KVER(5, 15, 0)
+#define KVER_6_1  KVER(6, 1, 0)
+#define KVER_6_6  KVER(6, 6, 0)
 #define KVER_INF KVER_(0xFFFFFFFFu)
 
 #define KVER_IS_AT_LEAST(kver, a, b, c) ((kver).kver >= KVER(a, b, c).kver)
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index 454940f..df2e7a6 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -1017,8 +1017,8 @@
             // This needs to be done before testing  private DNS because checkStrictModePrivateDns
             // will set the private DNS server to a nonexistent name, which will cause validation to
             // fail and could cause the default network to switch (e.g., from wifi to cellular).
-            systemDefaultCallback.assertNoCallback();
-            otherUidCallback.assertNoCallback();
+            assertNoCallbackExceptCapOrLpChange(systemDefaultCallback);
+            assertNoCallbackExceptCapOrLpChange(otherUidCallback);
         }
 
         checkStrictModePrivateDns();
@@ -1026,6 +1026,11 @@
         receiver.unregisterQuietly();
     }
 
+    private void assertNoCallbackExceptCapOrLpChange(TestableNetworkCallback callback) {
+        callback.assertNoCallback(c -> !(c instanceof CallbackEntry.CapabilitiesChanged
+                || c instanceof CallbackEntry.LinkPropertiesChanged));
+    }
+
     @Test
     public void testAppAllowed() throws Exception {
         assumeTrue(supportedHardware());
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index dbececf..8dbcf2f 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -2108,6 +2108,46 @@
     }
 
     @Test
+    fun testRegisterService_registerImmediatelyAfterUnregister_serviceFound() {
+        val info1 = makeTestServiceInfo(network = testNetwork1.network).apply {
+            serviceName = "service11111"
+            port = 11111
+        }
+        val info2 = makeTestServiceInfo(network = testNetwork1.network).apply {
+            serviceName = "service22222"
+            port = 22222
+        }
+        val registrationRecord1 = NsdRegistrationRecord()
+        val discoveryRecord1 = NsdDiscoveryRecord()
+        val registrationRecord2 = NsdRegistrationRecord()
+        val discoveryRecord2 = NsdDiscoveryRecord()
+        tryTest {
+            registerService(registrationRecord1, info1)
+            nsdManager.discoverServices(serviceType,
+                    NsdManager.PROTOCOL_DNS_SD, testNetwork1.network, { it.run() },
+                    discoveryRecord1)
+            discoveryRecord1.waitForServiceDiscovered(info1.serviceName,
+                    serviceType, testNetwork1.network)
+            nsdManager.stopServiceDiscovery(discoveryRecord1)
+
+            nsdManager.unregisterService(registrationRecord1)
+            registerService(registrationRecord2, info2)
+            nsdManager.discoverServices(serviceType,
+                    NsdManager.PROTOCOL_DNS_SD, testNetwork1.network, { it.run() },
+                    discoveryRecord2)
+            val infoDiscovered = discoveryRecord2.waitForServiceDiscovered(info2.serviceName,
+                    serviceType, testNetwork1.network)
+            val infoResolved = resolveService(infoDiscovered)
+            assertEquals(22222, infoResolved.port)
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(discoveryRecord2)
+            discoveryRecord2.expectCallback<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord2)
+        }
+    }
+
+    @Test
     fun testServiceTypeClientRemovedAfterSocketDestroyed() {
         val si = makeTestServiceInfo(testNetwork1.network)
         // Register service on testNetwork1
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index 3928961..1023173 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -47,6 +47,7 @@
 
     // Change to system current when TetheringManager move to bootclass path.
     platform_apis: true,
+    min_sdk_version: "30",
     host_required: ["net-tests-utils-host-common"],
 }
 
@@ -80,8 +81,8 @@
 
 // Tethering CTS tests for development and release. These tests always target the platform SDK
 // version, and are subject to all the restrictions appropriate to that version. Before SDK
-// finalization, these tests have a min_sdk_version of 10000, and cannot be installed on release
-// devices.
+// finalization, these tests have a min_sdk_version of 10000, but they can still be installed on
+// release devices as their min_sdk_version is set to a production version.
 android_test {
     name: "CtsTetheringTest",
     defaults: ["CtsTetheringTestDefaults"],
@@ -93,6 +94,14 @@
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
+        "mts-dnsresolver",
+        "mts-networking",
+        "mts-tethering",
+        "mts-wifi",
+        "mcts-dnsresolver",
+        "mcts-networking",
+        "mcts-tethering",
+        "mcts-wifi",
         "general-tests",
     ],
 
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 361d68c..035f607 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -66,6 +66,8 @@
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.tryTest
+import java.util.function.BiConsumer
+import java.util.function.Consumer
 import kotlin.test.assertEquals
 import kotlin.test.assertNotNull
 import kotlin.test.assertTrue
@@ -87,8 +89,6 @@
 import org.mockito.Mockito.mock
 import org.mockito.MockitoAnnotations
 import org.mockito.Spy
-import java.util.function.Consumer
-import java.util.function.BiConsumer
 
 const val SERVICE_BIND_TIMEOUT_MS = 5_000L
 const val TEST_TIMEOUT_MS = 10_000L
@@ -225,6 +225,7 @@
         override fun getSystemProperties() = mock(MockableSystemProperties::class.java)
         override fun makeNetIdManager() = TestNetIdManager()
         override fun getBpfNetMaps(context: Context?, netd: INetd?) = mock(BpfNetMaps::class.java)
+        override fun isChangeEnabled(changeId: Long, uid: Int) = true
 
         override fun makeMultinetworkPolicyTracker(
             c: Context,
diff --git a/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
index 332f2a3..c491f37 100644
--- a/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
+++ b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
@@ -54,17 +54,17 @@
         assertEquals(beforeParcel.advertisingConfig, afterParcel.advertisingConfig)
     }
 
-@Test
-fun testBuilder_setNullTtl_success() {
-    val info = NsdServiceInfo().apply {
-        serviceType = "_ipp._tcp"
-    }
-    val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
-            .setTtl(null)
-            .build()
+    @Test
+    fun testBuilder_setNullTtl_success() {
+        val info = NsdServiceInfo().apply {
+            serviceType = "_ipp._tcp"
+        }
+        val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+                .setTtl(null)
+                .build()
 
-    assertNull(request.ttl)
-}
+        assertNull(request.ttl)
+    }
 
     @Test
     fun testBuilder_setPropertiesSuccess() {
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 76a649e..27c4561 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -240,7 +240,7 @@
 
         AdvertisingRequest capturedRequest = getAdvertisingRequest(
                 req -> verify(mServiceConn).registerService(anyInt(), req.capture()));
-        assertEquals(request, capturedRequest);
+        assertEquals(request.getTtl(), capturedRequest.getTtl());
     }
 
     private void doTestRegisterService() throws Exception {
diff --git a/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
index 53baee1..8a9286f 100644
--- a/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
+++ b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
@@ -1,5 +1,6 @@
 package com.android.metrics
 
+import android.net.ConnectivityThread
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.CONNECTIVITY_MANAGED_CAPABILITIES
 import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
@@ -15,13 +16,20 @@
 import android.net.NetworkCapabilities.NET_ENTERPRISE_ID_3
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
 import android.net.NetworkScore
 import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
 import android.net.NetworkScore.POLICY_EXITING
 import android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY
 import android.os.Build
 import android.os.Handler
+import android.os.Process
+import android.os.Process.SYSTEM_UID
 import android.stats.connectivity.MeteredState
+import android.stats.connectivity.RequestType
+import android.stats.connectivity.RequestType.RT_APP
+import android.stats.connectivity.RequestType.RT_SYSTEM
+import android.stats.connectivity.RequestType.RT_SYSTEM_ON_BEHALF_OF_APP
 import android.stats.connectivity.ValidatedState
 import androidx.test.filters.SmallTest
 import com.android.net.module.util.BitUtils
@@ -31,11 +39,13 @@
 import com.android.server.connectivity.FullScore.POLICY_IS_UNMETERED
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
-import org.junit.Test
-import org.junit.runner.RunWith
+import com.android.testutils.TestableNetworkCallback
 import java.util.concurrent.CompletableFuture
 import kotlin.test.assertEquals
 import kotlin.test.fail
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
 
 private fun <T> Handler.onHandler(f: () -> T): T {
     val future = CompletableFuture<T>()
@@ -80,7 +90,7 @@
 @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
 class ConnectivitySampleMetricsTest : CSTest() {
     @Test
-    fun testSampleConnectivityState() {
+    fun testSampleConnectivityState_Network() {
         val wifi1Caps = NetworkCapabilities.Builder()
                 .addTransportType(TRANSPORT_WIFI)
                 .addCapability(NET_CAPABILITY_NOT_METERED)
@@ -179,4 +189,61 @@
                         "expected ${expectedWifi2Policies.toPolicyString()}, " +
                         "found ${foundWifi2.scorePolicies.toPolicyString()}")
     }
+
+    private fun fileNetworkRequest(requestType: RequestType, requestCount: Int, uid: Int? = null) {
+        if (uid != null) {
+            deps.setCallingUid(uid)
+        }
+        try {
+            repeat(requestCount) {
+                when (requestType) {
+                    RT_APP, RT_SYSTEM -> cm.requestNetwork(
+                            NetworkRequest.Builder().build(),
+                            TestableNetworkCallback()
+                    )
+
+                    RT_SYSTEM_ON_BEHALF_OF_APP -> cm.registerDefaultNetworkCallbackForUid(
+                            Process.myUid(),
+                            TestableNetworkCallback(),
+                            Handler(ConnectivityThread.getInstanceLooper()))
+
+                    else -> fail("invalid requestType: " + requestType)
+                }
+            }
+        } finally {
+            deps.unmockCallingUid()
+        }
+    }
+
+
+    @Test
+    fun testSampleConnectivityState_NetworkRequest() {
+        val requestCount = 5
+        fileNetworkRequest(RT_APP, requestCount);
+        fileNetworkRequest(RT_SYSTEM, requestCount, SYSTEM_UID);
+        fileNetworkRequest(RT_SYSTEM_ON_BEHALF_OF_APP, requestCount, SYSTEM_UID);
+
+        val stats = csHandler.onHandler { service.sampleConnectivityState() }
+
+        assertEquals(3, stats.networkRequestCount.requestCountForTypeList.size)
+        val appRequest = stats.networkRequestCount.requestCountForTypeList.find {
+            it.requestType == RT_APP
+        } ?: fail("Can't find RT_APP request")
+        val systemRequest = stats.networkRequestCount.requestCountForTypeList.find {
+            it.requestType == RT_SYSTEM
+        } ?: fail("Can't find RT_SYSTEM request")
+        val systemOnBehalfOfAppRequest = stats.networkRequestCount.requestCountForTypeList.find {
+            it.requestType == RT_SYSTEM_ON_BEHALF_OF_APP
+        } ?: fail("Can't find RT_SYSTEM_ON_BEHALF_OF_APP request")
+
+        // Verify request count is equal or larger than the number of request this test filed
+        // since ConnectivityService internally files network requests
+        assertTrue("Unexpected RT_APP count, expected >= $requestCount, " +
+                "found ${appRequest.requestCount}", appRequest.requestCount >= requestCount)
+        assertTrue("Unexpected RT_SYSTEM count, expected >= $requestCount, " +
+                "found ${systemRequest.requestCount}", systemRequest.requestCount >= requestCount)
+        assertTrue("Unexpected RT_SYSTEM_ON_BEHALF_OF_APP count, expected >= $requestCount, " +
+                "found ${systemOnBehalfOfAppRequest.requestCount}",
+                systemOnBehalfOfAppRequest.requestCount >= requestCount)
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index b8ebf0f..df48f6c 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -286,7 +286,6 @@
 
         postSync { socketCb.onInterfaceDestroyed(TEST_SOCKETKEY_1, mockSocket1) }
         verify(mockInterfaceAdvertiser1).destroyNow()
-        postSync { intAdvCbCaptor.value.onDestroyed(mockSocket1) }
         verify(cb).onOffloadStop(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO_NO_SUBTYPE2))
     }
 
@@ -364,10 +363,10 @@
         verify(cb).onOffloadStop(eq(TEST_INTERFACE1), eq(OFFLOAD_SERVICEINFO))
         verify(cb).onOffloadStop(eq(TEST_INTERFACE2), eq(OFFLOAD_SERVICEINFO))
 
-        // Interface advertisers call onDestroyed after sending exit announcements
-        postSync { intAdvCbCaptor1.value.onDestroyed(mockSocket1) }
+        // Interface advertisers call onAllServicesRemoved after sending exit announcements
+        postSync { intAdvCbCaptor1.value.onAllServicesRemoved(mockSocket1) }
         verify(socketProvider, never()).unrequestSocket(any())
-        postSync { intAdvCbCaptor2.value.onDestroyed(mockSocket2) }
+        postSync { intAdvCbCaptor2.value.onAllServicesRemoved(mockSocket2) }
         verify(socketProvider).unrequestSocket(socketCb)
     }
 
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index 28608bb..69fec85 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -179,7 +179,7 @@
         // Exit announcements finish: the advertiser has no left service and destroys itself
         announceCb.onFinished(testExitInfo)
         thread.waitForIdle(TIMEOUT_MS)
-        verify(cb).onDestroyed(socket)
+        verify(cb).onAllServicesRemoved(socket)
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index 8d1dff6..c69b1e1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -38,6 +38,7 @@
 import java.net.InetSocketAddress
 import java.net.NetworkInterface
 import java.util.Collections
+import java.time.Duration
 import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
@@ -146,7 +147,7 @@
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         assertEquals(0, repository.servicesCount)
         assertEquals(-1,
-                repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */))
+                repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, Duration.ofSeconds(50)))
         assertEquals(1, repository.servicesCount)
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -169,7 +170,7 @@
         assertEquals(MdnsServiceRecord(expectedName,
                 0L /* receiptTimeMillis */,
                 false /* cacheFlush */,
-                SHORT_TTL /* ttlMillis */,
+                50_000L /* ttlMillis */,
                 0 /* servicePriority */, 0 /* serviceWeight */,
                 TEST_PORT, TEST_HOSTNAME), packet.authorityRecords[0])
 
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
index 8740e80..4ce8ba6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
@@ -54,7 +54,8 @@
                         "192.168.1.1",
                         "2001::1",
                         List.of("vn=Google Inc.", "mn=Google Nest Hub Max"),
-                        /* textEntries= */ null);
+                        /* textEntries= */ null,
+                        INTERFACE_INDEX_UNSPECIFIED);
 
         assertTrue(info.getAttributeByKey("vn").equals("Google Inc."));
         assertTrue(info.getAttributeByKey("mn").equals("Google Nest Hub Max"));
@@ -73,7 +74,8 @@
                         "2001::1",
                         /* textStrings= */ null,
                         List.of(MdnsServiceInfo.TextEntry.fromString("vn=Google Inc."),
-                                MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max")));
+                                MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max")),
+                        INTERFACE_INDEX_UNSPECIFIED);
 
         assertTrue(info.getAttributeByKey("vn").equals("Google Inc."));
         assertTrue(info.getAttributeByKey("mn").equals("Google Nest Hub Max"));
@@ -93,7 +95,8 @@
                         List.of("vn=Alphabet Inc.", "mn=Google Nest Hub Max", "id=12345"),
                         List.of(
                                 MdnsServiceInfo.TextEntry.fromString("vn=Google Inc."),
-                                MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max")));
+                                MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max")),
+                        INTERFACE_INDEX_UNSPECIFIED);
 
         assertEquals(Map.of("vn", "Google Inc.", "mn", "Google Nest Hub Max"),
                 info.getAttributes());
@@ -113,7 +116,8 @@
                         List.of("vn=Alphabet Inc.", "mn=Google Nest Hub Max", "id=12345"),
                         List.of(MdnsServiceInfo.TextEntry.fromString("vn=Google Inc."),
                                 MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max"),
-                                MdnsServiceInfo.TextEntry.fromString("mn=Google WiFi Router")));
+                                MdnsServiceInfo.TextEntry.fromString("mn=Google WiFi Router")),
+                        INTERFACE_INDEX_UNSPECIFIED);
 
         assertEquals(Map.of("vn", "Google Inc.", "mn", "Google Nest Hub Max"),
                 info.getAttributes());
@@ -131,7 +135,8 @@
                         "192.168.1.1",
                         "2001::1",
                         List.of("KEY=Value"),
-                        /* textEntries= */ null);
+                        /* textEntries= */ null,
+                        INTERFACE_INDEX_UNSPECIFIED);
 
         assertEquals("Value", info.getAttributeByKey("key"));
         assertEquals("Value", info.getAttributeByKey("KEY"));
@@ -150,7 +155,9 @@
                         12345,
                         "192.168.1.1",
                         "2001::1",
-                        List.of());
+                        List.of(),
+                        /* textEntries= */ null,
+                        INTERFACE_INDEX_UNSPECIFIED);
 
         assertEquals(info.getInterfaceIndex(), INTERFACE_INDEX_UNSPECIFIED);
     }
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
index 8b7ab71..7ced1cb 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
@@ -26,14 +26,17 @@
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.Manifest.permission;
 import android.annotation.RequiresPermission;
 import android.content.Context;
+import android.net.ConnectivityManager;
 import android.net.wifi.WifiManager;
 import android.net.wifi.WifiManager.MulticastLock;
 import android.text.format.DateUtils;
@@ -48,6 +51,7 @@
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatchers;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -71,6 +75,7 @@
 
     @Mock private Context mContext;
     @Mock private WifiManager mockWifiManager;
+    @Mock private ConnectivityManager mockConnectivityManager;
     @Mock private MdnsSocket mockMulticastSocket;
     @Mock private MdnsSocket mockUnicastSocket;
     @Mock private MulticastLock mockMulticastLock;
@@ -84,6 +89,9 @@
     public void setup() throws RuntimeException, IOException {
         MockitoAnnotations.initMocks(this);
 
+        doReturn(mockConnectivityManager).when(mContext).getSystemService(
+                Context.CONNECTIVITY_SERVICE);
+
         when(mockWifiManager.createMulticastLock(ArgumentMatchers.anyString()))
                 .thenReturn(mockMulticastLock);
 
@@ -320,19 +328,25 @@
 
     @Test
     public void testStartStop() throws IOException {
-        for (int i = 0; i < 5; i++) {
+        for (int i = 1; i <= 5; i++) {
             mdnsClient.startDiscovery();
 
             Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
             Thread socketThread = mdnsClient.sendThread;
+            final ArgumentCaptor<ConnectivityManager.NetworkCallback> cbCaptor =
+                    ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
 
             assertTrue(multicastReceiverThread.isAlive());
             assertTrue(socketThread.isAlive());
+            verify(mockConnectivityManager, times(i))
+                    .registerNetworkCallback(any(), cbCaptor.capture());
 
             mdnsClient.stopDiscovery();
 
             assertFalse(multicastReceiverThread.isAlive());
             assertFalse(socketThread.isAlive());
+            verify(mockConnectivityManager, times(i))
+                    .unregisterNetworkCallback(cbCaptor.getValue());
         }
     }
 
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index c1730a4..83fff87 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -38,6 +38,7 @@
 import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
 import android.net.NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK
 import android.net.RouteInfo
+import android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_LOCAL_NETWORK
 import android.os.Build
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
@@ -47,12 +48,15 @@
 import kotlin.test.assertFailsWith
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.never
 import org.mockito.Mockito.timeout
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
 
 private const val TIMEOUT_MS = 200L
 private const val MEDIUM_TIMEOUT_MS = 1_000L
@@ -88,10 +92,10 @@
 class CSLocalAgentTests : CSTest() {
     val multicastRoutingConfigMinScope =
                 MulticastRoutingConfig.Builder(MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE, 4)
-                .build();
+                .build()
     val multicastRoutingConfigSelected =
                 MulticastRoutingConfig.Builder(MulticastRoutingConfig.FORWARD_SELECTED)
-                .build();
+                .build()
     val upstreamSelectorAny = NetworkRequest.Builder()
                 .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
                 .build()
@@ -205,6 +209,9 @@
                 nc = nc(TRANSPORT_THREAD, NET_CAPABILITY_LOCAL_NETWORK),
                 lp = lp(name),
                 lnc = localNetworkConfig,
+                score = FromS(NetworkScore.Builder()
+                        .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+                        .build())
         )
         return localAgent
     }
@@ -219,9 +226,12 @@
                 nc = nc(TRANSPORT_CELLULAR, NET_CAPABILITY_INTERNET))
     }
 
-    private fun sendLocalNetworkConfig(localAgent: CSAgentWrapper,
-                upstreamSelector: NetworkRequest?, upstreamConfig: MulticastRoutingConfig,
-                downstreamConfig: MulticastRoutingConfig) {
+    private fun sendLocalNetworkConfig(
+            localAgent: CSAgentWrapper,
+            upstreamSelector: NetworkRequest?,
+            upstreamConfig: MulticastRoutingConfig,
+            downstreamConfig: MulticastRoutingConfig
+    ) {
         val newLnc = LocalNetworkConfig.Builder()
                 .setUpstreamSelector(upstreamSelector)
                 .setUpstreamMulticastRoutingConfig(upstreamConfig)
@@ -458,7 +468,6 @@
         wifiAgent.disconnect()
     }
 
-
     @Test
     fun testUnregisterUpstreamAfterReplacement_SameIfaceName() {
         doTestUnregisterUpstreamAfterReplacement(true)
@@ -824,4 +833,59 @@
 
         listenCb.expect<Lost>()
     }
+
+    fun doTestLocalNetworkRequest(
+            request: NetworkRequest,
+            enableMatchLocalNetwork: Boolean,
+            expectCallback: Boolean
+    ) {
+        deps.setBuildSdk(VERSION_V)
+        deps.setChangeIdEnabled(enableMatchLocalNetwork, ENABLE_MATCH_LOCAL_NETWORK)
+
+        val requestCb = TestableNetworkCallback()
+        val listenCb = TestableNetworkCallback()
+        cm.requestNetwork(request, requestCb)
+        cm.registerNetworkCallback(request, listenCb)
+
+        val localAgent = createLocalAgent("local0", FromS(LocalNetworkConfig.Builder().build()))
+        localAgent.connect()
+
+        if (expectCallback) {
+            requestCb.expectAvailableCallbacks(localAgent.network, validated = false)
+            listenCb.expectAvailableCallbacks(localAgent.network, validated = false)
+        } else {
+            waitForIdle()
+            requestCb.assertNoCallback(timeoutMs = 0)
+            listenCb.assertNoCallback(timeoutMs = 0)
+        }
+        localAgent.disconnect()
+    }
+
+    @Test
+    fun testLocalNetworkRequest() {
+        val request = NetworkRequest.Builder().build()
+        // If ENABLE_MATCH_LOCAL_NETWORK is false, request is not satisfied by local network
+        doTestLocalNetworkRequest(
+                request,
+                enableMatchLocalNetwork = false,
+                expectCallback = false)
+        // If ENABLE_MATCH_LOCAL_NETWORK is true, request is satisfied by local network
+        doTestLocalNetworkRequest(
+                request,
+                enableMatchLocalNetwork = true,
+                expectCallback = true)
+    }
+
+    @Test
+    fun testLocalNetworkRequest_withCapability() {
+        val request = NetworkRequest.Builder().addCapability(NET_CAPABILITY_LOCAL_NETWORK).build()
+        doTestLocalNetworkRequest(
+                request,
+                enableMatchLocalNetwork = false,
+                expectCallback = true)
+        doTestLocalNetworkRequest(
+                request,
+                enableMatchLocalNetwork = true,
+                expectCallback = true)
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index d7343b1..7007b16 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -28,6 +28,7 @@
 import android.net.NetworkAgent
 import android.net.NetworkAgentConfig
 import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.net.NetworkProvider
@@ -39,6 +40,9 @@
 import com.android.testutils.RecorderCallback.CallbackEntry.Available
 import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.TestableNetworkCallback
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.test.assertEquals
+import kotlin.test.fail
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.anyInt
@@ -46,9 +50,6 @@
 import org.mockito.Mockito.doNothing
 import org.mockito.Mockito.verify
 import org.mockito.stubbing.Answer
-import java.util.concurrent.atomic.AtomicInteger
-import kotlin.test.assertEquals
-import kotlin.test.fail
 
 const val SHORT_TIMEOUT_MS = 200L
 
@@ -140,6 +141,9 @@
         val request = NetworkRequest.Builder().apply {
             clearCapabilities()
             if (nc.transportTypes.isNotEmpty()) addTransportType(nc.transportTypes[0])
+            if (nc.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) {
+                addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+            }
         }.build()
         val cb = TestableNetworkCallback()
         mgr.registerNetworkCallback(request, cb)
@@ -166,6 +170,9 @@
         val request = NetworkRequest.Builder().apply {
             clearCapabilities()
             if (nc.transportTypes.isNotEmpty()) addTransportType(nc.transportTypes[0])
+            if (nc.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) {
+                addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+            }
         }.build()
         val cb = TestableNetworkCallback(timeoutMs = SHORT_TIMEOUT_MS)
         mgr.registerNetworkCallback(request, cb)
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index 3b83c41..1966cb1 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -17,6 +17,7 @@
 package com.android.server
 
 import android.app.AlarmManager
+import android.app.AppOpsManager
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
@@ -42,6 +43,7 @@
 import android.net.NetworkProvider
 import android.net.NetworkScore
 import android.net.PacProxyManager
+import android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_LOCAL_NETWORK
 import android.net.networkstack.NetworkStackClientBase
 import android.os.BatteryStatsManager
 import android.os.Bundle
@@ -53,7 +55,6 @@
 import android.permission.PermissionManager.PermissionResult
 import android.telephony.TelephonyManager
 import android.testing.TestableContext
-import android.util.ArraySet
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.internal.app.IBatteryStats
 import com.android.internal.util.test.BroadcastInterceptingContext
@@ -75,8 +76,8 @@
 import java.util.concurrent.Executors
 import java.util.concurrent.LinkedBlockingQueue
 import java.util.concurrent.TimeUnit
-import java.util.function.Consumer
 import java.util.function.BiConsumer
+import java.util.function.Consumer
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
 import kotlin.test.fail
@@ -103,6 +104,8 @@
 internal const val VERSION_V = 5
 internal const val VERSION_MAX = VERSION_V
 
+internal const val CALLING_UID_UNMOCKED = Process.INVALID_UID
+
 private fun NetworkCapabilities.getLegacyType() =
         when (transportTypes.getOrElse(0) { TRANSPORT_WIFI }) {
             TRANSPORT_BLUETOOTH -> ConnectivityManager.TYPE_BLUETOOTH
@@ -176,6 +179,7 @@
     val systemConfigManager = makeMockSystemConfigManager()
     val batteryStats = mock<IBatteryStats>()
     val batteryManager = BatteryStatsManager(batteryStats)
+    val appOpsManager = mock<AppOpsManager>()
     val telephonyManager = mock<TelephonyManager>().also {
         doReturn(true).`when`(it).isDataCapable()
     }
@@ -263,7 +267,7 @@
                 enabledFeatures[name] ?: fail("Unmocked feature $name, see CSTest.enabledFeatures")
 
         // Mocked change IDs
-        private val enabledChangeIds = ArraySet<Long>()
+        private val enabledChangeIds = arrayListOf(ENABLE_MATCH_LOCAL_NETWORK)
         fun setChangeIdEnabled(enabled: Boolean, changeId: Long) {
             // enabledChangeIds is read on the handler thread and maybe the test thread, so
             // make sure both threads see it before continuing.
@@ -298,6 +302,19 @@
         override fun isAtLeastT() = if (isSdkUnmocked) super.isAtLeastT() else sdkLevel >= VERSION_T
         override fun isAtLeastU() = if (isSdkUnmocked) super.isAtLeastU() else sdkLevel >= VERSION_U
         override fun isAtLeastV() = if (isSdkUnmocked) super.isAtLeastV() else sdkLevel >= VERSION_V
+
+        private var callingUid = CALLING_UID_UNMOCKED
+
+        fun unmockCallingUid() {
+            setCallingUid(CALLING_UID_UNMOCKED)
+        }
+
+        fun setCallingUid(callingUid: Int) {
+            visibleOnHandlerThread(csHandler) { this.callingUid = callingUid }
+        }
+
+        override fun getCallingUid() =
+                if (callingUid == CALLING_UID_UNMOCKED) super.getCallingUid() else callingUid
     }
 
     inner class CSContext(base: Context) : BroadcastInterceptingContext(base) {
@@ -398,6 +415,7 @@
             Context.TELEPHONY_SERVICE -> telephonyManager
             Context.BATTERY_STATS_SERVICE -> batteryManager
             Context.STATS_MANAGER -> null // Stats manager is final and can't be mocked
+            Context.APP_OPS_SERVICE -> appOpsManager
             else -> super.getSystemService(serviceName)
         }
 
diff --git a/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
index 27e6f96..99f762d 100644
--- a/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
+++ b/tests/unit/java/com/android/server/net/TrafficStatsRateLimitCacheTest.kt
@@ -16,30 +16,35 @@
 
 package com.android.server.net
 
-import android.net.NetworkStats
+import android.net.NetworkStats.Entry
 import com.android.testutils.DevSdkIgnoreRunner
 import java.time.Clock
+import java.util.function.Supplier
 import kotlin.test.assertEquals
 import kotlin.test.assertNull
+import kotlin.test.fail
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 
 @RunWith(DevSdkIgnoreRunner::class)
 class TrafficStatsRateLimitCacheTest {
     companion object {
         private const val expiryDurationMs = 1000L
+        private const val maxSize = 2
     }
 
     private val clock = mock(Clock::class.java)
-    private val entry = mock(NetworkStats.Entry::class.java)
-    private val cache = TrafficStatsRateLimitCache(clock, expiryDurationMs)
+    private val entry = mock(Entry::class.java)
+    private val cache = TrafficStatsRateLimitCache(clock, expiryDurationMs, maxSize)
 
     @Test
     fun testGet_returnsEntryIfNotExpired() {
         cache.put("iface", 2, entry)
-        `when`(clock.millis()).thenReturn(500L) // Set clock to before expiry
+        doReturn(500L).`when`(clock).millis() // Set clock to before expiry
         val result = cache.get("iface", 2)
         assertEquals(entry, result)
     }
@@ -47,7 +52,7 @@
     @Test
     fun testGet_returnsNullIfExpired() {
         cache.put("iface", 2, entry)
-        `when`(clock.millis()).thenReturn(2000L) // Set clock to after expiry
+        doReturn(2000L).`when`(clock).millis() // Set clock to after expiry
         assertNull(cache.get("iface", 2))
     }
 
@@ -59,8 +64,8 @@
 
     @Test
     fun testPutAndGet_retrievesCorrectEntryForDifferentKeys() {
-        val entry1 = mock(NetworkStats.Entry::class.java)
-        val entry2 = mock(NetworkStats.Entry::class.java)
+        val entry1 = mock(Entry::class.java)
+        val entry2 = mock(Entry::class.java)
 
         cache.put("iface1", 2, entry1)
         cache.put("iface2", 4, entry2)
@@ -71,8 +76,8 @@
 
     @Test
     fun testPut_overridesExistingEntry() {
-        val entry1 = mock(NetworkStats.Entry::class.java)
-        val entry2 = mock(NetworkStats.Entry::class.java)
+        val entry1 = mock(Entry::class.java)
+        val entry2 = mock(Entry::class.java)
 
         cache.put("iface", 2, entry1)
         cache.put("iface", 2, entry2) // Put with the same key
@@ -81,6 +86,62 @@
     }
 
     @Test
+    fun testPut_removeLru() {
+        // Assumes max size is 2. Verify eldest entry get removed.
+        val entry1 = mock(Entry::class.java)
+        val entry2 = mock(Entry::class.java)
+        val entry3 = mock(Entry::class.java)
+
+        cache.put("iface1", 2, entry1)
+        cache.put("iface2", 4, entry2)
+        cache.put("iface3", 8, entry3)
+
+        assertNull(cache.get("iface1", 2))
+        assertEquals(entry2, cache.get("iface2", 4))
+        assertEquals(entry3, cache.get("iface3", 8))
+    }
+
+    @Test
+    fun testGetOrCompute_cacheHit() {
+        val entry1 = mock(Entry::class.java)
+
+        cache.put("iface1", 2, entry1)
+
+        // Set clock to before expiry.
+        doReturn(500L).`when`(clock).millis()
+
+        // Now call getOrCompute
+        val result = cache.getOrCompute("iface1", 2) {
+            fail("Supplier should not be called")
+        }
+
+        // Assertions
+        assertEquals(entry1, result) // Should get the cached entry.
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    @Test
+    fun testGetOrCompute_cacheMiss() {
+        val entry1 = mock(Entry::class.java)
+
+        cache.put("iface1", 2, entry1)
+
+        // Set clock to after expiry.
+        doReturn(1500L).`when`(clock).millis()
+
+        // Mock the supplier to return our network stats entry.
+        val supplier = mock(Supplier::class.java) as Supplier<Entry>
+        doReturn(entry1).`when`(supplier).get()
+
+        // Now call getOrCompute.
+        val result = cache.getOrCompute("iface1", 2, supplier)
+
+        // Assertions.
+        assertEquals(entry1, result) // Should get the cached entry.
+        verify(supplier).get()
+    }
+
+    @Test
     fun testClear() {
         cache.put("iface", 2, entry)
         cache.clear()
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 0623b87..1b36d2b 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -489,7 +489,14 @@
         @Override
         public void onAvailable(@NonNull Network network) {
             checkOnHandlerThread();
-            Log.i(TAG, "Thread network available: " + network);
+            Log.i(TAG, "Thread network is available: " + network);
+        }
+
+        @Override
+        public void onLost(@NonNull Network network) {
+            checkOnHandlerThread();
+            Log.i(TAG, "Thread network is lost: " + network);
+            disableBorderRouting();
         }
 
         @Override
@@ -504,7 +511,7 @@
                             + localNetworkInfo
                             + "}");
             if (localNetworkInfo.getUpstreamNetwork() == null) {
-                mUpstreamNetwork = null;
+                disableBorderRouting();
                 return;
             }
             if (!localNetworkInfo.getUpstreamNetwork().equals(mUpstreamNetwork)) {
@@ -523,6 +530,7 @@
                         // requirement.
                         .clearCapabilities()
                         .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+                        .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
                         .build(),
                 new ThreadNetworkCallback(),
                 mHandler);
@@ -936,23 +944,22 @@
             mBorderRouterConfig.isBorderRoutingEnabled = true;
 
             mOtDaemon.configureBorderRouter(
-                    mBorderRouterConfig,
-                    new IOtStatusReceiver.Stub() {
-                        @Override
-                        public void onSuccess() {
-                            Log.i(TAG, "configure border router successfully");
-                        }
+                    mBorderRouterConfig, new ConfigureBorderRouterStatusReceiver());
+        } catch (RemoteException | IOException e) {
+            Log.w(TAG, "Failed to enable border routing", e);
+        }
+    }
 
-                        @Override
-                        public void onError(int i, String s) {
-                            Log.w(
-                                    TAG,
-                                    String.format(
-                                            "failed to configure border router: %d %s", i, s));
-                        }
-                    });
-        } catch (Exception e) {
-            Log.w(TAG, "enableBorderRouting failed: " + e);
+    private void disableBorderRouting() {
+        mUpstreamNetwork = null;
+        mBorderRouterConfig.infraInterfaceName = null;
+        mBorderRouterConfig.infraInterfaceIcmp6Socket = null;
+        mBorderRouterConfig.isBorderRoutingEnabled = false;
+        try {
+            mOtDaemon.configureBorderRouter(
+                    mBorderRouterConfig, new ConfigureBorderRouterStatusReceiver());
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed to disable border routing", e);
         }
     }
 
@@ -1073,6 +1080,20 @@
         }
     }
 
+    private static final class ConfigureBorderRouterStatusReceiver extends IOtStatusReceiver.Stub {
+        public ConfigureBorderRouterStatusReceiver() {}
+
+        @Override
+        public void onSuccess() {
+            Log.i(TAG, "Configured border router successfully");
+        }
+
+        @Override
+        public void onError(int i, String s) {
+            Log.w(TAG, String.format("Failed to configure border router: %d %s", i, s));
+        }
+    }
+
     /**
      * Handles and forwards Thread daemon callbacks. This class must be accessed from the thread of
      * {@code mHandler}.
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index ee21405..88ee47e 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -177,7 +177,7 @@
     }
 
     @Test
-    public void unicastRouting_infraDevicePingTheadDeviceOmr_replyReceived() throws Exception {
+    public void unicastRouting_infraDevicePingThreadDeviceOmr_replyReceived() throws Exception {
         /*
          * <pre>
          * Topology:
@@ -199,6 +199,30 @@
     }
 
     @Test
+    public void unicastRouting_afterFactoryResetInfraDevicePingThreadDeviceOmr_replyReceived()
+            throws Exception {
+        /*
+         * <pre>
+         * Topology:
+         *                 infra network                       Thread
+         * infra device -------------------- Border Router -------------- Full Thread device
+         *                                   (Cuttlefish)
+         * </pre>
+         */
+
+        // Form the network.
+        mOtCtl.factoryReset();
+        startBrLeader();
+        startInfraDevice();
+        FullThreadDevice ftd = mFtds.get(0);
+        startFtdChild(ftd);
+
+        mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
+
+        assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
+    }
+
+    @Test
     public void unicastRouting_borderRouterSendsUdpToThreadDevice_datagramReceived()
             throws Exception {
         assumeTrue(isSimulatedThreadRadioSupported());