Merge "Support checking DnsResolver version in isFeatureSupported." into main
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 8ed5ac0..8d96066 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -215,6 +215,7 @@
             "android.net.nsd",
             "android.net.thread",
             "android.net.wear",
+            "android.net.http.internal",
         ],
     },
 }
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 39a7540..6e00756 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -49,7 +49,10 @@
         "//packages/modules/NetworkStack/tests:__subpackages__",
         "//packages/modules/Wifi/service/tests/wifitests",
     ],
-    stub_only_libs: ["framework-connectivity.stubs.module_lib"],
+    stub_only_libs: [
+        "framework-connectivity.stubs.module_lib",
+        "sdk_module-lib_current_framework-wifi",
+    ],
 
     jarjar_rules: ":framework-tethering-jarjar-rules",
     installable: true,
@@ -97,10 +100,17 @@
     srcs: [
         ":framework-tethering-srcs",
     ],
-    libs: ["framework-connectivity.stubs.module_lib"],
+    libs: [
+        "framework-connectivity.stubs.module_lib",
+        "sdk_module-lib_current_framework-wifi",
+    ],
+    static_libs: [
+        "com.android.net.flags-aconfig-java",
+    ],
     aidl: {
         include_dirs: [
             "packages/modules/Connectivity/framework/aidl-export",
+            "packages/modules/Wifi/framework/aidl-export",
         ],
     },
     apex_available: ["com.android.tethering"],
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index cccafd5..3efaac2 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -102,6 +102,7 @@
     method public int getConnectivityScope();
     method @Nullable public android.net.LinkAddress getLocalIpv4Address();
     method public boolean getShouldShowEntitlementUi();
+    method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") @Nullable public android.net.wifi.SoftApConfiguration getSoftApConfiguration();
     method public int getTetheringType();
     method public boolean isExemptFromEntitlementCheck();
     method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -114,6 +115,7 @@
     method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setConnectivityScope(int);
     method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setExemptFromEntitlementCheck(boolean);
     method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setShouldShowEntitlementUi(boolean);
+    method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setSoftApConfiguration(@Nullable android.net.wifi.SoftApConfiguration);
     method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setStaticIpv4Addresses(@NonNull android.net.LinkAddress, @NonNull android.net.LinkAddress);
   }
 
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 0f5a014..5aca642 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -26,6 +26,8 @@
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.content.Context;
+import android.net.wifi.SoftApConfiguration;
+import android.net.wifi.WifiManager;
 import android.os.Bundle;
 import android.os.ConditionVariable;
 import android.os.IBinder;
@@ -38,6 +40,7 @@
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.net.flags.Flags;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -62,16 +65,6 @@
  */
 @SystemApi
 public class TetheringManager {
-    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
-    // available here
-    /** @hide */
-    public static class Flags {
-        static final String TETHERING_REQUEST_WITH_SOFT_AP_CONFIG =
-                "com.android.net.flags.tethering_request_with_soft_ap_config";
-        static final String TETHERING_REQUEST_VIRTUAL =
-                "com.android.net.flags.tethering_request_virtual";
-    }
-
     private static final String TAG = TetheringManager.class.getSimpleName();
     private static final int DEFAULT_TIMEOUT_MS = 60_000;
     private static final long CONNECTOR_POLL_INTERVAL_MILLIS = 200L;
@@ -204,7 +197,7 @@
      * AVF(Android Virtualization Framework).
      * @hide
      */
-    @FlaggedApi(Flags.TETHERING_REQUEST_VIRTUAL)
+    @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_VIRTUAL)
     @SystemApi
     public static final int TETHERING_VIRTUAL = 7;
 
@@ -705,7 +698,7 @@
         /**
          * @hide
          */
-        @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+        @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
         public TetheringRequest(@NonNull final TetheringRequestParcel request) {
             mRequestParcel = request;
         }
@@ -714,7 +707,7 @@
             mRequestParcel = in.readParcelable(TetheringRequestParcel.class.getClassLoader());
         }
 
-        @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+        @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
         @NonNull
         public static final Creator<TetheringRequest> CREATOR = new Creator<>() {
             @Override
@@ -728,13 +721,13 @@
             }
         };
 
-        @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+        @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
         @Override
         public int describeContents() {
             return 0;
         }
 
-        @FlaggedApi(Flags.TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+        @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
         @Override
         public void writeToParcel(@NonNull Parcel dest, int flags) {
             dest.writeParcelable(mRequestParcel, flags);
@@ -753,6 +746,7 @@
                 mBuilderParcel.exemptFromEntitlementCheck = false;
                 mBuilderParcel.showProvisioningUi = true;
                 mBuilderParcel.connectivityScope = getDefaultConnectivityScope(type);
+                mBuilderParcel.softApConfig = null;
             }
 
             /**
@@ -812,6 +806,30 @@
                 return this;
             }
 
+            /**
+             * Set the desired SoftApConfiguration for {@link #TETHERING_WIFI}. If this is null or
+             * not set, then the persistent tethering SoftApConfiguration from
+             * {@link WifiManager#getSoftApConfiguration()} will be used.
+             * </p>
+             * If TETHERING_WIFI is already enabled and a new request is made with a different
+             * SoftApConfiguration, the request will be accepted if the device can support an
+             * additional tethering Wi-Fi AP interface. Otherwise, the request will be rejected.
+             *
+             * @param softApConfig SoftApConfiguration to use.
+             * @throws IllegalArgumentException if the tethering type isn't TETHERING_WIFI.
+             */
+            @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+            @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+            @NonNull
+            public Builder setSoftApConfiguration(@Nullable SoftApConfiguration softApConfig) {
+                if (mBuilderParcel.tetheringType != TETHERING_WIFI) {
+                    throw new IllegalArgumentException(
+                            "SoftApConfiguration can only be set for TETHERING_WIFI");
+                }
+                mBuilderParcel.softApConfig = softApConfig;
+                return this;
+            }
+
             /** Build {@link TetheringRequest} with the currently set configuration. */
             @NonNull
             public TetheringRequest build() {
@@ -893,6 +911,15 @@
         }
 
         /**
+         * Get the desired SoftApConfiguration of the request, if one was specified.
+         */
+        @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+        @Nullable
+        public SoftApConfiguration getSoftApConfiguration() {
+            return mRequestParcel.softApConfig;
+        }
+
+        /**
          * Get a TetheringRequestParcel from the configuration
          * @hide
          */
@@ -905,9 +932,10 @@
             return "TetheringRequest [ type= " + mRequestParcel.tetheringType
                     + ", localIPv4Address= " + mRequestParcel.localIPv4Address
                     + ", staticClientAddress= " + mRequestParcel.staticClientAddress
-                    + ", exemptFromEntitlementCheck= "
-                    + mRequestParcel.exemptFromEntitlementCheck + ", showProvisioningUi= "
-                    + mRequestParcel.showProvisioningUi + " ]";
+                    + ", exemptFromEntitlementCheck= " + mRequestParcel.exemptFromEntitlementCheck
+                    + ", showProvisioningUi= " + mRequestParcel.showProvisioningUi
+                    + ", softApConfig= " + mRequestParcel.softApConfig
+                    + " ]";
         }
 
         @Override
@@ -921,7 +949,8 @@
                     && Objects.equals(parcel.staticClientAddress, otherParcel.staticClientAddress)
                     && parcel.exemptFromEntitlementCheck == otherParcel.exemptFromEntitlementCheck
                     && parcel.showProvisioningUi == otherParcel.showProvisioningUi
-                    && parcel.connectivityScope == otherParcel.connectivityScope;
+                    && parcel.connectivityScope == otherParcel.connectivityScope
+                    && Objects.equals(parcel.softApConfig, otherParcel.softApConfig);
         }
 
         @Override
@@ -929,7 +958,7 @@
             TetheringRequestParcel parcel = getParcel();
             return Objects.hash(parcel.tetheringType, parcel.localIPv4Address,
                     parcel.staticClientAddress, parcel.exemptFromEntitlementCheck,
-                    parcel.showProvisioningUi, parcel.connectivityScope);
+                    parcel.showProvisioningUi, parcel.connectivityScope, parcel.softApConfig);
         }
     }
 
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
index f13c970..ea7a353 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
@@ -17,6 +17,7 @@
 package android.net;
 
 import android.net.LinkAddress;
+import android.net.wifi.SoftApConfiguration;
 
 /**
  * Configuration details for requesting tethering.
@@ -29,4 +30,5 @@
     boolean exemptFromEntitlementCheck;
     boolean showProvisioningUi;
     int connectivityScope;
+    SoftApConfiguration softApConfig;
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index ac4d8b1..2202106 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -65,6 +65,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.DeviceConfigUtils;
 import com.android.networkstack.tethering.UpstreamNetworkState;
 
 import java.util.ArrayList;
@@ -86,6 +88,11 @@
     private static final String SETTINGS_PKG_NAME = "com.android.settings";
     private static final String SYSTEMUI_PKG_NAME = "com.android.systemui";
     private static final String GMS_PKG_NAME = "com.google.android.gms";
+    /**
+     * A feature flag to control whether upstream data usage metrics should be enabled.
+     */
+    private static final String TETHER_UPSTREAM_DATA_USAGE_METRICS =
+            "tether_upstream_data_usage_metrics";
     private final SparseArray<NetworkTetheringReported.Builder> mBuilderMap = new SparseArray<>();
     private final SparseArray<Long> mDownstreamStartTime = new SparseArray<Long>();
     private final ArrayList<RecordUpstreamEvent> mUpstreamEventList = new ArrayList<>();
@@ -119,6 +126,16 @@
         public long timeNow() {
             return System.currentTimeMillis();
         }
+
+        /**
+         * Indicates whether {@link #TETHER_UPSTREAM_DATA_USAGE_METRICS} is enabled.
+         */
+        public boolean isUpstreamDataUsageMetricsEnabled(Context context) {
+            // Getting data usage requires building a NetworkTemplate. However, the
+            // NetworkTemplate#Builder API was introduced in Android T.
+            return SdkLevel.isAtLeastT() && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+                    context, TETHER_UPSTREAM_DATA_USAGE_METRICS);
+        }
     }
 
     /**
@@ -135,16 +152,36 @@
         mDependencies = dependencies;
     }
 
+    private static class DataUsage {
+        final long mTxBytes;
+        final long mRxBytes;
+
+        DataUsage(long txBytes, long rxBytes) {
+            mTxBytes = txBytes;
+            mRxBytes = rxBytes;
+        }
+
+        public long getTxBytes() {
+            return mTxBytes;
+        }
+
+        public long getRxBytes() {
+            return mRxBytes;
+        }
+    }
+
     private static class RecordUpstreamEvent {
-        public final long mStartTime;
-        public final long mStopTime;
-        public final UpstreamType mUpstreamType;
+        final long mStartTime;
+        final long mStopTime;
+        final UpstreamType mUpstreamType;
+        final DataUsage mDataUsage;
 
         RecordUpstreamEvent(final long startTime, final long stopTime,
-                final UpstreamType upstream) {
+                final UpstreamType upstream, final DataUsage dataUsage) {
             mStartTime = startTime;
             mStopTime = stopTime;
             mUpstreamType = upstream;
+            mDataUsage = dataUsage;
         }
     }
 
@@ -182,6 +219,15 @@
         statsBuilder.setErrorCode(errorCodeToEnum(errCode));
     }
 
+    private DataUsage calculateDataUsage(@Nullable UpstreamType upstream) {
+        if (upstream != null && mDependencies.isUpstreamDataUsageMetricsEnabled(mContext)
+                && isUsageSupportedForUpstreamType(upstream)) {
+            // TODO: Implement data usage calculation for the upstream type.
+            return new DataUsage(0L, 0L);
+        }
+        return new DataUsage(0L, 0L);
+    }
+
     /**
      * Update the list of upstream types and their duration whenever the current upstream type
      * changes.
@@ -193,8 +239,9 @@
 
         final long newTime = mDependencies.timeNow();
         if (mCurrentUpstream != null) {
+            final DataUsage dataUsage = calculateDataUsage(upstream);
             mUpstreamEventList.add(new RecordUpstreamEvent(mCurrentUpStreamStartTime, newTime,
-                    mCurrentUpstream));
+                    mCurrentUpstream, dataUsage));
         }
         mCurrentUpstream = upstream;
         mCurrentUpStreamStartTime = newTime;
@@ -245,13 +292,14 @@
             final long startTime = Math.max(downstreamStartTime, event.mStartTime);
             // Handle completed upstream events.
             addUpstreamEvent(upstreamEventsBuilder, startTime, event.mStopTime,
-                    event.mUpstreamType, 0L /* txBytes */, 0L /* rxBytes */);
+                    event.mUpstreamType, event.mDataUsage.mTxBytes, event.mDataUsage.mRxBytes);
         }
         final long startTime = Math.max(downstreamStartTime, mCurrentUpStreamStartTime);
         final long stopTime = mDependencies.timeNow();
         // Handle the last upstream event.
+        final DataUsage dataUsage = calculateDataUsage(mCurrentUpstream);
         addUpstreamEvent(upstreamEventsBuilder, startTime, stopTime, mCurrentUpstream,
-                0L /* txBytes */, 0L /* rxBytes */);
+                dataUsage.mTxBytes, dataUsage.mRxBytes);
         statsBuilder.setUpstreamEvents(upstreamEventsBuilder);
         statsBuilder.setDurationMillis(stopTime - downstreamStartTime);
     }
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 3944a8a..1eb6255 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -26,7 +26,9 @@
 import static android.net.TetheringManager.TETHERING_ETHERNET;
 import static android.net.TetheringTester.buildTcpPacket;
 import static android.net.TetheringTester.buildUdpPacket;
+import static android.net.TetheringTester.buildUdpPackets;
 import static android.net.TetheringTester.isAddressIpv4;
+import static android.net.TetheringTester.isExpectedFragmentIpPacket;
 import static android.net.TetheringTester.isExpectedIcmpPacket;
 import static android.net.TetheringTester.isExpectedTcpPacket;
 import static android.net.TetheringTester.isExpectedUdpPacket;
@@ -58,12 +60,14 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.SystemClock;
+import android.util.ArrayMap;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.FragmentHeader;
 import com.android.net.module.util.structs.Ipv6Header;
 import com.android.testutils.HandlerUtils;
 import com.android.testutils.TapPacketReader;
@@ -678,6 +682,57 @@
         });
     }
 
+    protected void sendDownloadFragmentedUdpPackets(@NonNull final Inet6Address srcIp,
+            @NonNull final Inet6Address dstIp, @NonNull final TetheringTester tester,
+            @NonNull final ByteBuffer payload, int l2mtu) throws Exception {
+        final List<ByteBuffer> testPackets = buildUdpPackets(null /* srcMac */, null /* dstMac */,
+                srcIp, dstIp, REMOTE_PORT, LOCAL_PORT, payload, l2mtu);
+        assertTrue("No packet fragmentation occurs", testPackets.size() > 1);
+
+        short id = 0;
+        final ArrayMap<Short, ByteBuffer> fragmentPayloads = new ArrayMap<>();
+        for (ByteBuffer testPacket : testPackets) {
+            Struct.parse(Ipv6Header.class, testPacket);
+            final FragmentHeader fragmentHeader = Struct.parse(FragmentHeader.class, testPacket);
+            // Conversion of IPv6's fragmentOffset field to IPv4's flagsAndFragmentOffset field.
+            // IPv6 Fragment Header:
+            //   '13 bits of offset in multiples of 8' + 2 zero bits + more fragment bit
+            //      0                   1                   2                   3
+            //      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+            //     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            //     |  Next Header  |   Reserved    |      Fragment Offset    |Res|M|
+            //     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            //     |                         Identification                        |
+            //     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            // IPv4 Header:
+            //   zero bit + don't frag bit + more frag bit + '13 bits of offset in multiples of 8'
+            //      0                   1                   2                   3
+            //      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+            //     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            //     |Version|  IHL  |Type of Service|          Total Length         |
+            //     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            //     |         Identification        |Flags|      Fragment Offset    |
+            //     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            //     +                           . . .                               +
+            //     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            short offset = (short) (((fragmentHeader.fragmentOffset & 0x1) << 13)
+                    | (fragmentHeader.fragmentOffset >> 3));
+            // RFC6145: for fragment id, copied from the low-order 16 bits in the identification
+            //          field in the Fragment Header.
+            id = (short) (fragmentHeader.identification & 0xffff);
+            final byte[] fragmentPayload = new byte[testPacket.remaining()];
+            testPacket.get(fragmentPayload);
+            testPacket.flip();
+            fragmentPayloads.put(offset, ByteBuffer.wrap(fragmentPayload));
+        }
+
+        final short fragId = id;
+        tester.verifyDownloadBatch(testPackets, p -> {
+            Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+            return isExpectedFragmentIpPacket(p, fragId, fragmentPayloads);
+        });
+    }
+
     protected void sendDownloadPacketTcp(@NonNull final InetAddress srcIp,
             @NonNull final InetAddress dstIp, short seq, short ack, byte tcpFlags,
             @NonNull final ByteBuffer payload, @NonNull final TetheringTester tester,
diff --git a/Tethering/tests/integration/base/android/net/TetheringTester.java b/Tethering/tests/integration/base/android/net/TetheringTester.java
index ae4ae55..b152b4c 100644
--- a/Tethering/tests/integration/base/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/base/android/net/TetheringTester.java
@@ -27,6 +27,7 @@
 import static android.system.OsConstants.IPPROTO_IPV6;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
+
 import static com.android.net.module.util.DnsPacket.ANSECTION;
 import static com.android.net.module.util.DnsPacket.DnsHeader;
 import static com.android.net.module.util.DnsPacket.DnsRecord;
@@ -53,6 +54,7 @@
 import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
 import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
 import static com.android.net.module.util.NetworkStackConstants.TCPHDR_SYN;
+
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
@@ -91,6 +93,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 import java.util.Random;
 import java.util.concurrent.TimeoutException;
 import java.util.function.Predicate;
@@ -497,6 +500,51 @@
         });
     }
 
+    /**
+     * Checks if the given raw packet data represents an expected fragmented IP packet.
+     *
+     * @param rawPacket the raw packet data to check.
+     * @param id the identification field of the fragmented IP packet.
+     * @param expectedPayloads a map of fragment offsets to their corresponding payload data.
+     * @return true if the packet is a valid fragmented IP packet with matching payload fragments;
+     *         false otherwise.
+     */
+    public static boolean isExpectedFragmentIpPacket(@NonNull final byte[] rawPacket, int id,
+            @NonNull final Map<Short, ByteBuffer> expectedPayloads) {
+        final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
+        try {
+            // Validate Ethernet header and IPv4 header.
+            if (!hasExpectedEtherHeader(buf, true /* isIpv4 */)) return false;
+            final Ipv4Header ipv4Header = Struct.parse(Ipv4Header.class, buf);
+            if (ipv4Header.protocol != (byte) IPPROTO_UDP) return false;
+            if (ipv4Header.id != id) return false;
+            // Validate payload data which expected at a specific fragment offset.
+            final ByteBuffer expectedPayload =
+                    expectedPayloads.get(ipv4Header.flagsAndFragmentOffset);
+            if (expectedPayload == null) return false;
+            if (buf.remaining() != expectedPayload.limit()) return false;
+            // Validate UDP header (which located in the 1st fragment).
+            // TODO: Validate the checksum field in UDP header. Currently, it'll be altered by NAT.
+            if ((ipv4Header.flagsAndFragmentOffset & 0x1FFF) == 0) {
+                final UdpHeader receivedUdpHeader = Struct.parse(UdpHeader.class, buf);
+                final UdpHeader expectedUdpHeader = Struct.parse(UdpHeader.class, expectedPayload);
+                if (receivedUdpHeader == null || expectedUdpHeader == null) return false;
+                if (receivedUdpHeader.srcPort != expectedUdpHeader.srcPort
+                        || receivedUdpHeader.dstPort != expectedUdpHeader.dstPort
+                        || receivedUdpHeader.length != expectedUdpHeader.length) {
+                    return false;
+                }
+                return true;
+            }
+            // Check the contents of the remaining payload.
+            return Arrays.equals(getRemaining(buf),
+                    getRemaining(expectedPayload.asReadOnlyBuffer()));
+        } catch (Exception e) {
+            // A failed packet parsing indicates that the packet is not a fragmented IPv4 packet.
+            return false;
+        }
+    }
+
     // |expectedPayload| is copied as read-only because the caller may reuse it.
     // See hasExpectedDnsMessage.
     public static boolean isExpectedUdpDnsPacket(@NonNull final byte[] rawPacket, boolean hasEth,
@@ -683,10 +731,10 @@
     }
 
     @NonNull
-    public static ByteBuffer buildUdpPacket(
+    public static List<ByteBuffer> buildUdpPackets(
             @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
             @NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
-            short srcPort, short dstPort, @Nullable final ByteBuffer payload)
+            short srcPort, short dstPort, @Nullable final ByteBuffer payload, int l2mtu)
             throws Exception {
         final int ipProto = getIpProto(srcIp, dstIp);
         final boolean hasEther = (srcMac != null && dstMac != null);
@@ -720,7 +768,30 @@
             payload.clear();
         }
 
-        return packetBuilder.finalizePacket();
+        return l2mtu == 0
+                ? Arrays.asList(packetBuilder.finalizePacket())
+                : packetBuilder.finalizePacket(l2mtu);
+    }
+
+    /**
+     * Builds a UDP packet.
+     *
+     * @param srcMac the source MAC address.
+     * @param dstMac the destination MAC address.
+     * @param srcIp the source IP address.
+     * @param dstIp the destination IP address.
+     * @param srcPort the source port number.
+     * @param dstPort the destination port number.
+     * @param payload the optional payload data to be included in the packet.
+     * @return a ByteBuffer containing the constructed UDP packet.
+     */
+    @NonNull
+    public static ByteBuffer buildUdpPacket(
+            @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
+            @NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
+            short srcPort, short dstPort, @Nullable final ByteBuffer payload)
+            throws Exception {
+        return buildUdpPackets(srcMac, dstMac, srcIp, dstIp, srcPort, dstPort, payload, 0).get(0);
     }
 
     @NonNull
@@ -994,6 +1065,28 @@
         return verifyPacketNotNull("Download fail", getDownloadPacket(filter));
     }
 
+    /**
+     * Sends a batch of download packets and verifies against a specified filtering condition.
+     *
+     * This method is designed for testing fragmented packets. All packets are sent before
+     * verification because the kernel buffers fragments until the last one is received.
+     * Captured packets are then verified against the provided filter.
+     *
+     * @param packets the list of ByteBuffers containing the packets to send.
+     * @param filter a Predicate that defines the filtering condition to apply to each received
+     *               packet. If the filter returns true for a packet's data, it is considered to
+     *               meet the verification criteria.
+     */
+    public void verifyDownloadBatch(final List<ByteBuffer> packets, final Predicate<byte[]> filter)
+            throws Exception {
+        for (ByteBuffer packet : packets) {
+            sendDownloadPacket(packet);
+        }
+        for (int i = 0; i < packets.size(); ++i) {
+            verifyPacketNotNull("Download fail", getDownloadPacket(filter));
+        }
+    }
+
     // Send DHCPDISCOVER to DHCP server to see if DHCP server is still alive to handle
     // the upcoming DHCP packets. This method should be only used when we know the DHCP
     // server has been created successfully before.
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 9cdba2f..049f5f0 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -33,6 +33,9 @@
 import static com.android.net.module.util.HexDump.dumpHexString;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN;
 import static com.android.testutils.DeviceInfoUtils.KVersion;
 import static com.android.testutils.TestPermissionUtil.runAsShell;
 
@@ -62,6 +65,10 @@
 import com.android.net.module.util.BpfDump;
 import com.android.net.module.util.Ipv6Utils;
 import com.android.net.module.util.Struct;
+import com.android.net.module.util.bpf.ClatEgress4Key;
+import com.android.net.module.util.bpf.ClatEgress4Value;
+import com.android.net.module.util.bpf.ClatIngress6Key;
+import com.android.net.module.util.bpf.ClatIngress6Value;
 import com.android.net.module.util.bpf.Tether4Key;
 import com.android.net.module.util.bpf.Tether4Value;
 import com.android.net.module.util.bpf.TetherStatsKey;
@@ -122,6 +129,8 @@
     private static final int TX_UDP_PACKET_SIZE = 30;
     private static final int TX_UDP_PACKET_COUNT = 123;
 
+    private static final String DUMPSYS_CLAT_RAWMAP_EGRESS4_ARG = "clatEgress4RawBpfMap";
+    private static final String DUMPSYS_CLAT_RAWMAP_INGRESS6_ARG = "clatIngress6RawBpfMap";
     private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
     private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
     private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
@@ -901,11 +910,10 @@
 
     @NonNull
     private <K extends Struct, V extends Struct> HashMap<K, V> dumpAndParseRawMap(
-            Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+            Class<K> keyClass, Class<V> valueClass, @NonNull String service, @NonNull String[] args)
             throws Exception {
-        final String[] args = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG, mapArg};
         final String rawMapStr = runAsShell(DUMP, () ->
-                DumpTestUtils.dumpService(Context.TETHERING_SERVICE, args));
+                DumpTestUtils.dumpService(service, args));
         final HashMap<K, V> map = new HashMap<>();
 
         for (final String line : rawMapStr.split(LINE_DELIMITER)) {
@@ -918,10 +926,10 @@
 
     @Nullable
     private <K extends Struct, V extends Struct> HashMap<K, V> pollRawMapFromDump(
-            Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+            Class<K> keyClass, Class<V> valueClass, @NonNull String service, @NonNull String[] args)
             throws Exception {
         for (int retryCount = 0; retryCount < DUMP_POLLING_MAX_RETRY; retryCount++) {
-            final HashMap<K, V> map = dumpAndParseRawMap(keyClass, valueClass, mapArg);
+            final HashMap<K, V> map = dumpAndParseRawMap(keyClass, valueClass, service, args);
             if (!map.isEmpty()) return map;
 
             Thread.sleep(DUMP_POLLING_INTERVAL_MS);
@@ -977,8 +985,10 @@
         Thread.sleep(UDP_STREAM_SLACK_MS);
 
         // [1] Verify IPv4 upstream rule map.
+        final String[] upstreamArgs = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG,
+                DUMPSYS_RAWMAP_ARG_UPSTREAM4};
         final HashMap<Tether4Key, Tether4Value> upstreamMap = pollRawMapFromDump(
-                Tether4Key.class, Tether4Value.class, DUMPSYS_RAWMAP_ARG_UPSTREAM4);
+                Tether4Key.class, Tether4Value.class, Context.TETHERING_SERVICE, upstreamArgs);
         assertNotNull(upstreamMap);
         assertEquals(1, upstreamMap.size());
 
@@ -1017,8 +1027,10 @@
         }
 
         // Dump stats map to verify.
+        final String[] statsArgs = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG,
+                DUMPSYS_RAWMAP_ARG_STATS};
         final HashMap<TetherStatsKey, TetherStatsValue> statsMap = pollRawMapFromDump(
-                TetherStatsKey.class, TetherStatsValue.class, DUMPSYS_RAWMAP_ARG_STATS);
+                TetherStatsKey.class, TetherStatsValue.class, Context.TETHERING_SERVICE, statsArgs);
         assertNotNull(statsMap);
         assertEquals(1, statsMap.size());
 
@@ -1053,4 +1065,102 @@
 
         runUdp4Test();
     }
+
+    private ClatEgress4Value getClatEgress4Value() throws Exception {
+        // Command: dumpsys connectivity clatEgress4RawBpfMap
+        final String[] args = new String[] {DUMPSYS_CLAT_RAWMAP_EGRESS4_ARG};
+        final HashMap<ClatEgress4Key, ClatEgress4Value> egress4Map = pollRawMapFromDump(
+                ClatEgress4Key.class, ClatEgress4Value.class, Context.CONNECTIVITY_SERVICE, args);
+        assertNotNull(egress4Map);
+        assertEquals(1, egress4Map.size());
+        return egress4Map.entrySet().iterator().next().getValue();
+    }
+
+    private ClatIngress6Value getClatIngress6Value() throws Exception {
+        // Command: dumpsys connectivity clatIngress6RawBpfMap
+        final String[] args = new String[] {DUMPSYS_CLAT_RAWMAP_INGRESS6_ARG};
+        final HashMap<ClatIngress6Key, ClatIngress6Value> ingress6Map = pollRawMapFromDump(
+                ClatIngress6Key.class, ClatIngress6Value.class, Context.CONNECTIVITY_SERVICE, args);
+        assertNotNull(ingress6Map);
+        assertEquals(1, ingress6Map.size());
+        return ingress6Map.entrySet().iterator().next().getValue();
+    }
+
+    /**
+     * Test network topology:
+     *
+     *            public network (rawip)                 private network
+     *                      |         UE (CLAT support)         |
+     * +---------------+    V    +------------+------------+    V    +------------+
+     * | NAT64 Gateway +---------+  Upstream  | Downstream +---------+   Client   |
+     * +---------------+         +------------+------------+         +------------+
+     * remote ip                 public ip                           private ip
+     * [64:ff9b::808:808]:443    [clat ipv6]:9876                    [TetheredDevice ipv4]:9876
+     *
+     * Note that CLAT IPv6 address is generated by ClatCoordinator. Get the CLAT IPv6 address by
+     * sending out an IPv4 packet and extracting the source address from CLAT translated IPv6
+     * packet.
+     */
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testTetherClatBpfOffloadUdp() throws Exception {
+        assumeKernelSupportBpfOffloadUdpV4();
+
+        // CLAT only starts on IPv6 only network.
+        final TetheringTester tester = initTetheringTester(toList(TEST_IP6_ADDR),
+                toList(TEST_IP6_DNS));
+        final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, true /* hasIpv6 */);
+
+        // Get CLAT IPv6 address.
+        final Inet6Address clatIp6 = getClatIpv6Address(tester, tethered);
+
+        // Get current values before sending packets.
+        final ClatEgress4Value oldEgress4 = getClatEgress4Value();
+        final ClatIngress6Value oldIngress6 = getClatIngress6Value();
+
+        // Send an IPv4 UDP packet in original direction.
+        // IPv4 packet -- CLAT translation --> IPv6 packet
+        for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
+            sendUploadPacketUdp(tethered.macAddr, tethered.routerMacAddr, tethered.ipv4Addr,
+                    REMOTE_IP4_ADDR, tester, true /* is4To6 */);
+        }
+
+        // Send an IPv6 UDP packet in reply direction.
+        // IPv6 packet -- CLAT translation --> IPv4 packet
+        for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
+            sendDownloadPacketUdp(REMOTE_NAT64_ADDR, clatIp6, tester, true /* is6To4 */);
+        }
+
+        // Send fragmented IPv6 UDP packets in the reply direction.
+        // IPv6 frament packet -- CLAT translation --> IPv4 fragment packet
+        final int payloadLen = 1500;
+        final int l2mtu = 1000;
+        final int fragPktCnt = 2; // 1500 bytes of UDP payload were fragmented into two packets.
+        final long fragRxBytes = payloadLen + UDP_HEADER_LEN + fragPktCnt * IPV4_HEADER_MIN_LEN;
+        final byte[] payload = new byte[payloadLen];
+        // Initialize the payload with random bytes.
+        Random random = new Random();
+        random.nextBytes(payload);
+        sendDownloadFragmentedUdpPackets(REMOTE_NAT64_ADDR, clatIp6, tester,
+                ByteBuffer.wrap(payload), l2mtu);
+
+        // After sending test packets, get stats again to verify their differences.
+        final ClatEgress4Value newEgress4 = getClatEgress4Value();
+        final ClatIngress6Value newIngress6 = getClatIngress6Value();
+
+        assertEquals(RX_UDP_PACKET_COUNT + fragPktCnt, newIngress6.packets - oldIngress6.packets);
+        assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE + fragRxBytes,
+                newIngress6.bytes - oldIngress6.bytes);
+        assertEquals(TX_UDP_PACKET_COUNT, newEgress4.packets - oldEgress4.packets);
+        // The increase in egress traffic equals the expected size of the translated UDP packets.
+        // Calculation:
+        // - Original UDP packet was TX_UDP_PACKET_SIZE bytes (IPv4 header + UDP header + payload).
+        // - After CLAT translation, each packet is now:
+        //     IPv6 header + unchanged UDP header + unchanged payload
+        // Therefore, the total size of the translated UDP packet should be:
+        //     TX_UDP_PACKET_SIZE + IPV6_HEADER_LEN - IPV4_HEADER_MIN_LEN
+        assertEquals(
+                TX_UDP_PACKET_COUNT * (TX_UDP_PACKET_SIZE + IPV6_HEADER_LEN - IPV4_HEADER_MIN_LEN),
+                newEgress4.bytes - oldEgress4.bytes);
+    }
 }
diff --git a/staticlibs/native/bpf_headers/Android.bp b/bpf/headers/Android.bp
similarity index 100%
rename from staticlibs/native/bpf_headers/Android.bp
rename to bpf/headers/Android.bp
diff --git a/staticlibs/native/bpf_headers/BpfMapTest.cpp b/bpf/headers/BpfMapTest.cpp
similarity index 100%
rename from staticlibs/native/bpf_headers/BpfMapTest.cpp
rename to bpf/headers/BpfMapTest.cpp
diff --git a/staticlibs/native/bpf_headers/BpfRingbufTest.cpp b/bpf/headers/BpfRingbufTest.cpp
similarity index 100%
rename from staticlibs/native/bpf_headers/BpfRingbufTest.cpp
rename to bpf/headers/BpfRingbufTest.cpp
diff --git a/staticlibs/native/bpf_headers/TEST_MAPPING b/bpf/headers/TEST_MAPPING
similarity index 100%
rename from staticlibs/native/bpf_headers/TEST_MAPPING
rename to bpf/headers/TEST_MAPPING
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h b/bpf/headers/include/bpf/BpfClassic.h
similarity index 100%
rename from staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
rename to bpf/headers/include/bpf/BpfClassic.h
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h b/bpf/headers/include/bpf/BpfMap.h
similarity index 100%
rename from staticlibs/native/bpf_headers/include/bpf/BpfMap.h
rename to bpf/headers/include/bpf/BpfMap.h
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h b/bpf/headers/include/bpf/BpfRingbuf.h
similarity index 100%
rename from staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
rename to bpf/headers/include/bpf/BpfRingbuf.h
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h b/bpf/headers/include/bpf/BpfUtils.h
similarity index 100%
rename from staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
rename to bpf/headers/include/bpf/BpfUtils.h
diff --git a/staticlibs/native/bpf_headers/include/bpf/KernelUtils.h b/bpf/headers/include/bpf/KernelUtils.h
similarity index 100%
rename from staticlibs/native/bpf_headers/include/bpf/KernelUtils.h
rename to bpf/headers/include/bpf/KernelUtils.h
diff --git a/staticlibs/native/bpf_headers/include/bpf/WaitForProgsLoaded.h b/bpf/headers/include/bpf/WaitForProgsLoaded.h
similarity index 100%
rename from staticlibs/native/bpf_headers/include/bpf/WaitForProgsLoaded.h
rename to bpf/headers/include/bpf/WaitForProgsLoaded.h
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/bpf/headers/include/bpf_helpers.h
similarity index 95%
rename from staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
rename to bpf/headers/include/bpf_helpers.h
index 4ddec8b..ca0ca76 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/bpf/headers/include/bpf_helpers.h
@@ -7,7 +7,7 @@
 #include "bpf_map_def.h"
 
 /******************************************************************************
- * WARNING: CHANGES TO THIS FILE OUTSIDE OF AOSP/MASTER ARE LIKELY TO BREAK   *
+ * WARNING: CHANGES TO THIS FILE OUTSIDE OF AOSP/MAIN ARE LIKELY TO BREAK     *
  * DEVICE COMPATIBILITY WITH MAINLINE MODULES SHIPPING EBPF CODE.             *
  *                                                                            *
  * THIS WILL LIKELY RESULT IN BRICKED DEVICES AT SOME ARBITRARY FUTURE TIME   *
@@ -71,11 +71,11 @@
  * In which case it's just best to use the default.
  */
 #ifndef BPFLOADER_MIN_VER
-#define BPFLOADER_MIN_VER BPFLOADER_PLATFORM_VERSION
+#define BPFLOADER_MIN_VER BPFLOADER_PLATFORM_VERSION  // inclusive, ie. >=
 #endif
 
 #ifndef BPFLOADER_MAX_VER
-#define BPFLOADER_MAX_VER DEFAULT_BPFLOADER_MAX_VER
+#define BPFLOADER_MAX_VER 0x10000u  // exclusive, ie. < v1.0
 #endif
 
 /* place things in different elf sections */
@@ -99,23 +99,19 @@
  *
  * If missing, bpfloader_{min/max}_ver default to 0/0x10000 ie. [v0.0, v1.0),
  * while size_of_bpf_{map/prog}_def default to 32/20 which are the v0.0 sizes.
- */
-#define LICENSE(NAME)                                                                           \
-    unsigned int _bpfloader_min_ver SECTION("bpfloader_min_ver") = BPFLOADER_MIN_VER;           \
-    unsigned int _bpfloader_max_ver SECTION("bpfloader_max_ver") = BPFLOADER_MAX_VER;           \
-    size_t _size_of_bpf_map_def SECTION("size_of_bpf_map_def") = sizeof(struct bpf_map_def);    \
-    size_t _size_of_bpf_prog_def SECTION("size_of_bpf_prog_def") = sizeof(struct bpf_prog_def); \
-    char _license[] SECTION("license") = (NAME)
-
-/* This macro disables loading BTF map debug information on Android <=U *and* all user builds.
  *
- * Note: Bpfloader v0.39+ honours 'btf_user_min_bpfloader_ver' on user builds,
- * and 'btf_min_bpfloader_ver' on non-user builds.
- * Older BTF capable versions unconditionally honour 'btf_min_bpfloader_ver'
+ * This macro also disables loading BTF map debug information, as versions
+ * of the platform bpfloader that support BTF require fork-exec of btfloader
+ * which causes a regression in boot time.
  */
-#define DISABLE_BTF_ON_USER_BUILDS() \
-    unsigned _btf_min_bpfloader_ver SECTION("btf_min_bpfloader_ver") = 39u; \
-    unsigned _btf_user_min_bpfloader_ver SECTION("btf_user_min_bpfloader_ver") = 0xFFFFFFFFu
+#define LICENSE(NAME)                                                                              \
+    unsigned int _bpfloader_min_ver SECTION("bpfloader_min_ver") = BPFLOADER_MIN_VER;              \
+    unsigned int _bpfloader_max_ver SECTION("bpfloader_max_ver") = BPFLOADER_MAX_VER;              \
+    size_t _size_of_bpf_map_def SECTION("size_of_bpf_map_def") = sizeof(struct bpf_map_def);       \
+    size_t _size_of_bpf_prog_def SECTION("size_of_bpf_prog_def") = sizeof(struct bpf_prog_def);    \
+    unsigned _btf_min_bpfloader_ver SECTION("btf_min_bpfloader_ver") = BPFLOADER_MAINLINE_VERSION; \
+    unsigned _btf_user_min_bpfloader_ver SECTION("btf_user_min_bpfloader_ver") = 0xFFFFFFFFu;      \
+    char _license[] SECTION("license") = (NAME)
 
 /* flag the resulting bpf .o file as critical to system functionality,
  * loading all kernel version appropriate programs in it must succeed
@@ -387,13 +383,17 @@
 static int (*bpf_probe_read_user_str)(void* dst, int size, const void* unsafe_ptr) = (void*) BPF_FUNC_probe_read_user_str;
 static unsigned long long (*bpf_ktime_get_ns)(void) = (void*) BPF_FUNC_ktime_get_ns;
 static unsigned long long (*bpf_ktime_get_boot_ns)(void) = (void*)BPF_FUNC_ktime_get_boot_ns;
-static int (*bpf_trace_printk)(const char* fmt, int fmt_size, ...) = (void*) BPF_FUNC_trace_printk;
 static unsigned long long (*bpf_get_current_pid_tgid)(void) = (void*) BPF_FUNC_get_current_pid_tgid;
 static unsigned long long (*bpf_get_current_uid_gid)(void) = (void*) BPF_FUNC_get_current_uid_gid;
 static unsigned long long (*bpf_get_smp_processor_id)(void) = (void*) BPF_FUNC_get_smp_processor_id;
 static long (*bpf_get_stackid)(void* ctx, void* map, uint64_t flags) = (void*) BPF_FUNC_get_stackid;
 static long (*bpf_get_current_comm)(void* buf, uint32_t buf_size) = (void*) BPF_FUNC_get_current_comm;
 
+static int (*bpf_trace_printk)(const char* fmt, int fmt_size, ...) = (void*) BPF_FUNC_trace_printk;
+#define bpf_printf(s, n...) bpf_trace_printk(s, sizeof(s), ## n)
+// Note: bpf only supports up to 3 arguments, log via: bpf_printf("msg %d %d %d", 1, 2, 3);
+// and read via the blocking: sudo cat /sys/kernel/debug/tracing/trace_pipe
+
 #define DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv,  \
                             min_loader, max_loader, opt, selinux, pindir, ignore_eng,    \
                             ignore_user, ignore_userdebug)                               \
@@ -414,19 +414,10 @@
     SECTION(SECTION_NAME)                                                                \
     int the_prog
 
-#ifndef DEFAULT_BPF_PROG_SELINUX_CONTEXT
-#define DEFAULT_BPF_PROG_SELINUX_CONTEXT ""
-#endif
-
-#ifndef DEFAULT_BPF_PROG_PIN_SUBDIR
-#define DEFAULT_BPF_PROG_PIN_SUBDIR ""
-#endif
-
 #define DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
                                        opt)                                                        \
     DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv,                \
-                        BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, opt,                                 \
-                        DEFAULT_BPF_PROG_SELINUX_CONTEXT, DEFAULT_BPF_PROG_PIN_SUBDIR,             \
+                        BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, opt, "", "",                         \
                         LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
 // Programs (here used in the sense of functions/sections) marked optional are allowed to fail
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/bpf/headers/include/bpf_map_def.h
similarity index 95%
rename from staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
rename to bpf/headers/include/bpf_map_def.h
index 00ef91a..2d6736c 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/bpf/headers/include/bpf_map_def.h
@@ -29,7 +29,7 @@
  *                                                                            *
  *                          ! ! ! W A R N I N G ! ! !                         *
  *                                                                            *
- * CHANGES TO THESE STRUCTURE DEFINITIONS OUTSIDE OF AOSP/MASTER *WILL* BREAK *
+ * CHANGES TO THESE STRUCTURE DEFINITIONS OUTSIDE OF AOSP/MAIN *WILL* BREAK   *
  * MAINLINE MODULE COMPATIBILITY                                              *
  *                                                                            *
  * AND THUS MAY RESULT IN YOUR DEVICE BRICKING AT SOME ARBITRARY POINT IN     *
@@ -42,12 +42,6 @@
  *                                                                            *
  ******************************************************************************/
 
-// These are the values used if these fields are missing
-#define DEFAULT_BPFLOADER_MIN_VER 0u        // v0.0 (this is inclusive ie. >= v0.0)
-#define DEFAULT_BPFLOADER_MAX_VER 0x10000u  // v1.0 (this is exclusive ie. < v1.0)
-#define DEFAULT_SIZEOF_BPF_MAP_DEF 32       // v0.0 struct: enum (uint sized) + 7 uint
-#define DEFAULT_SIZEOF_BPF_PROG_DEF 20      // v0.0 struct: 4 uint + bool + 3 byte alignment pad
-
 /*
  * The bpf_{map,prog}_def structures are compiled for different architectures.
  * Once by the BPF compiler for the BPF architecture, and once by a C++
diff --git a/netbpfload/Android.bp b/bpf/loader/Android.bp
similarity index 100%
rename from netbpfload/Android.bp
rename to bpf/loader/Android.bp
diff --git a/netbpfload/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
similarity index 88%
rename from netbpfload/NetBpfLoad.cpp
rename to bpf/loader/NetBpfLoad.cpp
index afb44cc..c058433 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -17,7 +17,6 @@
 #define LOG_TAG "NetBpfLoad"
 
 #include <arpa/inet.h>
-#include <cstdlib>
 #include <dirent.h>
 #include <elf.h>
 #include <errno.h>
@@ -26,8 +25,6 @@
 #include <fstream>
 #include <inttypes.h>
 #include <iostream>
-#include <linux/bpf.h>
-#include <linux/elf.h>
 #include <linux/unistd.h>
 #include <log/log.h>
 #include <net/if.h>
@@ -60,7 +57,7 @@
 
 #include "BpfSyscallWrappers.h"
 #include "bpf/BpfUtils.h"
-#include "bpf/bpf_map_def.h"
+#include "bpf_map_def.h"
 
 using android::base::EndsWith;
 using android::base::StartsWith;
@@ -250,7 +247,7 @@
     vector<char> rel_data;
     optional<struct bpf_prog_def> prog_def;
 
-    unique_fd prog_fd; /* fd after loading */
+    unique_fd prog_fd; // fd after loading
 } codeSection;
 
 static int readElfHeader(ifstream& elfFile, Elf64_Ehdr* eh) {
@@ -262,7 +259,7 @@
     return 0;
 }
 
-/* Reads all section header tables into an Shdr array */
+// Reads all section header tables into an Shdr array
 static int readSectionHeadersAll(ifstream& elfFile, vector<Elf64_Shdr>& shTable) {
     Elf64_Ehdr eh;
     int ret = 0;
@@ -273,7 +270,7 @@
     elfFile.seekg(eh.e_shoff);
     if (elfFile.fail()) return -1;
 
-    /* Read shdr table entries */
+    // Read shdr table entries
     shTable.resize(eh.e_shnum);
 
     if (!elfFile.read((char*)shTable.data(), (eh.e_shnum * eh.e_shentsize))) return -ENOMEM;
@@ -281,7 +278,7 @@
     return 0;
 }
 
-/* Read a section by its index - for ex to get sec hdr strtab blob */
+// Read a section by its index - for ex to get sec hdr strtab blob
 static int readSectionByIdx(ifstream& elfFile, int id, vector<char>& sec) {
     vector<Elf64_Shdr> shTable;
     int ret = readSectionHeadersAll(elfFile, shTable);
@@ -296,7 +293,7 @@
     return 0;
 }
 
-/* Read whole section header string table */
+// Read whole section header string table
 static int readSectionHeaderStrtab(ifstream& elfFile, vector<char>& strtab) {
     Elf64_Ehdr eh;
     int ret = readElfHeader(elfFile, &eh);
@@ -308,7 +305,7 @@
     return 0;
 }
 
-/* Get name from offset in strtab */
+// Get name from offset in strtab
 static int getSymName(ifstream& elfFile, int nameOff, string& name) {
     int ret;
     vector<char> secStrTab;
@@ -322,7 +319,7 @@
     return 0;
 }
 
-/* Reads a full section by name - example to get the GPL license */
+// Reads a full section by name - example to get the GPL license
 static int readSectionByName(const char* name, ifstream& elfFile, vector<char>& data) {
     vector<char> secStrTab;
     vector<Elf64_Shdr> shTable;
@@ -354,15 +351,15 @@
     return -2;
 }
 
-unsigned int readSectionUint(const char* name, ifstream& elfFile, unsigned int defVal) {
+unsigned int readSectionUint(const char* name, ifstream& elfFile) {
     vector<char> theBytes;
     int ret = readSectionByName(name, elfFile, theBytes);
     if (ret) {
-        ALOGD("Couldn't find section %s (defaulting to %u [0x%x]).", name, defVal, defVal);
-        return defVal;
+        ALOGE("Couldn't find section %s.", name);
+        abort();
     } else if (theBytes.size() < sizeof(unsigned int)) {
-        ALOGE("Section %s too short (defaulting to %u [0x%x]).", name, defVal, defVal);
-        return defVal;
+        ALOGE("Section %s is too short.", name);
+        abort();
     } else {
         // decode first 4 bytes as LE32 uint, there will likely be more bytes due to alignment.
         unsigned int value = static_cast<unsigned char>(theBytes[3]);
@@ -372,7 +369,7 @@
         value += static_cast<unsigned char>(theBytes[1]);
         value <<= 8;
         value += static_cast<unsigned char>(theBytes[0]);
-        ALOGI("Section %s value is %u [0x%x]", name, value, value);
+        ALOGD("Section %s value is %u [0x%x]", name, value, value);
         return value;
     }
 }
@@ -428,43 +425,24 @@
     return BPF_PROG_TYPE_UNSPEC;
 }
 
-/*
-static string getSectionName(enum bpf_prog_type type)
-{
-    for (auto& snt : sectionNameTypes)
-        if (snt.type == type)
-            return string(snt.name);
-
-    return "UNKNOWN SECTION NAME " + std::to_string(type);
-}
-*/
-
-static int readProgDefs(ifstream& elfFile, vector<struct bpf_prog_def>& pd,
-                        size_t sizeOfBpfProgDef) {
+static int readProgDefs(ifstream& elfFile, vector<struct bpf_prog_def>& pd) {
     vector<char> pdData;
     int ret = readSectionByName("progs", elfFile, pdData);
     if (ret) return ret;
 
-    if (pdData.size() % sizeOfBpfProgDef) {
+    if (pdData.size() % sizeof(struct bpf_prog_def)) {
         ALOGE("readProgDefs failed due to improper sized progs section, %zu %% %zu != 0",
-              pdData.size(), sizeOfBpfProgDef);
+              pdData.size(), sizeof(struct bpf_prog_def));
         return -1;
     };
 
-    int progCount = pdData.size() / sizeOfBpfProgDef;
-    pd.resize(progCount);
-    size_t trimmedSize = std::min(sizeOfBpfProgDef, sizeof(struct bpf_prog_def));
+    pd.resize(pdData.size() / sizeof(struct bpf_prog_def));
 
     const char* dataPtr = pdData.data();
     for (auto& p : pd) {
-        // First we zero initialize
-        memset(&p, 0, sizeof(p));
-        // Then we set non-zero defaults
-        p.bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER;  // v1.0
-        // Then we copy over the structure prefix from the ELF file.
-        memcpy(&p, dataPtr, trimmedSize);
-        // Move to next struct in the ELF file
-        dataPtr += sizeOfBpfProgDef;
+        // Copy the structure from the ELF file and move to the next one.
+        memcpy(&p, dataPtr, sizeof(struct bpf_prog_def));
+        dataPtr += sizeof(struct bpf_prog_def);
     }
     return 0;
 }
@@ -479,7 +457,7 @@
     ret = readSymTab(elfFile, 1 /* sort */, symtab);
     if (ret) return ret;
 
-    /* Get index of section */
+    // Get index of section
     ret = readSectionHeadersAll(elfFile, shTable);
     if (ret) return ret;
 
@@ -494,7 +472,7 @@
         }
     }
 
-    /* No section found with matching name*/
+    // No section found with matching name
     if (sec_idx == -1) {
         ALOGW("No %s section could be found in elf object", sectionName.c_str());
         return -1;
@@ -514,8 +492,8 @@
     return 0;
 }
 
-/* Read a section by its index - for ex to get sec hdr strtab blob */
-static int readCodeSections(ifstream& elfFile, vector<codeSection>& cs, size_t sizeOfBpfProgDef) {
+// Read a section by its index - for ex to get sec hdr strtab blob
+static int readCodeSections(ifstream& elfFile, vector<codeSection>& cs) {
     vector<Elf64_Shdr> shTable;
     int entries, ret = 0;
 
@@ -524,7 +502,7 @@
     entries = shTable.size();
 
     vector<struct bpf_prog_def> pd;
-    ret = readProgDefs(elfFile, pd, sizeOfBpfProgDef);
+    ret = readProgDefs(elfFile, pd);
     if (ret) return ret;
     vector<string> progDefNames;
     ret = getSectionSymNames(elfFile, "progs", progDefNames);
@@ -568,7 +546,7 @@
             }
         }
 
-        /* Check for rel section */
+        // Check for rel section
         if (cs_temp.data.size() > 0 && i < entries) {
             ret = getSymName(elfFile, shTable[i + 1].sh_name, name);
             if (ret) return ret;
@@ -626,6 +604,9 @@
     if (type == BPF_MAP_TYPE_DEVMAP || type == BPF_MAP_TYPE_DEVMAP_HASH)
         desired_map_flags |= BPF_F_RDONLY_PROG;
 
+    if (type == BPF_MAP_TYPE_LPM_TRIE)
+        desired_map_flags |= BPF_F_NO_PREALLOC;
+
     // The .h file enforces that this is a power of two, and page size will
     // also always be a power of two, so this logic is actually enough to
     // force it to be a multiple of the page size, as required by the kernel.
@@ -657,8 +638,7 @@
 }
 
 static int createMaps(const char* elfPath, ifstream& elfFile, vector<unique_fd>& mapFds,
-                      const char* prefix, const size_t sizeOfBpfMapDef,
-                      const unsigned int bpfloader_ver) {
+                      const char* prefix, const unsigned int bpfloader_ver) {
     int ret;
     vector<char> mdData;
     vector<struct bpf_map_def> md;
@@ -669,27 +649,19 @@
     if (ret == -2) return 0;  // no maps to read
     if (ret) return ret;
 
-    if (mdData.size() % sizeOfBpfMapDef) {
+    if (mdData.size() % sizeof(struct bpf_map_def)) {
         ALOGE("createMaps failed due to improper sized maps section, %zu %% %zu != 0",
-              mdData.size(), sizeOfBpfMapDef);
+              mdData.size(), sizeof(struct bpf_map_def));
         return -1;
     };
 
-    int mapCount = mdData.size() / sizeOfBpfMapDef;
-    md.resize(mapCount);
-    size_t trimmedSize = std::min(sizeOfBpfMapDef, sizeof(struct bpf_map_def));
+    md.resize(mdData.size() / sizeof(struct bpf_map_def));
 
     const char* dataPtr = mdData.data();
     for (auto& m : md) {
-        // First we zero initialize
-        memset(&m, 0, sizeof(m));
-        // Then we set non-zero defaults
-        m.bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER;  // v1.0
-        m.max_kver = 0xFFFFFFFFu;                         // matches KVER_INF from bpf_helpers.h
-        // Then we copy over the structure prefix from the ELF file.
-        memcpy(&m, dataPtr, trimmedSize);
-        // Move to next struct in the ELF file
-        dataPtr += sizeOfBpfMapDef;
+        // Copy the structure from the ELF file and move to the next one.
+        memcpy(&m, dataPtr, sizeof(struct bpf_map_def));
+        dataPtr += sizeof(struct bpf_map_def);
     }
 
     ret = getSectionSymNames(elfFile, "maps", mapNames);
@@ -701,28 +673,28 @@
         if (md[i].zero != 0) abort();
 
         if (bpfloader_ver < md[i].bpfloader_min_ver) {
-            ALOGI("skipping map %s which requires bpfloader min ver 0x%05x", mapNames[i].c_str(),
+            ALOGD("skipping map %s which requires bpfloader min ver 0x%05x", mapNames[i].c_str(),
                   md[i].bpfloader_min_ver);
             mapFds.push_back(unique_fd());
             continue;
         }
 
         if (bpfloader_ver >= md[i].bpfloader_max_ver) {
-            ALOGI("skipping map %s which requires bpfloader max ver 0x%05x", mapNames[i].c_str(),
+            ALOGD("skipping map %s which requires bpfloader max ver 0x%05x", mapNames[i].c_str(),
                   md[i].bpfloader_max_ver);
             mapFds.push_back(unique_fd());
             continue;
         }
 
         if (kvers < md[i].min_kver) {
-            ALOGI("skipping map %s which requires kernel version 0x%x >= 0x%x",
+            ALOGD("skipping map %s which requires kernel version 0x%x >= 0x%x",
                   mapNames[i].c_str(), kvers, md[i].min_kver);
             mapFds.push_back(unique_fd());
             continue;
         }
 
         if (kvers >= md[i].max_kver) {
-            ALOGI("skipping map %s which requires kernel version 0x%x < 0x%x",
+            ALOGD("skipping map %s which requires kernel version 0x%x < 0x%x",
                   mapNames[i].c_str(), kvers, md[i].max_kver);
             mapFds.push_back(unique_fd());
             continue;
@@ -730,7 +702,7 @@
 
         if ((md[i].ignore_on_eng && isEng()) || (md[i].ignore_on_user && isUser()) ||
             (md[i].ignore_on_userdebug && isUserdebug())) {
-            ALOGI("skipping map %s which is ignored on %s builds", mapNames[i].c_str(),
+            ALOGD("skipping map %s which is ignored on %s builds", mapNames[i].c_str(),
                   getBuildType().c_str());
             mapFds.push_back(unique_fd());
             continue;
@@ -741,7 +713,7 @@
             (isX86() && isKernel32Bit() && md[i].ignore_on_x86_32) ||
             (isX86() && isKernel64Bit() && md[i].ignore_on_x86_64) ||
             (isRiscV() && md[i].ignore_on_riscv64)) {
-            ALOGI("skipping map %s which is ignored on %s", mapNames[i].c_str(),
+            ALOGD("skipping map %s which is ignored on %s", mapNames[i].c_str(),
                   describeArch());
             mapFds.push_back(unique_fd());
             continue;
@@ -779,14 +751,14 @@
 
         domain selinux_context = getDomainFromSelinuxContext(md[i].selinux_context);
         if (specified(selinux_context)) {
-            ALOGI("map %s selinux_context [%-32s] -> %d -> '%s' (%s)", mapNames[i].c_str(),
+            ALOGV("map %s selinux_context [%-32s] -> %d -> '%s' (%s)", mapNames[i].c_str(),
                   md[i].selinux_context, static_cast<int>(selinux_context),
                   lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
         }
 
         domain pin_subdir = getDomainFromPinSubdir(md[i].pin_subdir);
         if (specified(pin_subdir)) {
-            ALOGI("map %s pin_subdir [%-32s] -> %d -> '%s'", mapNames[i].c_str(), md[i].pin_subdir,
+            ALOGV("map %s pin_subdir [%-32s] -> %d -> '%s'", mapNames[i].c_str(), md[i].pin_subdir,
                   static_cast<int>(pin_subdir), lookupPinSubdir(pin_subdir));
         }
 
@@ -810,13 +782,17 @@
               .key_size = md[i].key_size,
               .value_size = md[i].value_size,
               .max_entries = max_entries,
-              .map_flags = md[i].map_flags,
+              .map_flags = md[i].map_flags | (type == BPF_MAP_TYPE_LPM_TRIE ? BPF_F_NO_PREALLOC : 0),
             };
             if (isAtLeastKernelVersion(4, 15, 0))
                 strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
             fd.reset(bpf(BPF_MAP_CREATE, req));
             saved_errno = errno;
-            ALOGD("bpf_create_map name %s, ret: %d", mapNames[i].c_str(), fd.get());
+            if (fd.ok()) {
+              ALOGD("bpf_create_map[%s] -> %d", mapNames[i].c_str(), fd.get());
+            } else {
+              ALOGE("bpf_create_map[%s] -> %d errno:%d", mapNames[i].c_str(), fd.get(), saved_errno);
+            }
         }
 
         if (!fd.ok()) return -saved_errno;
@@ -870,7 +846,8 @@
 
         int mapId = bpfGetFdMapId(fd);
         if (mapId == -1) {
-            ALOGE("bpfGetFdMapId failed, ret: %d [%d]", mapId, errno);
+            if (isAtLeastKernelVersion(4, 14, 0))
+                ALOGE("bpfGetFdMapId failed, ret: %d [%d]", mapId, errno);
         } else {
             ALOGI("map %s id %d", mapPinLoc.c_str(), mapId);
         }
@@ -881,26 +858,6 @@
     return ret;
 }
 
-/* For debugging, dump all instructions */
-static void dumpIns(char* ins, int size) {
-    for (int row = 0; row < size / 8; row++) {
-        ALOGE("%d: ", row);
-        for (int j = 0; j < 8; j++) {
-            ALOGE("%3x ", ins[(row * 8) + j]);
-        }
-        ALOGE("\n");
-    }
-}
-
-/* For debugging, dump all code sections from cs list */
-static void dumpAllCs(vector<codeSection>& cs) {
-    for (int i = 0; i < (int)cs.size(); i++) {
-        ALOGE("Dumping cs %d, name %s", int(i), cs[i].name.c_str());
-        dumpIns((char*)cs[i].data.data(), cs[i].data.size());
-        ALOGE("-----------");
-    }
-}
-
 static void applyRelo(void* insnsPtr, Elf64_Addr offset, int fd) {
     int insnIndex;
     struct bpf_insn *insn, *insns;
@@ -918,9 +875,7 @@
     }
 
     if (insn->code != (BPF_LD | BPF_IMM | BPF_DW)) {
-        ALOGE("Dumping all instructions till ins %d", insnIndex);
         ALOGE("invalid relo for insn %d: code 0x%x", insnIndex, insn->code);
-        dumpIns((char*)insnsPtr, (insnIndex + 3) * 8);
         return;
     }
 
@@ -945,7 +900,7 @@
             ret = getSymNameByIdx(elfFile, symIndex, symName);
             if (ret) return;
 
-            /* Find the map fd and apply relo */
+            // Find the map fd and apply relo
             for (int j = 0; j < (int)mapNames.size(); j++) {
                 if (!mapNames[j].compare(symName)) {
                     applyRelo(cs[k].data.data(), rel[i].r_offset, mapFds[j]);
@@ -1012,13 +967,13 @@
         }
 
         if (specified(selinux_context)) {
-            ALOGI("prog %s selinux_context [%-32s] -> %d -> '%s' (%s)", name.c_str(),
+            ALOGV("prog %s selinux_context [%-32s] -> %d -> '%s' (%s)", name.c_str(),
                   cs[i].prog_def->selinux_context, static_cast<int>(selinux_context),
                   lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
         }
 
         if (specified(pin_subdir)) {
-            ALOGI("prog %s pin_subdir [%-32s] -> %d -> '%s'", name.c_str(),
+            ALOGV("prog %s pin_subdir [%-32s] -> %d -> '%s'", name.c_str(),
                   cs[i].prog_def->pin_subdir, static_cast<int>(pin_subdir),
                   lookupPinSubdir(pin_subdir));
         }
@@ -1043,13 +998,13 @@
 
             union bpf_attr req = {
               .prog_type = cs[i].type,
-              .kern_version = kvers,
-              .license = ptr_to_u64(license.c_str()),
-              .insns = ptr_to_u64(cs[i].data.data()),
               .insn_cnt = static_cast<__u32>(cs[i].data.size() / sizeof(struct bpf_insn)),
+              .insns = ptr_to_u64(cs[i].data.data()),
+              .license = ptr_to_u64(license.c_str()),
               .log_level = 1,
-              .log_buf = ptr_to_u64(log_buf.data()),
               .log_size = static_cast<__u32>(log_buf.size()),
+              .log_buf = ptr_to_u64(log_buf.data()),
+              .kern_version = kvers,
               .expected_attach_type = cs[i].attach_type,
             };
             if (isAtLeastKernelVersion(4, 15, 0))
@@ -1060,17 +1015,20 @@
                   cs[i].name.c_str(), fd.get(), (!fd.ok() ? std::strerror(errno) : "no error"));
 
             if (!fd.ok()) {
-                vector<string> lines = android::base::Split(log_buf.data(), "\n");
+                if (log_buf.size()) {
+                    vector<string> lines = android::base::Split(log_buf.data(), "\n");
 
-                ALOGW("BPF_PROG_LOAD - BEGIN log_buf contents:");
-                for (const auto& line : lines) ALOGW("%s", line.c_str());
-                ALOGW("BPF_PROG_LOAD - END log_buf contents.");
+                    ALOGW("BPF_PROG_LOAD - BEGIN log_buf contents:");
+                    for (const auto& line : lines) ALOGW("%s", line.c_str());
+                    ALOGW("BPF_PROG_LOAD - END log_buf contents.");
+                }
 
                 if (cs[i].prog_def->optional) {
-                    ALOGW("failed program is marked optional - continuing...");
+                    ALOGW("failed program %s is marked optional - continuing...",
+                          cs[i].name.c_str());
                     continue;
                 }
-                ALOGE("non-optional program failed to load.");
+                ALOGE("non-optional program %s failed to load.", cs[i].name.c_str());
             }
         }
 
@@ -1128,7 +1086,7 @@
 }
 
 int loadProg(const char* const elfPath, bool* const isCritical, const unsigned int bpfloader_ver,
-             const Location& location) {
+             const char* const prefix) {
     vector<char> license;
     vector<char> critical;
     vector<codeSection> cs;
@@ -1154,63 +1112,33 @@
               elfPath, (char*)license.data());
     }
 
-    // the following default values are for bpfloader V0.0 format which does not include them
-    unsigned int bpfLoaderMinVer =
-            readSectionUint("bpfloader_min_ver", elfFile, DEFAULT_BPFLOADER_MIN_VER);
-    unsigned int bpfLoaderMaxVer =
-            readSectionUint("bpfloader_max_ver", elfFile, DEFAULT_BPFLOADER_MAX_VER);
-    unsigned int bpfLoaderMinRequiredVer =
-            readSectionUint("bpfloader_min_required_ver", elfFile, 0);
-    size_t sizeOfBpfMapDef =
-            readSectionUint("size_of_bpf_map_def", elfFile, DEFAULT_SIZEOF_BPF_MAP_DEF);
-    size_t sizeOfBpfProgDef =
-            readSectionUint("size_of_bpf_prog_def", elfFile, DEFAULT_SIZEOF_BPF_PROG_DEF);
+    unsigned int bpfLoaderMinVer = readSectionUint("bpfloader_min_ver", elfFile);
+    unsigned int bpfLoaderMaxVer = readSectionUint("bpfloader_max_ver", elfFile);
 
     // inclusive lower bound check
     if (bpfloader_ver < bpfLoaderMinVer) {
-        ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with min ver 0x%05x",
+        ALOGD("BpfLoader version 0x%05x ignoring ELF object %s with min ver 0x%05x",
               bpfloader_ver, elfPath, bpfLoaderMinVer);
         return 0;
     }
 
     // exclusive upper bound check
     if (bpfloader_ver >= bpfLoaderMaxVer) {
-        ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with max ver 0x%05x",
+        ALOGD("BpfLoader version 0x%05x ignoring ELF object %s with max ver 0x%05x",
               bpfloader_ver, elfPath, bpfLoaderMaxVer);
         return 0;
     }
 
-    if (bpfloader_ver < bpfLoaderMinRequiredVer) {
-        ALOGI("BpfLoader version 0x%05x failing due to ELF object %s with required min ver 0x%05x",
-              bpfloader_ver, elfPath, bpfLoaderMinRequiredVer);
-        return -1;
-    }
-
-    ALOGI("BpfLoader version 0x%05x processing ELF object %s with ver [0x%05x,0x%05x)",
+    ALOGD("BpfLoader version 0x%05x processing ELF object %s with ver [0x%05x,0x%05x)",
           bpfloader_ver, elfPath, bpfLoaderMinVer, bpfLoaderMaxVer);
 
-    if (sizeOfBpfMapDef < DEFAULT_SIZEOF_BPF_MAP_DEF) {
-        ALOGE("sizeof(bpf_map_def) of %zu is too small (< %d)", sizeOfBpfMapDef,
-              DEFAULT_SIZEOF_BPF_MAP_DEF);
-        return -1;
-    }
-
-    if (sizeOfBpfProgDef < DEFAULT_SIZEOF_BPF_PROG_DEF) {
-        ALOGE("sizeof(bpf_prog_def) of %zu is too small (< %d)", sizeOfBpfProgDef,
-              DEFAULT_SIZEOF_BPF_PROG_DEF);
-        return -1;
-    }
-
-    ret = readCodeSections(elfFile, cs, sizeOfBpfProgDef);
+    ret = readCodeSections(elfFile, cs);
     if (ret) {
         ALOGE("Couldn't read all code sections in %s", elfPath);
         return ret;
     }
 
-    /* Just for future debugging */
-    if (0) dumpAllCs(cs);
-
-    ret = createMaps(elfPath, elfFile, mapFds, location.prefix, sizeOfBpfMapDef, bpfloader_ver);
+    ret = createMaps(elfPath, elfFile, mapFds, prefix, bpfloader_ver);
     if (ret) {
         ALOGE("Failed to create maps: (ret=%d) in %s", ret, elfPath);
         return ret;
@@ -1221,7 +1149,7 @@
 
     applyMapRelo(elfFile, mapFds, cs);
 
-    ret = loadCodeSections(elfPath, cs, string(license.data()), location.prefix, bpfloader_ver);
+    ret = loadCodeSections(elfPath, cs, string(license.data()), prefix, bpfloader_ver);
     if (ret) ALOGE("Failed to load programs, loadCodeSections ret=%d", ret);
 
     return ret;
@@ -1235,33 +1163,35 @@
     abort();  // can only hit this if permissions (likely selinux) are screwed up
 }
 
+#define APEXROOT "/apex/com.android.tethering"
+#define BPFROOT APEXROOT "/etc/bpf"
 
 const Location locations[] = {
         // S+ Tethering mainline module (network_stack): tether offload
         {
-                .dir = "/apex/com.android.tethering/etc/bpf/",
+                .dir = BPFROOT "/",
                 .prefix = "tethering/",
         },
         // T+ Tethering mainline module (shared with netd & system server)
         // netutils_wrapper (for iptables xt_bpf) has access to programs
         {
-                .dir = "/apex/com.android.tethering/etc/bpf/netd_shared/",
+                .dir = BPFROOT "/netd_shared/",
                 .prefix = "netd_shared/",
         },
         // T+ Tethering mainline module (shared with netd & system server)
         // netutils_wrapper has no access, netd has read only access
         {
-                .dir = "/apex/com.android.tethering/etc/bpf/netd_readonly/",
+                .dir = BPFROOT "/netd_readonly/",
                 .prefix = "netd_readonly/",
         },
         // T+ Tethering mainline module (shared with system server)
         {
-                .dir = "/apex/com.android.tethering/etc/bpf/net_shared/",
+                .dir = BPFROOT "/net_shared/",
                 .prefix = "net_shared/",
         },
         // T+ Tethering mainline module (not shared, just network_stack)
         {
-                .dir = "/apex/com.android.tethering/etc/bpf/net_private/",
+                .dir = BPFROOT "/net_private/",
                 .prefix = "net_private/",
         },
 };
@@ -1280,7 +1210,7 @@
             progPath += s;
 
             bool critical;
-            int ret = loadProg(progPath.c_str(), &critical, bpfloader_ver, location);
+            int ret = loadProg(progPath.c_str(), &critical, bpfloader_ver, location.prefix);
             if (ret) {
                 if (critical) retVal = ret;
                 ALOGE("Failed to load object: %s, ret: %s", progPath.c_str(), std::strerror(-ret));
diff --git a/netbpfload/initrc-doc/README.txt b/bpf/loader/initrc-doc/README.txt
similarity index 100%
rename from netbpfload/initrc-doc/README.txt
rename to bpf/loader/initrc-doc/README.txt
diff --git a/netbpfload/initrc-doc/bpfloader-sdk30-11-R.rc b/bpf/loader/initrc-doc/bpfloader-sdk30-11-R.rc
similarity index 100%
rename from netbpfload/initrc-doc/bpfloader-sdk30-11-R.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk30-11-R.rc
diff --git a/netbpfload/initrc-doc/bpfloader-sdk31-12-S.rc b/bpf/loader/initrc-doc/bpfloader-sdk31-12-S.rc
similarity index 100%
rename from netbpfload/initrc-doc/bpfloader-sdk31-12-S.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk31-12-S.rc
diff --git a/netbpfload/initrc-doc/bpfloader-sdk33-13-T.rc b/bpf/loader/initrc-doc/bpfloader-sdk33-13-T.rc
similarity index 100%
rename from netbpfload/initrc-doc/bpfloader-sdk33-13-T.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk33-13-T.rc
diff --git a/netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc b/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
similarity index 100%
rename from netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
diff --git a/netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc b/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR3.rc
similarity index 100%
copy from netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
copy to bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR3.rc
diff --git a/netbpfload/initrc-doc/bpfloader-sdk34-14-U.rc b/bpf/loader/initrc-doc/bpfloader-sdk34-14-U.rc
similarity index 100%
rename from netbpfload/initrc-doc/bpfloader-sdk34-14-U.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk34-14-U.rc
diff --git a/bpf/loader/initrc-doc/bpfloader-sdk35-15-V.rc b/bpf/loader/initrc-doc/bpfloader-sdk35-15-V.rc
new file mode 100644
index 0000000..066cfc8
--- /dev/null
+++ b/bpf/loader/initrc-doc/bpfloader-sdk35-15-V.rc
@@ -0,0 +1,8 @@
+on load_bpf_programs
+    exec_start bpfloader
+
+service bpfloader /system/bin/false
+    user root
+    oneshot
+    reboot_on_failure reboot,netbpfload-missing
+    updatable
diff --git a/netbpfload/netbpfload.33rc b/bpf/loader/netbpfload.33rc
similarity index 98%
rename from netbpfload/netbpfload.33rc
rename to bpf/loader/netbpfload.33rc
index 493731f..eb937dd 100644
--- a/netbpfload/netbpfload.33rc
+++ b/bpf/loader/netbpfload.33rc
@@ -18,4 +18,3 @@
     rlimit memlock 1073741824 1073741824
     oneshot
     reboot_on_failure reboot,netbpfload-failed
-    override
diff --git a/netbpfload/netbpfload.35rc b/bpf/loader/netbpfload.35rc
similarity index 100%
rename from netbpfload/netbpfload.35rc
rename to bpf/loader/netbpfload.35rc
diff --git a/netbpfload/netbpfload.rc b/bpf/loader/netbpfload.rc
similarity index 100%
rename from netbpfload/netbpfload.rc
rename to bpf/loader/netbpfload.rc
diff --git a/netd/Android.bp b/bpf/netd/Android.bp
similarity index 100%
rename from netd/Android.bp
rename to bpf/netd/Android.bp
diff --git a/netd/BpfBaseTest.cpp b/bpf/netd/BpfBaseTest.cpp
similarity index 100%
rename from netd/BpfBaseTest.cpp
rename to bpf/netd/BpfBaseTest.cpp
diff --git a/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
similarity index 100%
rename from netd/BpfHandler.cpp
rename to bpf/netd/BpfHandler.cpp
diff --git a/netd/BpfHandler.h b/bpf/netd/BpfHandler.h
similarity index 100%
rename from netd/BpfHandler.h
rename to bpf/netd/BpfHandler.h
diff --git a/netd/BpfHandlerTest.cpp b/bpf/netd/BpfHandlerTest.cpp
similarity index 100%
rename from netd/BpfHandlerTest.cpp
rename to bpf/netd/BpfHandlerTest.cpp
diff --git a/netd/NetdUpdatable.cpp b/bpf/netd/NetdUpdatable.cpp
similarity index 100%
rename from netd/NetdUpdatable.cpp
rename to bpf/netd/NetdUpdatable.cpp
diff --git a/netd/include/NetdUpdatablePublic.h b/bpf/netd/include/NetdUpdatablePublic.h
similarity index 100%
rename from netd/include/NetdUpdatablePublic.h
rename to bpf/netd/include/NetdUpdatablePublic.h
diff --git a/netd/libnetd_updatable.map.txt b/bpf/netd/libnetd_updatable.map.txt
similarity index 100%
rename from netd/libnetd_updatable.map.txt
rename to bpf/netd/libnetd_updatable.map.txt
diff --git a/bpf_progs/Android.bp b/bpf/progs/Android.bp
similarity index 98%
rename from bpf_progs/Android.bp
rename to bpf/progs/Android.bp
index f6717c5..dc1f56d 100644
--- a/bpf_progs/Android.bp
+++ b/bpf/progs/Android.bp
@@ -47,8 +47,8 @@
         "com.android.tethering",
     ],
     visibility: [
+        "//packages/modules/Connectivity/bpf/netd",
         "//packages/modules/Connectivity/DnsResolver",
-        "//packages/modules/Connectivity/netd",
         "//packages/modules/Connectivity/service",
         "//packages/modules/Connectivity/service/native/libs/libclat",
         "//packages/modules/Connectivity/Tethering",
diff --git a/bpf_progs/block.c b/bpf/progs/block.c
similarity index 84%
rename from bpf_progs/block.c
rename to bpf/progs/block.c
index 152dda6..0e2dba9 100644
--- a/bpf_progs/block.c
+++ b/bpf/progs/block.c
@@ -14,24 +14,16 @@
  * limitations under the License.
  */
 
-#include <linux/types.h>
-#include <linux/bpf.h>
-#include <netinet/in.h>
-#include <stdint.h>
-
 // The resulting .o needs to load on Android T+
 #define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
 
-#include "bpf_helpers.h"
-
-static const int ALLOW = 1;
-static const int DISALLOW = 0;
+#include "bpf_net_helpers.h"
 
 DEFINE_BPF_MAP_GRW(blocked_ports_map, ARRAY, int, uint64_t,
         1024 /* 64K ports -> 1024 u64s */, AID_SYSTEM)
 
 static inline __always_inline int block_port(struct bpf_sock_addr *ctx) {
-    if (!ctx->user_port) return ALLOW;
+    if (!ctx->user_port) return BPF_ALLOW;
 
     switch (ctx->protocol) {
         case IPPROTO_TCP:
@@ -42,7 +34,7 @@
         case IPPROTO_SCTP:
             break;
         default:
-            return ALLOW; // unknown protocols are allowed
+            return BPF_ALLOW; // unknown protocols are allowed
     }
 
     int key = ctx->user_port >> 6;
@@ -51,10 +43,10 @@
     uint64_t *val = bpf_blocked_ports_map_lookup_elem(&key);
     // Lookup should never fail in reality, but if it does return here to keep the
     // BPF verifier happy.
-    if (!val) return ALLOW;
+    if (!val) return BPF_ALLOW;
 
-    if ((*val >> shift) & 1) return DISALLOW;
-    return ALLOW;
+    if ((*val >> shift) & 1) return BPF_DISALLOW;
+    return BPF_ALLOW;
 }
 
 // the program need to be accessible/loadable by netd (from netd updatable plugin)
@@ -75,4 +67,3 @@
 
 LICENSE("Apache 2.0");
 CRITICAL("ConnectivityNative");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/bpf_net_helpers.h b/bpf/progs/bpf_net_helpers.h
similarity index 68%
rename from bpf_progs/bpf_net_helpers.h
rename to bpf/progs/bpf_net_helpers.h
index ba2f26b..a86c3e6 100644
--- a/bpf_progs/bpf_net_helpers.h
+++ b/bpf/progs/bpf_net_helpers.h
@@ -17,21 +17,72 @@
 #pragma once
 
 #include <linux/bpf.h>
+#include <linux/if.h>
+#include <linux/if_ether.h>
 #include <linux/if_packet.h>
-#include <stdbool.h>
-#include <stdint.h>
-
+#include <linux/in.h>
+#include <linux/in6.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/pkt_cls.h>
+#include <linux/tcp.h>
 // bionic kernel uapi linux/udp.h header is munged...
 #define __kernel_udphdr udphdr
 #include <linux/udp.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "bpf_helpers.h"
+
+// IP flags. (from kernel's include/net/ip.h)
+#define IP_CE      0x8000  // Flag: "Congestion" (really reserved 'evil bit')
+#define IP_DF      0x4000  // Flag: "Don't Fragment"
+#define IP_MF      0x2000  // Flag: "More Fragments"
+#define IP_OFFSET  0x1FFF  // "Fragment Offset" part
+
+// IPv6 fragmentation header. (from kernel's include/net/ipv6.h)
+struct frag_hdr {
+    __u8   nexthdr;
+    __u8   reserved;        // always zero
+    __be16 frag_off;        // 13 bit offset, 2 bits zero, 1 bit "More Fragments"
+    __be32 identification;
+};
+
+// ----- Helper functions for offsets to fields -----
+
+// They all assume simple IP packets:
+//   - no VLAN ethernet tags
+//   - no IPv4 options (see IPV4_HLEN/TCP4_OFFSET/UDP4_OFFSET)
+//   - no IPv6 extension headers
+//   - no TCP options (see TCP_HLEN)
+
+//#define ETH_HLEN sizeof(struct ethhdr)
+#define IP4_HLEN sizeof(struct iphdr)
+#define IP6_HLEN sizeof(struct ipv6hdr)
+#define TCP_HLEN sizeof(struct tcphdr)
+#define UDP_HLEN sizeof(struct udphdr)
 
 // Offsets from beginning of L4 (TCP/UDP) header
 #define TCP_OFFSET(field) offsetof(struct tcphdr, field)
 #define UDP_OFFSET(field) offsetof(struct udphdr, field)
 
-// Offsets from beginning of L3 (IPv4/IPv6) header
+// Offsets from beginning of L3 (IPv4) header
 #define IP4_OFFSET(field) offsetof(struct iphdr, field)
+#define IP4_TCP_OFFSET(field) (IP4_HLEN + TCP_OFFSET(field))
+#define IP4_UDP_OFFSET(field) (IP4_HLEN + UDP_OFFSET(field))
+
+// Offsets from beginning of L3 (IPv6) header
 #define IP6_OFFSET(field) offsetof(struct ipv6hdr, field)
+#define IP6_TCP_OFFSET(field) (IP6_HLEN + TCP_OFFSET(field))
+#define IP6_UDP_OFFSET(field) (IP6_HLEN + UDP_OFFSET(field))
+
+// Offsets from beginning of L2 (ie. Ethernet) header (which must be present)
+#define ETH_IP4_OFFSET(field) (ETH_HLEN + IP4_OFFSET(field))
+#define ETH_IP4_TCP_OFFSET(field) (ETH_HLEN + IP4_TCP_OFFSET(field))
+#define ETH_IP4_UDP_OFFSET(field) (ETH_HLEN + IP4_UDP_OFFSET(field))
+#define ETH_IP6_OFFSET(field) (ETH_HLEN + IP6_OFFSET(field))
+#define ETH_IP6_TCP_OFFSET(field) (ETH_HLEN + IP6_TCP_OFFSET(field))
+#define ETH_IP6_UDP_OFFSET(field) (ETH_HLEN + IP6_UDP_OFFSET(field))
 
 // this returns 0 iff skb->sk is NULL
 static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_cookie;
@@ -103,3 +154,10 @@
 struct updatetime_bool { bool updatetime; };
 #define NO_UPDATETIME ((struct updatetime_bool){ .updatetime = false })
 #define UPDATETIME ((struct updatetime_bool){ .updatetime = true })
+
+// Return value for xt_bpf (netfilter match extension) programs
+static const int XTBPF_NOMATCH = 0;
+static const int XTBPF_MATCH = 1;
+
+static const int BPF_DISALLOW = 0;
+static const int BPF_ALLOW = 1;
diff --git a/bpf_progs/clat_mark.h b/bpf/progs/clat_mark.h
similarity index 100%
rename from bpf_progs/clat_mark.h
rename to bpf/progs/clat_mark.h
diff --git a/bpf_progs/clatd.c b/bpf/progs/clatd.c
similarity index 95%
rename from bpf_progs/clatd.c
rename to bpf/progs/clatd.c
index da6ccbf..2d4551e 100644
--- a/bpf_progs/clatd.c
+++ b/bpf/progs/clatd.c
@@ -14,44 +14,13 @@
  * limitations under the License.
  */
 
-#include <linux/bpf.h>
-#include <linux/if.h>
-#include <linux/if_ether.h>
-#include <linux/in.h>
-#include <linux/in6.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/pkt_cls.h>
-#include <linux/swab.h>
-#include <stdbool.h>
-#include <stdint.h>
-
-// bionic kernel uapi linux/udp.h header is munged...
-#define __kernel_udphdr udphdr
-#include <linux/udp.h>
-
 // The resulting .o needs to load on Android T+
 #define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
 
-#include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
 #include "clatd.h"
 #include "clat_mark.h"
 
-// IP flags. (from kernel's include/net/ip.h)
-#define IP_CE      0x8000  // Flag: "Congestion" (really reserved 'evil bit')
-#define IP_DF      0x4000  // Flag: "Don't Fragment"
-#define IP_MF      0x2000  // Flag: "More Fragments"
-#define IP_OFFSET  0x1FFF  // "Fragment Offset" part
-
-// from kernel's include/net/ipv6.h
-struct frag_hdr {
-    __u8   nexthdr;
-    __u8   reserved;        // always zero
-    __be16 frag_off;        // 13 bit offset, 2 bits zero, 1 bit "More Fragments"
-    __be32 identification;
-};
-
 DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM)
 
 static inline __always_inline int nat64(struct __sk_buff* skb,
@@ -430,4 +399,3 @@
 
 LICENSE("Apache 2.0");
 CRITICAL("Connectivity");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/clatd.h b/bpf/progs/clatd.h
similarity index 100%
rename from bpf_progs/clatd.h
rename to bpf/progs/clatd.h
diff --git a/bpf_progs/dscpPolicy.c b/bpf/progs/dscpPolicy.c
similarity index 83%
rename from bpf_progs/dscpPolicy.c
rename to bpf/progs/dscpPolicy.c
index ed114e4..39f2961 100644
--- a/bpf_progs/dscpPolicy.c
+++ b/bpf/progs/dscpPolicy.c
@@ -14,35 +14,24 @@
  * limitations under the License.
  */
 
-#include <linux/bpf.h>
-#include <linux/if_ether.h>
-#include <linux/if_packet.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/pkt_cls.h>
-#include <linux/tcp.h>
-#include <linux/types.h>
-#include <netinet/in.h>
-#include <netinet/udp.h>
-#include <stdint.h>
-#include <string.h>
-
 // The resulting .o needs to load on Android T+
 #define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
 
-#include "bpf_helpers.h"
+#include "bpf_net_helpers.h"
 #include "dscpPolicy.h"
 
 #define ECN_MASK 3
-#define IP4_OFFSET(field, header) ((header) + offsetof(struct iphdr, field))
 #define UPDATE_TOS(dscp, tos) ((dscp) << 2) | ((tos) & ECN_MASK)
 
-DEFINE_BPF_MAP_GRW(socket_policy_cache_map, HASH, uint64_t, RuleEntry, CACHE_MAP_SIZE, AID_SYSTEM)
+// The cache is never read nor written by userspace and is indexed by socket cookie % CACHE_MAP_SIZE
+#define CACHE_MAP_SIZE 32  // should be a power of two so we can % cheaply
+DEFINE_BPF_MAP_GRO(socket_policy_cache_map, PERCPU_ARRAY, uint32_t, RuleEntry, CACHE_MAP_SIZE,
+                   AID_SYSTEM)
 
 DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
 DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
 
-static inline __always_inline void match_policy(struct __sk_buff* skb, bool ipv4) {
+static inline __always_inline void match_policy(struct __sk_buff* skb, const bool ipv4) {
     void* data = (void*)(long)skb->data;
     const void* data_end = (void*)(long)skb->data_end;
 
@@ -57,6 +46,8 @@
     uint64_t cookie = bpf_get_socket_cookie(skb);
     if (!cookie) return;
 
+    uint32_t cacheid = cookie % CACHE_MAP_SIZE;
+
     __be16 sport = 0;
     uint16_t dport = 0;
     uint8_t protocol = 0;  // TODO: Use are reserved value? Or int (-1) and cast to uint below?
@@ -119,7 +110,8 @@
             return;
     }
 
-    RuleEntry* existing_rule = bpf_socket_policy_cache_map_lookup_elem(&cookie);
+    // this array lookup cannot actually fail
+    RuleEntry* existing_rule = bpf_socket_policy_cache_map_lookup_elem(&cacheid);
 
     if (existing_rule &&
         v6_equal(src_ip, existing_rule->src_ip) &&
@@ -131,9 +123,9 @@
         if (existing_rule->dscp_val < 0) return;
         if (ipv4) {
             uint8_t newTos = UPDATE_TOS(existing_rule->dscp_val, tos);
-            bpf_l3_csum_replace(skb, IP4_OFFSET(check, l2_header_size), htons(tos), htons(newTos),
+            bpf_l3_csum_replace(skb, l2_header_size + IP4_OFFSET(check), htons(tos), htons(newTos),
                                 sizeof(uint16_t));
-            bpf_skb_store_bytes(skb, IP4_OFFSET(tos, l2_header_size), &newTos, sizeof(newTos), 0);
+            bpf_skb_store_bytes(skb, l2_header_size + IP4_OFFSET(tos), &newTos, sizeof(newTos), 0);
         } else {
             __be32 new_first_be32 =
                 htonl(ntohl(old_first_be32) & 0xF03FFFFF | (existing_rule->dscp_val << 22));
@@ -159,27 +151,29 @@
             policy = bpf_ipv6_dscp_policies_map_lookup_elem(&key);
         }
 
-        // If the policy lookup failed, just continue (this should not ever happen)
-        if (!policy) continue;
+        // Lookup failure cannot happen on an array with MAX_POLICIES entries.
+        // While 'continue' would make logical sense here, 'return' should be
+        // easier for the verifier to analyze.
+        if (!policy) return;
 
         // If policy iface index does not match skb, then skip to next policy.
         if (policy->ifindex != skb->ifindex) continue;
 
         int score = 0;
 
-        if (policy->present_fields & PROTO_MASK_FLAG) {
+        if (policy->match_proto) {
             if (protocol != policy->proto) continue;
             score += 0xFFFF;
         }
-        if (policy->present_fields & SRC_IP_MASK_FLAG) {
+        if (policy->match_src_ip) {
             if (v6_not_equal(src_ip, policy->src_ip)) continue;
             score += 0xFFFF;
         }
-        if (policy->present_fields & DST_IP_MASK_FLAG) {
+        if (policy->match_dst_ip) {
             if (v6_not_equal(dst_ip, policy->dst_ip)) continue;
             score += 0xFFFF;
         }
-        if (policy->present_fields & SRC_PORT_MASK_FLAG) {
+        if (policy->match_src_port) {
             if (sport != policy->src_port) continue;
             score += 0xFFFF;
         }
@@ -204,15 +198,15 @@
     };
 
     // Update cache with found policy.
-    bpf_socket_policy_cache_map_update_elem(&cookie, &value, BPF_ANY);
+    bpf_socket_policy_cache_map_update_elem(&cacheid, &value, BPF_ANY);
 
     if (new_dscp < 0) return;
 
     // Need to store bytes after updating map or program will not load.
     if (ipv4) {
         uint8_t new_tos = UPDATE_TOS(new_dscp, tos);
-        bpf_l3_csum_replace(skb, IP4_OFFSET(check, l2_header_size), htons(tos), htons(new_tos), 2);
-        bpf_skb_store_bytes(skb, IP4_OFFSET(tos, l2_header_size), &new_tos, sizeof(new_tos), 0);
+        bpf_l3_csum_replace(skb, l2_header_size + IP4_OFFSET(check), htons(tos), htons(new_tos), 2);
+        bpf_skb_store_bytes(skb, l2_header_size + IP4_OFFSET(tos), &new_tos, sizeof(new_tos), 0);
     } else {
         __be32 new_first_be32 = htonl(ntohl(old_first_be32) & 0xF03FFFFF | (new_dscp << 22));
         bpf_skb_store_bytes(skb, l2_header_size, &new_first_be32, sizeof(__be32),
@@ -238,4 +232,3 @@
 
 LICENSE("Apache 2.0");
 CRITICAL("Connectivity");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/dscpPolicy.h b/bpf/progs/dscpPolicy.h
similarity index 63%
rename from bpf_progs/dscpPolicy.h
rename to bpf/progs/dscpPolicy.h
index e565966..6a6b711 100644
--- a/bpf_progs/dscpPolicy.h
+++ b/bpf/progs/dscpPolicy.h
@@ -14,14 +14,8 @@
  * limitations under the License.
  */
 
-#define CACHE_MAP_SIZE 1024
 #define MAX_POLICIES 16
 
-#define SRC_IP_MASK_FLAG     1
-#define DST_IP_MASK_FLAG     2
-#define SRC_PORT_MASK_FLAG   4
-#define PROTO_MASK_FLAG      8
-
 #define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
 
 // Retrieve the first (ie. high) 64 bits of an IPv6 address (in network order)
@@ -37,17 +31,6 @@
 // Returns 'a == b' as boolean
 #define v6_equal(a, b) (!v6_not_equal((a), (b)))
 
-// TODO: these are already defined in packages/modules/Connectivity/bpf_progs/bpf_net_helpers.h.
-// smove to common location in future.
-static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) =
-        (void*)BPF_FUNC_get_socket_cookie;
-static int (*bpf_skb_store_bytes)(struct __sk_buff* skb, __u32 offset, const void* from, __u32 len,
-                                  __u64 flags) = (void*)BPF_FUNC_skb_store_bytes;
-static int (*bpf_l3_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
-                                  __u64 flags) = (void*)BPF_FUNC_l3_csum_replace;
-static long (*bpf_skb_ecn_set_ce)(struct __sk_buff* skb) =
-        (void*)BPF_FUNC_skb_ecn_set_ce;
-
 typedef struct {
     struct in6_addr src_ip;
     struct in6_addr dst_ip;
@@ -57,10 +40,12 @@
     uint16_t dst_port_end;
     uint8_t proto;
     int8_t dscp_val;  // -1 none, or 0..63 DSCP value
-    uint8_t present_fields;
-    uint8_t pad[3];
+    bool match_src_ip;
+    bool match_dst_ip;
+    bool match_src_port;
+    bool match_proto;
 } DscpPolicy;
-STRUCT_SIZE(DscpPolicy, 2 * 16 + 4 + 3 * 2 + 3 * 1 + 3);  // 48
+STRUCT_SIZE(DscpPolicy, 2 * 16 + 4 + 3 * 2 + 6 * 1);  // 48
 
 typedef struct {
     struct in6_addr src_ip;
@@ -72,4 +57,4 @@
     int8_t dscp_val;  // -1 none, or 0..63 DSCP value
     uint8_t pad[2];
 } RuleEntry;
-STRUCT_SIZE(RuleEntry, 2 * 16 + 1 * 4 + 2 * 2 + 2 * 1 + 2);  // 44
+STRUCT_SIZE(RuleEntry, 2 * 16 + 4 + 2 * 2 + 4 * 1);  // 44
diff --git a/bpf_progs/netd.c b/bpf/progs/netd.c
similarity index 97%
rename from bpf_progs/netd.c
rename to bpf/progs/netd.c
index f08b007..4248a46 100644
--- a/bpf_progs/netd.c
+++ b/bpf/progs/netd.c
@@ -17,19 +17,6 @@
 // The resulting .o needs to load on Android T+
 #define BPFLOADER_MIN_VER BPFLOADER_MAINLINE_T_VERSION
 
-#include <bpf_helpers.h>
-#include <linux/bpf.h>
-#include <linux/if.h>
-#include <linux/if_ether.h>
-#include <linux/if_packet.h>
-#include <linux/in.h>
-#include <linux/in6.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/pkt_cls.h>
-#include <linux/tcp.h>
-#include <stdbool.h>
-#include <stdint.h>
 #include "bpf_net_helpers.h"
 #include "netd.h"
 
@@ -38,10 +25,6 @@
 static const int PASS = 1;
 static const int DROP_UNLESS_DNS = 2;  // internal to our program
 
-// This is used for xt_bpf program only.
-static const int BPF_NOMATCH = 0;
-static const int BPF_MATCH = 1;
-
 // Used for 'bool enable_tracing'
 static const bool TRACE_ON = true;
 static const bool TRACE_OFF = false;
@@ -579,12 +562,12 @@
     if (sock_uid == AID_SYSTEM) {
         uint64_t cookie = bpf_get_socket_cookie(skb);
         UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
-        if (utag && utag->uid == AID_CLAT) return BPF_NOMATCH;
+        if (utag && utag->uid == AID_CLAT) return XTBPF_NOMATCH;
     }
 
     uint32_t key = skb->ifindex;
     update_iface_stats_map(skb, &key, EGRESS, KVER_NONE);
-    return BPF_MATCH;
+    return XTBPF_MATCH;
 }
 
 // WARNING: Android T's non-updatable netd depends on the name of this program.
@@ -597,7 +580,7 @@
 
     uint32_t key = skb->ifindex;
     update_iface_stats_map(skb, &key, INGRESS, KVER_NONE);
-    return BPF_MATCH;
+    return XTBPF_MATCH;
 }
 
 DEFINE_SYS_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN,
@@ -615,7 +598,7 @@
 DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
 (struct __sk_buff* skb) {
     uint32_t sock_uid = bpf_get_socket_uid(skb);
-    if (is_system_uid(sock_uid)) return BPF_MATCH;
+    if (is_system_uid(sock_uid)) return XTBPF_MATCH;
 
     // kernel's DEFAULT_OVERFLOWUID is 65534, this is the overflow 'nobody' uid,
     // usually this being returned means that skb->sk is NULL during RX
@@ -623,11 +606,11 @@
     // packets to an unconnected udp socket.
     // But it can also happen for egress from a timewait socket.
     // Let's treat such cases as 'root' which is_system_uid()
-    if (sock_uid == 65534) return BPF_MATCH;
+    if (sock_uid == 65534) return XTBPF_MATCH;
 
     UidOwnerValue* allowlistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
-    if (allowlistMatch) return allowlistMatch->rule & HAPPY_BOX_MATCH ? BPF_MATCH : BPF_NOMATCH;
-    return BPF_NOMATCH;
+    if (allowlistMatch) return allowlistMatch->rule & HAPPY_BOX_MATCH ? XTBPF_MATCH : XTBPF_NOMATCH;
+    return XTBPF_NOMATCH;
 }
 
 // WARNING: Android T's non-updatable netd depends on the name of this program.
@@ -636,8 +619,8 @@
     uint32_t sock_uid = bpf_get_socket_uid(skb);
     UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
     uint32_t penalty_box = PENALTY_BOX_USER_MATCH | PENALTY_BOX_ADMIN_MATCH;
-    if (denylistMatch) return denylistMatch->rule & penalty_box ? BPF_MATCH : BPF_NOMATCH;
-    return BPF_NOMATCH;
+    if (denylistMatch) return denylistMatch->rule & penalty_box ? XTBPF_MATCH : XTBPF_NOMATCH;
+    return XTBPF_NOMATCH;
 }
 
 static __always_inline inline uint8_t get_app_permissions() {
@@ -657,8 +640,7 @@
 DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet_create", AID_ROOT, AID_ROOT, inet_socket_create,
                           KVER_4_14)
 (__unused struct bpf_sock* sk) {
-    // A return value of 1 means allow, everything else means deny.
-    return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? 1 : 0;
+    return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? BPF_ALLOW : BPF_DISALLOW;
 }
 
 DEFINE_NETD_V_BPF_PROG_KVER("cgroupsockrelease/inet_release", AID_ROOT, AID_ROOT,
@@ -685,7 +667,7 @@
     //   __u32 msg_src_ip6[4];	// BE, R: 1,2,4,8-byte, W: 4,8-byte
     //   __bpf_md_ptr(struct bpf_sock *, sk);
     // };
-    return 1;
+    return BPF_ALLOW;
 }
 
 DEFINE_NETD_V_BPF_PROG_KVER("connect4/inet4_connect", AID_ROOT, AID_ROOT, inet4_connect, KVER_4_14)
@@ -723,7 +705,7 @@
     // Tell kernel to return 'original' kernel reply (instead of the bpf modified buffer)
     // This is important if the answer is larger than PAGE_SIZE (max size this bpf hook can provide)
     ctx->optlen = 0;
-    return 1; // ALLOW
+    return BPF_ALLOW;
 }
 
 DEFINE_NETD_V_BPF_PROG_KVER("setsockopt/prog", AID_ROOT, AID_ROOT, setsockopt_prog, KVER_5_4)
@@ -731,9 +713,8 @@
     // Tell kernel to use/process original buffer provided by userspace.
     // This is important if it is larger than PAGE_SIZE (max size this bpf hook can handle).
     ctx->optlen = 0;
-    return 1; // ALLOW
+    return BPF_ALLOW;
 }
 
 LICENSE("Apache 2.0");
 CRITICAL("Connectivity and netd");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/netd.h b/bpf/progs/netd.h
similarity index 100%
rename from bpf_progs/netd.h
rename to bpf/progs/netd.h
diff --git a/bpf_progs/offload.c b/bpf/progs/offload.c
similarity index 90%
rename from bpf_progs/offload.c
rename to bpf/progs/offload.c
index 4d908d2..7e1184d 100644
--- a/bpf_progs/offload.c
+++ b/bpf/progs/offload.c
@@ -14,16 +14,6 @@
  * limitations under the License.
  */
 
-#include <linux/if.h>
-#include <linux/ip.h>
-#include <linux/ipv6.h>
-#include <linux/pkt_cls.h>
-#include <linux/tcp.h>
-
-// bionic kernel uapi linux/udp.h header is munged...
-#define __kernel_udphdr udphdr
-#include <linux/udp.h>
-
 #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+
@@ -35,61 +25,16 @@
 #define BPFLOADER_MAX_VER BPFLOADER_T_VERSION
 #endif /* MAINLINE */
 
-// Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
-#define TETHERING_UID AID_ROOT
-
-#define TETHERING_GID AID_NETWORK_STACK
-
-#include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
 #include "offload.h"
 
-// From kernel:include/net/ip.h
-#define IP_DF 0x4000  // Flag: "Don't Fragment"
-
-// ----- Helper functions for offsets to fields -----
-
-// They all assume simple IP packets:
-//   - no VLAN ethernet tags
-//   - no IPv4 options (see IPV4_HLEN/TCP4_OFFSET/UDP4_OFFSET)
-//   - no IPv6 extension headers
-//   - no TCP options (see TCP_HLEN)
-
-//#define ETH_HLEN sizeof(struct ethhdr)
-#define IP4_HLEN sizeof(struct iphdr)
-#define IP6_HLEN sizeof(struct ipv6hdr)
-#define TCP_HLEN sizeof(struct tcphdr)
-#define UDP_HLEN sizeof(struct udphdr)
-
-// Offsets from beginning of L4 (TCP/UDP) header
-#define TCP_OFFSET(field) offsetof(struct tcphdr, field)
-#define UDP_OFFSET(field) offsetof(struct udphdr, field)
-
-// Offsets from beginning of L3 (IPv4) header
-#define IP4_OFFSET(field) offsetof(struct iphdr, field)
-#define IP4_TCP_OFFSET(field) (IP4_HLEN + TCP_OFFSET(field))
-#define IP4_UDP_OFFSET(field) (IP4_HLEN + UDP_OFFSET(field))
-
-// Offsets from beginning of L3 (IPv6) header
-#define IP6_OFFSET(field) offsetof(struct ipv6hdr, field)
-#define IP6_TCP_OFFSET(field) (IP6_HLEN + TCP_OFFSET(field))
-#define IP6_UDP_OFFSET(field) (IP6_HLEN + UDP_OFFSET(field))
-
-// Offsets from beginning of L2 (ie. Ethernet) header (which must be present)
-#define ETH_IP4_OFFSET(field) (ETH_HLEN + IP4_OFFSET(field))
-#define ETH_IP4_TCP_OFFSET(field) (ETH_HLEN + IP4_TCP_OFFSET(field))
-#define ETH_IP4_UDP_OFFSET(field) (ETH_HLEN + IP4_UDP_OFFSET(field))
-#define ETH_IP6_OFFSET(field) (ETH_HLEN + IP6_OFFSET(field))
-#define ETH_IP6_TCP_OFFSET(field) (ETH_HLEN + IP6_TCP_OFFSET(field))
-#define ETH_IP6_UDP_OFFSET(field) (ETH_HLEN + IP6_UDP_OFFSET(field))
-
 // ----- Tethering Error Counters -----
 
 // Note that pre-T devices with Mediatek chipsets may have a kernel bug (bad patch
 // "[ALPS05162612] bpf: fix ubsan error") making it impossible to write to non-zero
 // offset of bpf map ARRAYs.  This file (offload.o) loads on S+, but luckily this
 // array is only written by bpf code, and only read by userspace.
-DEFINE_BPF_MAP_RO(tether_error_map, ARRAY, uint32_t, uint32_t, BPF_TETHER_ERR__MAX, TETHERING_GID)
+DEFINE_BPF_MAP_RO(tether_error_map, ARRAY, uint32_t, uint32_t, BPF_TETHER_ERR__MAX, AID_NETWORK_STACK)
 
 #define COUNT_AND_RETURN(counter, ret) do {                     \
     uint32_t code = BPF_TETHER_ERR_ ## counter;                 \
@@ -107,22 +52,22 @@
 // ----- Tethering Data Stats and Limits -----
 
 // Tethering stats, indexed by upstream interface.
-DEFINE_BPF_MAP_GRW(tether_stats_map, HASH, TetherStatsKey, TetherStatsValue, 16, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_stats_map, HASH, TetherStatsKey, TetherStatsValue, 16, AID_NETWORK_STACK)
 
 // Tethering data limit, indexed by upstream interface.
 // (tethering allowed when stats[iif].rxBytes + stats[iif].txBytes < limit[iif])
-DEFINE_BPF_MAP_GRW(tether_limit_map, HASH, TetherLimitKey, TetherLimitValue, 16, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_limit_map, HASH, TetherLimitKey, TetherLimitValue, 16, AID_NETWORK_STACK)
 
 // ----- IPv6 Support -----
 
 DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 64,
-                   TETHERING_GID)
+                   AID_NETWORK_STACK)
 
 DEFINE_BPF_MAP_GRW(tether_downstream64_map, HASH, TetherDownstream64Key, TetherDownstream64Value,
-                   1024, TETHERING_GID)
+                   1024, AID_NETWORK_STACK)
 
 DEFINE_BPF_MAP_GRW(tether_upstream6_map, HASH, TetherUpstream6Key, Tether6Value, 64,
-                   TETHERING_GID)
+                   AID_NETWORK_STACK)
 
 static inline __always_inline int do_forward6(struct __sk_buff* skb,
                                               const struct rawip_bool rawip,
@@ -302,13 +247,13 @@
     return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
 }
 
-DEFINE_BPF_PROG("schedcls/tether_downstream6_ether", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG("schedcls/tether_downstream6_ether", AID_ROOT, AID_NETWORK_STACK,
                 sched_cls_tether_downstream6_ether)
 (struct __sk_buff* skb) {
     return do_forward6(skb, ETHER, DOWNSTREAM, KVER_NONE);
 }
 
-DEFINE_BPF_PROG("schedcls/tether_upstream6_ether", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG("schedcls/tether_upstream6_ether", AID_ROOT, AID_NETWORK_STACK,
                 sched_cls_tether_upstream6_ether)
 (struct __sk_buff* skb) {
     return do_forward6(skb, ETHER, UPSTREAM, KVER_NONE);
@@ -328,26 +273,26 @@
 // and in system/netd/tests/binder_test.cpp NetdBinderTest TetherOffloadForwarding.
 //
 // Hence, these mandatory (must load successfully) implementations for 4.14+ kernels:
-DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$4_14", AID_ROOT, AID_NETWORK_STACK,
                      sched_cls_tether_downstream6_rawip_4_14, KVER_4_14)
 (struct __sk_buff* skb) {
     return do_forward6(skb, RAWIP, DOWNSTREAM, KVER_4_14);
 }
 
-DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$4_14", AID_ROOT, AID_NETWORK_STACK,
                      sched_cls_tether_upstream6_rawip_4_14, KVER_4_14)
 (struct __sk_buff* skb) {
     return do_forward6(skb, RAWIP, UPSTREAM, KVER_4_14);
 }
 
 // and define no-op stubs for pre-4.14 kernels.
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER_4_14)
 (__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER_4_14)
 (__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
@@ -355,9 +300,9 @@
 
 // ----- IPv4 Support -----
 
-DEFINE_BPF_MAP_GRW(tether_downstream4_map, HASH, Tether4Key, Tether4Value, 1024, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_downstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
 
-DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
 
 static inline __always_inline int do_forward4_bottom(struct __sk_buff* skb,
         const int l2_header_size, void* data, const void* data_end,
@@ -656,25 +601,25 @@
 
 // Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
 
-DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
                      sched_cls_tether_downstream4_rawip_5_8, KVER_5_8)
 (struct __sk_buff* skb) {
     return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_8);
 }
 
-DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
                      sched_cls_tether_upstream4_rawip_5_8, KVER_5_8)
 (struct __sk_buff* skb) {
     return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_8);
 }
 
-DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
                      sched_cls_tether_downstream4_ether_5_8, KVER_5_8)
 (struct __sk_buff* skb) {
     return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_8);
 }
 
-DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
                      sched_cls_tether_upstream4_ether_5_8, KVER_5_8)
 (struct __sk_buff* skb) {
     return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_8);
@@ -684,7 +629,7 @@
 // (optional, because we need to be able to fallback for 4.14/4.19/5.4 pre-S kernels)
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt",
-                                    TETHERING_UID, TETHERING_GID,
+                                    AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_downstream4_rawip_opt,
                                     KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
@@ -692,7 +637,7 @@
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
-                                    TETHERING_UID, TETHERING_GID,
+                                    AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_upstream4_rawip_opt,
                                     KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
@@ -700,7 +645,7 @@
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
-                                    TETHERING_UID, TETHERING_GID,
+                                    AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_downstream4_ether_opt,
                                     KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
@@ -708,7 +653,7 @@
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
-                                    TETHERING_UID, TETHERING_GID,
+                                    AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_upstream4_ether_opt,
                                     KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
@@ -729,13 +674,13 @@
 
 // RAWIP: Required for 5.4-R kernels -- which always support bpf_skb_change_head().
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_8)
 (struct __sk_buff* skb) {
     return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_5_4);
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_8)
 (struct __sk_buff* skb) {
     return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_5_4);
@@ -745,7 +690,7 @@
 // [Note: fallback for 4.14/4.19 (P/Q) kernels is below in stub section]
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$4_14",
-                                    TETHERING_UID, TETHERING_GID,
+                                    AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_downstream4_rawip_4_14,
                                     KVER_4_14, KVER_5_4)
 (struct __sk_buff* skb) {
@@ -753,7 +698,7 @@
 }
 
 DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$4_14",
-                                    TETHERING_UID, TETHERING_GID,
+                                    AID_ROOT, AID_NETWORK_STACK,
                                     sched_cls_tether_upstream4_rawip_4_14,
                                     KVER_4_14, KVER_5_4)
 (struct __sk_buff* skb) {
@@ -762,13 +707,13 @@
 
 // ETHER: Required for 4.14-Q/R, 4.19-Q/R & 5.4-R kernels.
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
     return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_8)
 (struct __sk_buff* skb) {
     return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER_4_14);
@@ -778,13 +723,13 @@
 
 // RAWIP: 4.9-P/Q, 4.14-P/Q & 4.19-Q kernels -- without bpf_skb_change_head() for tc programs
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER_5_4)
 (__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER_5_4)
 (__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
@@ -792,13 +737,13 @@
 
 // ETHER: 4.9-P/Q kernel
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER_4_14)
 (__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
 }
 
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK,
                            sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER_4_14)
 (__unused struct __sk_buff* skb) {
     return TC_ACT_PIPE;
@@ -806,7 +751,7 @@
 
 // ----- XDP Support -----
 
-DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, AID_NETWORK_STACK)
 
 static inline __always_inline int do_xdp_forward6(__unused struct xdp_md *ctx,
         __unused const struct rawip_bool rawip, __unused const struct stream_bool stream) {
@@ -853,7 +798,7 @@
 }
 
 #define DEFINE_XDP_PROG(str, func) \
-    DEFINE_BPF_PROG_KVER(str, TETHERING_UID, TETHERING_GID, func, KVER_5_9)(struct xdp_md *ctx)
+    DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER_5_9)(struct xdp_md *ctx)
 
 DEFINE_XDP_PROG("xdp/tether_downstream_ether",
                  xdp_tether_downstream_ether) {
@@ -877,4 +822,3 @@
 
 LICENSE("Apache 2.0");
 CRITICAL("Connectivity (Tethering)");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/offload.h b/bpf/progs/offload.h
similarity index 100%
rename from bpf_progs/offload.h
rename to bpf/progs/offload.h
diff --git a/bpf_progs/offload@mainline.c b/bpf/progs/offload@mainline.c
similarity index 100%
rename from bpf_progs/offload@mainline.c
rename to bpf/progs/offload@mainline.c
diff --git a/bpf_progs/test.c b/bpf/progs/test.c
similarity index 80%
rename from bpf_progs/test.c
rename to bpf/progs/test.c
index 6a4471c..bce402e 100644
--- a/bpf_progs/test.c
+++ b/bpf/progs/test.c
@@ -14,10 +14,6 @@
  * limitations under the License.
  */
 
-#include <linux/if_ether.h>
-#include <linux/in.h>
-#include <linux/ip.h>
-
 #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+
@@ -29,30 +25,24 @@
 #define BPFLOADER_MAX_VER BPFLOADER_T_VERSION
 #endif /* MAINLINE */
 
-// Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
-#define TETHERING_UID AID_ROOT
-
-#define TETHERING_GID AID_NETWORK_STACK
-
 // This is non production code, only used for testing
 // Needed because the bitmap array definition is non-kosher for pre-T OS devices.
 #define THIS_BPF_PROGRAM_IS_FOR_TEST_PURPOSES_ONLY
 
-#include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
 #include "offload.h"
 
 // Used only by TetheringPrivilegedTests, not by production code.
 DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
-                   TETHERING_GID)
+                   AID_NETWORK_STACK)
 DEFINE_BPF_MAP_GRW(tether2_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
-                   TETHERING_GID)
+                   AID_NETWORK_STACK)
 DEFINE_BPF_MAP_GRW(tether3_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
-                   TETHERING_GID)
+                   AID_NETWORK_STACK)
 // Used only by BpfBitmapTest, not by production code.
-DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2, TETHERING_GID)
+DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2, AID_NETWORK_STACK)
 
-DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", TETHERING_UID, TETHERING_GID,
+DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", AID_ROOT, AID_NETWORK_STACK,
                       xdp_test, KVER_5_9)
 (struct xdp_md *ctx) {
     void *data = (void *)(long)ctx->data;
@@ -71,4 +61,3 @@
 }
 
 LICENSE("Apache 2.0");
-DISABLE_BTF_ON_USER_BUILDS();
diff --git a/bpf_progs/test@mainline.c b/bpf/progs/test@mainline.c
similarity index 100%
rename from bpf_progs/test@mainline.c
rename to bpf/progs/test@mainline.c
diff --git a/staticlibs/native/bpf_syscall_wrappers/Android.bp b/bpf/syscall_wrappers/Android.bp
similarity index 100%
rename from staticlibs/native/bpf_syscall_wrappers/Android.bp
rename to bpf/syscall_wrappers/Android.bp
diff --git a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h b/bpf/syscall_wrappers/include/BpfSyscallWrappers.h
similarity index 100%
rename from staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
rename to bpf/syscall_wrappers/include/BpfSyscallWrappers.h
diff --git a/tests/mts/Android.bp b/bpf/tests/mts/Android.bp
similarity index 100%
rename from tests/mts/Android.bp
rename to bpf/tests/mts/Android.bp
diff --git a/tests/mts/OWNERS b/bpf/tests/mts/OWNERS
similarity index 100%
rename from tests/mts/OWNERS
rename to bpf/tests/mts/OWNERS
diff --git a/tests/mts/bpf_existence_test.cpp b/bpf/tests/mts/bpf_existence_test.cpp
similarity index 100%
rename from tests/mts/bpf_existence_test.cpp
rename to bpf/tests/mts/bpf_existence_test.cpp
diff --git a/common/FlaggedApi.bp b/common/FlaggedApi.bp
index fef9ac3..39ff2d4 100644
--- a/common/FlaggedApi.bp
+++ b/common/FlaggedApi.bp
@@ -40,6 +40,16 @@
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
 
+java_aconfig_library {
+    name: "com.android.net.thread.flags-aconfig-java",
+    aconfig_declarations: "com.android.net.thread.flags-aconfig",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
+
 aconfig_declarations {
     name: "nearby_flags",
     package: "com.android.nearby.flags",
@@ -48,6 +58,16 @@
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
 
+java_aconfig_library {
+    name: "com.android.nearby.flags-aconfig-java",
+    aconfig_declarations: "nearby_flags",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
+
 aconfig_declarations {
     name: "com.android.networksecurity.flags-aconfig",
     package: "com.android.net.ct.flags",
diff --git a/common/OWNERS b/common/OWNERS
index e7f5d11..989d286 100644
--- a/common/OWNERS
+++ b/common/OWNERS
@@ -1 +1,2 @@
 per-file thread_flags.aconfig = file:platform/packages/modules/Connectivity:main:/thread/OWNERS
+per-file networksecurity_flags.aconfig = file:platform/packages/modules/Connectivity:main:/networksecurity/OWNERS
\ No newline at end of file
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 1b0da4e..4c6d8ba 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -10,6 +10,7 @@
   namespace: "android_core_networking"
   description: "Set data saver through ConnectivityManager API"
   bug: "297836825"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -18,6 +19,7 @@
   namespace: "android_core_networking"
   description: "This flag controls whether isUidNetworkingBlocked is supported"
   bug: "297836825"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -26,6 +28,7 @@
   namespace: "android_core_networking"
   description: "Block network access for apps in a low importance background state"
   bug: "304347838"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -34,6 +37,7 @@
   namespace: "android_core_networking_ipsec"
   description: "The flag controls the access for getIpSecTransformState and IpSecTransformState"
   bug: "308011229"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -42,6 +46,7 @@
   namespace: "android_core_networking"
   description: "The flag controls the access for the parcelable TetheringRequest with getSoftApConfiguration/setSoftApConfiguration API"
   bug: "216524590"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -50,6 +55,7 @@
   namespace: "android_core_networking"
   description: "Flag for API to support requesting restricted wifi"
   bug: "315835605"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -58,6 +64,7 @@
   namespace: "android_core_networking"
   description: "Flag for local network capability API"
   bug: "313000440"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -66,6 +73,7 @@
   namespace: "android_core_networking"
   description: "Flag for satellite transport API"
   bug: "320514105"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -74,6 +82,7 @@
   namespace: "android_core_networking"
   description: "Flag for API to support nsd subtypes"
   bug: "265095929"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -82,6 +91,7 @@
   namespace: "android_core_networking"
   description: "Flag for API to register nsd offload engine"
   bug: "301713539"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -90,6 +100,7 @@
   namespace: "android_core_networking"
   description: "Flag for metered network firewall chain API"
   bug: "332628891"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -98,6 +109,7 @@
   namespace: "android_core_networking"
   description: "Flag for oem deny chains blocked reasons API"
   bug: "328732146"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -106,6 +118,7 @@
   namespace: "android_core_networking"
   description: "Flag for BLOCKED_REASON_NETWORK_RESTRICTED API"
   bug: "339559837"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -114,6 +127,7 @@
   namespace: "android_core_networking"
   description: "Flag for NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED API"
   bug: "343823469"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -122,6 +136,7 @@
   namespace: "android_core_networking"
   description: "Flag for introducing TETHERING_VIRTUAL type"
   bug: "340376953"
+  is_fixed_read_only: true
 }
 
 flag {
@@ -130,4 +145,14 @@
   namespace: "android_core_networking"
   description: "Flag for NetworkStats#addEntries API"
   bug: "335680025"
+  is_fixed_read_only: true
+}
+
+flag {
+  name: "tethering_active_sessions_metrics"
+  is_exported: true
+  namespace: "android_core_networking"
+  description: "Flag for collecting tethering active sessions metrics"
+  bug: "354619988"
+  is_fixed_read_only: true
 }
diff --git a/common/networksecurity_flags.aconfig b/common/networksecurity_flags.aconfig
index ef8ffcd..6438ba4 100644
--- a/common/networksecurity_flags.aconfig
+++ b/common/networksecurity_flags.aconfig
@@ -6,4 +6,5 @@
     namespace: "network_security"
     description: "Enable service for certificate transparency log list data"
     bug: "319829948"
+    is_fixed_read_only: true
 }
diff --git a/common/thread_flags.aconfig b/common/thread_flags.aconfig
index 43acd1b..c11c6c0 100644
--- a/common/thread_flags.aconfig
+++ b/common/thread_flags.aconfig
@@ -8,3 +8,21 @@
     description: "Controls whether the Android Thread feature is enabled"
     bug: "301473012"
 }
+
+flag {
+    name: "configuration_enabled"
+    is_exported: true
+    is_fixed_read_only: true
+    namespace: "thread_network"
+    description: "Controls whether the Android Thread configuration is enabled"
+    bug: "342519412"
+}
+
+flag {
+    name: "channel_max_powers_enabled"
+    is_exported: true
+    is_fixed_read_only: true
+    namespace: "thread_network"
+    description: "Controls whether the Android Thread setting max power of channel feature is enabled"
+    bug: "346686506"
+}
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index ac78d09..a05a529 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -62,6 +62,8 @@
     static_libs: [
         // Cannot go to framework-connectivity because mid_sdk checks require 31.
         "modules-utils-binary-xml",
+        "com.android.nearby.flags-aconfig-java",
+        "com.android.net.thread.flags-aconfig-java",
     ],
     impl_only_libs: [
         // The build system will use framework-bluetooth module_current stubs, because
@@ -97,20 +99,6 @@
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
 
-// The filegroup lists files that are necessary for verifying building mdns as a standalone,
-// for use with service-connectivity-mdns-standalone-build-test
-// This filegroup should never be included in anywhere in the module build. It is only used for
-// building service-connectivity-mdns-standalone-build-test target. The files will be renamed by
-// copybara to prevent them from being shadowed by the bootclasspath copies.
-filegroup {
-    name: "framework-connectivity-t-mdns-standalone-build-sources",
-    srcs: [
-        "src/android/net/nsd/OffloadEngine.java",
-        "src/android/net/nsd/OffloadServiceInfo.java",
-    ],
-    visibility: ["//packages/modules/Connectivity:__subpackages__"],
-}
-
 java_library {
     name: "framework-connectivity-t-pre-jarjar",
     defaults: ["framework-connectivity-t-defaults"],
@@ -141,6 +129,8 @@
         // been included in static_libs might still need to
         // be in stub_only_libs to be usable when generating the API stubs.
         "com.android.net.flags-aconfig-java",
+        "com.android.nearby.flags-aconfig-java",
+        "com.android.net.thread.flags-aconfig-java",
         // Use prebuilt framework-connectivity stubs to avoid circular dependencies
         "sdk_module-lib_current_framework-connectivity",
     ],
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 2354882..9f26bcf 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -516,6 +516,7 @@
     method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void registerOperationalDatasetCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
     method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.StateCallback);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void scheduleMigration(@NonNull android.net.thread.PendingOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+    method @FlaggedApi("com.android.net.thread.flags.channel_max_powers_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void setChannelMaxPowers(@NonNull @Size(min=1) android.util.SparseIntArray, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void setEnabled(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
     method @FlaggedApi("com.android.net.thread.flags.configuration_enabled") @RequiresPermission(android.Manifest.permission.THREAD_NETWORK_PRIVILEGED) public void unregisterConfigurationCallback(@NonNull java.util.function.Consumer<android.net.thread.ThreadConfiguration>);
     method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
@@ -525,6 +526,7 @@
     field public static final int DEVICE_ROLE_LEADER = 4; // 0x4
     field public static final int DEVICE_ROLE_ROUTER = 3; // 0x3
     field public static final int DEVICE_ROLE_STOPPED = 0; // 0x0
+    field public static final int MAX_POWER_CHANNEL_DISABLED = -2147483648; // 0x80000000
     field public static final int STATE_DISABLED = 0; // 0x0
     field public static final int STATE_DISABLING = 2; // 0x2
     field public static final int STATE_ENABLED = 1; // 0x1
@@ -557,6 +559,7 @@
     field public static final int ERROR_UNAVAILABLE = 4; // 0x4
     field public static final int ERROR_UNKNOWN = 11; // 0xb
     field public static final int ERROR_UNSUPPORTED_CHANNEL = 7; // 0x7
+    field public static final int ERROR_UNSUPPORTED_FEATURE = 13; // 0xd
   }
 
   @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkManager {
diff --git a/framework-t/src/android/net/IpSecManager.java b/framework-t/src/android/net/IpSecManager.java
index 3f74e1c..39e2b5b 100644
--- a/framework-t/src/android/net/IpSecManager.java
+++ b/framework-t/src/android/net/IpSecManager.java
@@ -65,13 +65,6 @@
 public class IpSecManager {
     private static final String TAG = "IpSecManager";
 
-    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
-    // available here
-    /** @hide */
-    public static class Flags {
-        static final String IPSEC_TRANSFORM_STATE = "com.android.net.flags.ipsec_transform_state";
-    }
-
     /**
      * Feature flag to declare the kernel support of updating IPsec SAs.
      *
diff --git a/framework-t/src/android/net/IpSecTransform.java b/framework-t/src/android/net/IpSecTransform.java
index 70c9bc8..35bd008 100644
--- a/framework-t/src/android/net/IpSecTransform.java
+++ b/framework-t/src/android/net/IpSecTransform.java
@@ -15,7 +15,6 @@
  */
 package android.net;
 
-import static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE;
 import static android.net.IpSecManager.INVALID_RESOURCE_ID;
 
 import android.annotation.CallbackExecutor;
@@ -35,6 +34,7 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.flags.Flags;
 
 import dalvik.system.CloseGuard;
 
@@ -220,7 +220,7 @@
      *     occurs.
      * @see IpSecTransformState
      */
-    @FlaggedApi(IPSEC_TRANSFORM_STATE)
+    @FlaggedApi(Flags.FLAG_IPSEC_TRANSFORM_STATE)
     public void requestIpSecTransformState(
             @CallbackExecutor @NonNull Executor executor,
             @NonNull OutcomeReceiver<IpSecTransformState, RuntimeException> callback) {
diff --git a/framework-t/src/android/net/IpSecTransformState.java b/framework-t/src/android/net/IpSecTransformState.java
index 5b80ae2..b6628ee 100644
--- a/framework-t/src/android/net/IpSecTransformState.java
+++ b/framework-t/src/android/net/IpSecTransformState.java
@@ -15,8 +15,6 @@
  */
 package android.net;
 
-import static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE;
-
 import static com.android.internal.annotations.VisibleForTesting.Visibility;
 
 import android.annotation.FlaggedApi;
@@ -26,6 +24,7 @@
 import android.os.SystemClock;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.flags.Flags;
 import com.android.net.module.util.HexDump;
 
 import java.util.Objects;
@@ -39,7 +38,7 @@
  * IpSecTransformStates at two timestamps. By comparing the changes in packet counts and sequence
  * numbers, callers can estimate IPsec data loss in the inbound direction.
  */
-@FlaggedApi(IPSEC_TRANSFORM_STATE)
+@FlaggedApi(Flags.FLAG_IPSEC_TRANSFORM_STATE)
 public final class IpSecTransformState implements Parcelable {
     private final long mTimestamp;
     private final long mTxHighestSequenceNumber;
@@ -197,7 +196,7 @@
      * <p>Except for testing, IPsec callers normally do not instantiate {@link IpSecTransformState}
      * themselves but instead get a reference via {@link IpSecTransformState}
      */
-    @FlaggedApi(IPSEC_TRANSFORM_STATE)
+    @FlaggedApi(Flags.FLAG_IPSEC_TRANSFORM_STATE)
     public static final class Builder {
         private long mTimestamp;
         private long mTxHighestSequenceNumber;
diff --git a/framework-t/src/android/net/nsd/DiscoveryRequest.java b/framework-t/src/android/net/nsd/DiscoveryRequest.java
index b0b71ea..b344943 100644
--- a/framework-t/src/android/net/nsd/DiscoveryRequest.java
+++ b/framework-t/src/android/net/nsd/DiscoveryRequest.java
@@ -24,12 +24,14 @@
 import android.os.Parcelable;
 import android.text.TextUtils;
 
+import com.android.net.flags.Flags;
+
 import java.util.Objects;
 
 /**
  * Encapsulates parameters for {@link NsdManager#discoverServices}.
  */
-@FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+@FlaggedApi(Flags.FLAG_NSD_SUBTYPES_SUPPORT_ENABLED)
 public final class DiscoveryRequest implements Parcelable {
     private final int mProtocolType;
 
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index b21e22a..116bea6 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -52,6 +52,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
 import com.android.net.module.util.CollectionUtils;
 
 import java.lang.annotation.Retention;
@@ -148,22 +149,6 @@
     private static final String TAG = NsdManager.class.getSimpleName();
     private static final boolean DBG = false;
 
-    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
-    // available here
-    /** @hide */
-    public static class Flags {
-        static final String REGISTER_NSD_OFFLOAD_ENGINE_API =
-                "com.android.net.flags.register_nsd_offload_engine_api";
-        static final String NSD_SUBTYPES_SUPPORT_ENABLED =
-                "com.android.net.flags.nsd_subtypes_support_enabled";
-        static final String ADVERTISE_REQUEST_API =
-                "com.android.net.flags.advertise_request_api";
-        static final String NSD_CUSTOM_HOSTNAME_ENABLED =
-                "com.android.net.flags.nsd_custom_hostname_enabled";
-        static final String NSD_CUSTOM_TTL_ENABLED =
-                "com.android.net.flags.nsd_custom_ttl_enabled";
-    }
-
     /**
      * A regex for the acceptable format of a type or subtype label.
      * @hide
@@ -451,7 +436,7 @@
      *
      * @hide
      */
-    @FlaggedApi(NsdManager.Flags.REGISTER_NSD_OFFLOAD_ENGINE_API)
+    @FlaggedApi(Flags.FLAG_REGISTER_NSD_OFFLOAD_ENGINE_API)
     @SystemApi
     @RequiresPermission(anyOf = {NETWORK_SETTINGS, PERMISSION_MAINLINE_NETWORK_STACK,
             NETWORK_STACK})
@@ -489,7 +474,7 @@
      *
      * @hide
      */
-    @FlaggedApi(NsdManager.Flags.REGISTER_NSD_OFFLOAD_ENGINE_API)
+    @FlaggedApi(Flags.FLAG_REGISTER_NSD_OFFLOAD_ENGINE_API)
     @SystemApi
     @RequiresPermission(anyOf = {NETWORK_SETTINGS, PERMISSION_MAINLINE_NETWORK_STACK,
             NETWORK_STACK})
@@ -1506,7 +1491,7 @@
      * @param listener  The listener notifies of a successful discovery and is used
      * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}.
      */
-    @FlaggedApi(Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+    @FlaggedApi(Flags.FLAG_NSD_SUBTYPES_SUPPORT_ENABLED)
     public void discoverServices(@NonNull DiscoveryRequest discoveryRequest,
             @NonNull Executor executor, @NonNull DiscoveryListener listener) {
         int key = putListener(listener, executor, discoveryRequest);
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index d8cccb2..18c59d9 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -30,6 +30,7 @@
 import android.util.ArraySet;
 import android.util.Log;
 
+import com.android.net.flags.Flags;
 import com.android.net.module.util.InetAddressUtils;
 
 import java.io.UnsupportedEncodingException;
@@ -527,7 +528,7 @@
      * Only one subtype will be registered if multiple elements of {@code subtypes} have the same
      * case-insensitive value.
      */
-    @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+    @FlaggedApi(Flags.FLAG_NSD_SUBTYPES_SUPPORT_ENABLED)
     public void setSubtypes(@NonNull Set<String> subtypes) {
         mSubtypes.clear();
         mSubtypes.addAll(subtypes);
@@ -540,7 +541,7 @@
      * NsdManager.DiscoveryListener}), the return value may or may not include the subtypes of this
      * service.
      */
-    @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+    @FlaggedApi(Flags.FLAG_NSD_SUBTYPES_SUPPORT_ENABLED)
     @NonNull
     public Set<String> getSubtypes() {
         return Collections.unmodifiableSet(mSubtypes);
diff --git a/framework-t/src/android/net/nsd/OffloadEngine.java b/framework-t/src/android/net/nsd/OffloadEngine.java
index 9015985..06655fa 100644
--- a/framework-t/src/android/net/nsd/OffloadEngine.java
+++ b/framework-t/src/android/net/nsd/OffloadEngine.java
@@ -21,6 +21,8 @@
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 
+import com.android.net.flags.Flags;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -34,7 +36,7 @@
  *
  * @hide
  */
-@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")
+@FlaggedApi(Flags.FLAG_REGISTER_NSD_OFFLOAD_ENGINE_API)
 @SystemApi
 public interface OffloadEngine {
     /**
diff --git a/framework-t/src/android/net/nsd/OffloadServiceInfo.java b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
index 98dc83a..e4b2f43 100644
--- a/framework-t/src/android/net/nsd/OffloadServiceInfo.java
+++ b/framework-t/src/android/net/nsd/OffloadServiceInfo.java
@@ -26,6 +26,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.net.flags.Flags;
 import com.android.net.module.util.HexDump;
 
 import java.util.Arrays;
@@ -40,7 +41,7 @@
  *
  * @hide
  */
-@FlaggedApi("com.android.net.flags.register_nsd_offload_engine_api")
+@FlaggedApi(Flags.FLAG_REGISTER_NSD_OFFLOAD_ENGINE_API)
 @SystemApi
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public final class OffloadServiceInfo implements Parcelable {
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 8cf6e04..63a6cd2 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -78,6 +78,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
 
 import libcore.net.event.NetworkEventDispatcher;
 
@@ -125,24 +126,6 @@
     private static final String TAG = "ConnectivityManager";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
-    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
-    // available here
-    /** @hide */
-    public static class Flags {
-        static final String SET_DATA_SAVER_VIA_CM =
-                "com.android.net.flags.set_data_saver_via_cm";
-        static final String SUPPORT_IS_UID_NETWORKING_BLOCKED =
-                "com.android.net.flags.support_is_uid_networking_blocked";
-        static final String BASIC_BACKGROUND_RESTRICTIONS_ENABLED =
-                "com.android.net.flags.basic_background_restrictions_enabled";
-        static final String METERED_NETWORK_FIREWALL_CHAINS =
-                "com.android.net.flags.metered_network_firewall_chains";
-        static final String BLOCKED_REASON_OEM_DENY_CHAINS =
-                "com.android.net.flags.blocked_reason_oem_deny_chains";
-        static final String BLOCKED_REASON_NETWORK_RESTRICTED =
-                "com.android.net.flags.blocked_reason_network_restricted";
-    }
-
     /**
      * A change in network connectivity has occurred. A default connection has either
      * been established or lost. The NetworkInfo for the affected network is
@@ -919,7 +902,7 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
+    @FlaggedApi(Flags.FLAG_BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int BLOCKED_REASON_APP_BACKGROUND = 1 << 6;
 
@@ -932,7 +915,7 @@
      * @see #FIREWALL_CHAIN_OEM_DENY_3
      * @hide
      */
-    @FlaggedApi(Flags.BLOCKED_REASON_OEM_DENY_CHAINS)
+    @FlaggedApi(Flags.FLAG_BLOCKED_REASON_OEM_DENY_CHAINS)
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int BLOCKED_REASON_OEM_DENY = 1 << 7;
 
@@ -943,7 +926,7 @@
      *
      * @hide
      */
-    @FlaggedApi(Flags.BLOCKED_REASON_NETWORK_RESTRICTED)
+    @FlaggedApi(Flags.FLAG_BLOCKED_REASON_NETWORK_RESTRICTED)
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public static final int BLOCKED_REASON_NETWORK_RESTRICTED = 1 << 8;
 
@@ -1052,7 +1035,7 @@
      * exempted for specific situations while in the background.
      * @hide
      */
-    @FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
+    @FlaggedApi(Flags.FLAG_BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
     @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_BACKGROUND = 6;
 
@@ -1120,7 +1103,7 @@
      * @hide
      */
     // TODO: Merge this chain with data saver and support setFirewallChainEnabled
-    @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+    @FlaggedApi(Flags.FLAG_METERED_NETWORK_FIREWALL_CHAINS)
     @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_METERED_ALLOW = 10;
 
@@ -1139,7 +1122,7 @@
      * @hide
      */
     // TODO: Support setFirewallChainEnabled to control this chain
-    @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+    @FlaggedApi(Flags.FLAG_METERED_NETWORK_FIREWALL_CHAINS)
     @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_METERED_DENY_USER = 11;
 
@@ -1158,7 +1141,7 @@
      * @hide
      */
     // TODO: Support setFirewallChainEnabled to control this chain
-    @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+    @FlaggedApi(Flags.FLAG_METERED_NETWORK_FIREWALL_CHAINS)
     @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_METERED_DENY_ADMIN = 12;
 
@@ -6453,7 +6436,7 @@
      * @throws IllegalStateException if failed.
      * @hide
      */
-    @FlaggedApi(Flags.SET_DATA_SAVER_VIA_CM)
+    @FlaggedApi(Flags.FLAG_SET_DATA_SAVER_VIA_CM)
     @SystemApi(client = MODULE_LIBRARIES)
     @RequiresPermission(anyOf = {
             android.Manifest.permission.NETWORK_SETTINGS,
@@ -6714,7 +6697,7 @@
     // is provided by linux file group permission AID_NET_BW_ACCT and the
     // selinux context fs_bpf_net*.
     // Only the system server process and the network stack have access.
-    @FlaggedApi(Flags.SUPPORT_IS_UID_NETWORKING_BLOCKED)
+    @FlaggedApi(Flags.FLAG_SUPPORT_IS_UID_NETWORKING_BLOCKED)
     @SystemApi(client = MODULE_LIBRARIES)
     // Note b/326143935 kernel bug can trigger crash on some T device.
     @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 6a14bde..4a50397 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -41,6 +41,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
 import com.android.net.module.util.BitUtils;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.NetworkCapabilitiesUtils;
@@ -124,22 +125,6 @@
 public final class NetworkCapabilities implements Parcelable {
     private static final String TAG = "NetworkCapabilities";
 
-    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
-    // available here
-    /** @hide */
-    public static class Flags {
-        static final String FLAG_FORBIDDEN_CAPABILITY =
-                "com.android.net.flags.forbidden_capability";
-        static final String FLAG_NET_CAPABILITY_LOCAL_NETWORK =
-                "com.android.net.flags.net_capability_local_network";
-        static final String REQUEST_RESTRICTED_WIFI =
-                "com.android.net.flags.request_restricted_wifi";
-        static final String SUPPORT_TRANSPORT_SATELLITE =
-                "com.android.net.flags.support_transport_satellite";
-        static final String NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED =
-                "com.android.net.flags.net_capability_not_bandwidth_constrained";
-    }
-
     /**
      * Mechanism to support redaction of fields in NetworkCapabilities that are guarded by specific
      * app permissions.
@@ -761,7 +746,7 @@
      * usage on constrained networks, such as disabling network access to apps that are not in the
      * foreground.
      */
-    @FlaggedApi(Flags.NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+    @FlaggedApi(Flags.FLAG_NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
     public static final int NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED = 37;
 
     private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED;
@@ -1374,7 +1359,7 @@
     /**
      * Indicates this network uses a Satellite transport.
      */
-    @FlaggedApi(Flags.SUPPORT_TRANSPORT_SATELLITE)
+    @FlaggedApi(Flags.FLAG_SUPPORT_TRANSPORT_SATELLITE)
     public static final int TRANSPORT_SATELLITE = 10;
 
     /** @hide */
@@ -2864,7 +2849,7 @@
      * @return
      */
     @NonNull
-    @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
+    @FlaggedApi(Flags.FLAG_REQUEST_RESTRICTED_WIFI)
     public Set<Integer> getSubscriptionIds() {
         return new ArraySet<>(mSubIds);
     }
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 502ac6f..89572b3 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -50,6 +50,8 @@
 import android.text.TextUtils;
 import android.util.Range;
 
+import com.android.net.flags.Flags;
+
 import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
@@ -144,12 +146,6 @@
  * Look up the specific capability to learn whether its usage requires this self-certification.
  */
 public class NetworkRequest implements Parcelable {
-
-    /** @hide */
-    public static class Flags {
-        static final String REQUEST_RESTRICTED_WIFI =
-                "com.android.net.flags.request_restricted_wifi";
-    }
     /**
      * The first requestId value that will be allocated.
      * @hide only used by ConnectivityService.
@@ -616,7 +612,7 @@
          * @param subIds A {@code Set} that represents subscription IDs.
          */
         @NonNull
-        @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
+        @FlaggedApi(Flags.FLAG_REQUEST_RESTRICTED_WIFI)
         public Builder setSubscriptionIds(@NonNull Set<Integer> subIds) {
             mNetworkCapabilities.setSubscriptionIds(subIds);
             return this;
@@ -880,7 +876,7 @@
      * @return Set of Integer values for this instance.
      */
     @NonNull
-    @FlaggedApi(Flags.REQUEST_RESTRICTED_WIFI)
+    @FlaggedApi(Flags.FLAG_REQUEST_RESTRICTED_WIFI)
     public Set<Integer> getSubscriptionIds() {
         // No need to make a defensive copy here as NC#getSubscriptionIds() already returns
         // a new set.
diff --git a/nearby/framework/Android.bp b/nearby/framework/Android.bp
index 41a28a0..f84ddcf 100644
--- a/nearby/framework/Android.bp
+++ b/nearby/framework/Android.bp
@@ -54,6 +54,7 @@
     ],
     static_libs: [
         "modules-utils-preconditions",
+        "com.android.nearby.flags-aconfig-java",
     ],
     visibility: [
         "//packages/modules/Connectivity/nearby/tests:__subpackages__",
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index cae653d..39adee3 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -37,6 +37,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
+import com.android.nearby.flags.Flags;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -84,7 +85,7 @@
      * Return value of {@link #getPoweredOffFindingMode()} when this powered off finding is not
      * supported the device.
      */
-    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
     public static final int POWERED_OFF_FINDING_MODE_UNSUPPORTED = 0;
 
     /**
@@ -92,7 +93,7 @@
      * #setPoweredOffFindingMode(int)} when powered off finding is supported but disabled. The
      * device will not start to advertise when powered off.
      */
-    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
     public static final int POWERED_OFF_FINDING_MODE_DISABLED = 1;
 
     /**
@@ -100,7 +101,7 @@
      * #setPoweredOffFindingMode(int)} when powered off finding is enabled. The device will start to
      * advertise when powered off.
      */
-    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
     public static final int POWERED_OFF_FINDING_MODE_ENABLED = 2;
 
     /**
@@ -526,7 +527,7 @@
      *
      * @throws IllegalArgumentException if the length of one of the EIDs is not 20 bytes
      */
-    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
     public void setPoweredOffFindingEphemeralIds(@NonNull List<byte[]> eids) {
         Objects.requireNonNull(eids);
@@ -570,7 +571,7 @@
      * @throws IllegalStateException if called with {@link #POWERED_OFF_FINDING_MODE_ENABLED} when
      * Bluetooth or location services are disabled
      */
-    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
     public void setPoweredOffFindingMode(@PoweredOffFindingMode int poweredOffFindingMode) {
         Preconditions.checkArgument(
@@ -602,7 +603,7 @@
      * #POWERED_OFF_FINDING_MODE_ENABLED} if this was the last value set by {@link
      * #setPoweredOffFindingMode(int)}
      */
-    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @FlaggedApi(Flags.FLAG_POWERED_OFF_FINDING)
     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
     public @PoweredOffFindingMode int getPoweredOffFindingMode() {
         if (!isPoweredOffFindingSupported()) {
diff --git a/networksecurity/TEST_MAPPING b/networksecurity/TEST_MAPPING
new file mode 100644
index 0000000..20ecbce
--- /dev/null
+++ b/networksecurity/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "postsubmit": [
+    {
+      "name": "NetworkSecurityUnitTests"
+    }
+  ]
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
new file mode 100644
index 0000000..f35b163
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -0,0 +1,190 @@
+/*
+ * 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.server.net.ct;
+
+import android.app.DownloadManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/** Helper class to download certificate transparency log files. */
+class CertificateTransparencyDownloader extends BroadcastReceiver {
+
+    private static final String TAG = "CertificateTransparencyDownloader";
+
+    private final Context mContext;
+    private final DataStore mDataStore;
+    private final DownloadHelper mDownloadHelper;
+    private final CertificateTransparencyInstaller mInstaller;
+
+    @VisibleForTesting
+    CertificateTransparencyDownloader(
+            Context context,
+            DataStore dataStore,
+            DownloadHelper downloadHelper,
+            CertificateTransparencyInstaller installer) {
+        mContext = context;
+        mDataStore = dataStore;
+        mDownloadHelper = downloadHelper;
+        mInstaller = installer;
+    }
+
+    CertificateTransparencyDownloader(Context context, DataStore dataStore) {
+        this(
+                context,
+                dataStore,
+                new DownloadHelper(context),
+                new CertificateTransparencyInstaller());
+    }
+
+    void registerReceiver() {
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
+        mContext.registerReceiver(this, intentFilter);
+
+        if (Config.DEBUG) {
+            Log.d(TAG, "CertificateTransparencyDownloader initialized successfully");
+        }
+    }
+
+    void startMetadataDownload(String metadataUrl) {
+        long downloadId = download(metadataUrl);
+        if (downloadId == -1) {
+            Log.e(TAG, "Metadata download request failed for " + metadataUrl);
+            return;
+        }
+        mDataStore.setPropertyLong(Config.METADATA_URL_KEY, downloadId);
+        mDataStore.store();
+    }
+
+    void startContentDownload(String contentUrl) {
+        long downloadId = download(contentUrl);
+        if (downloadId == -1) {
+            Log.e(TAG, "Content download request failed for " + contentUrl);
+            return;
+        }
+        mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, downloadId);
+        mDataStore.store();
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+        if (!DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
+            Log.w(TAG, "Received unexpected broadcast with action " + action);
+            return;
+        }
+
+        long completedId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
+        if (completedId == -1) {
+            Log.e(TAG, "Invalid completed download Id");
+            return;
+        }
+
+        if (isMetadataDownloadId(completedId)) {
+            handleMetadataDownloadCompleted(completedId);
+            return;
+        }
+
+        if (isContentDownloadId(completedId)) {
+            handleContentDownloadCompleted(completedId);
+            return;
+        }
+
+        Log.e(TAG, "Download id " + completedId + " is neither metadata nor content.");
+    }
+
+    private void handleMetadataDownloadCompleted(long downloadId) {
+        if (!mDownloadHelper.isSuccessful(downloadId)) {
+            Log.w(TAG, "Metadata download failed.");
+            // TODO: re-attempt download
+            return;
+        }
+
+        startContentDownload(mDataStore.getProperty(Config.CONTENT_URL_PENDING));
+    }
+
+    private void handleContentDownloadCompleted(long downloadId) {
+        if (!mDownloadHelper.isSuccessful(downloadId)) {
+            Log.w(TAG, "Content download failed.");
+            // TODO: re-attempt download
+            return;
+        }
+
+        Uri contentUri = getContentDownloadUri();
+        Uri metadataUri = getMetadataDownloadUri();
+        if (contentUri == null || metadataUri == null) {
+            Log.e(TAG, "Invalid URIs");
+            return;
+        }
+
+        // TODO: 1. verify file signature, 2. validate file content.
+
+        String version = mDataStore.getProperty(Config.VERSION_PENDING);
+        String contentUrl = mDataStore.getProperty(Config.CONTENT_URL_PENDING);
+        String metadataUrl = mDataStore.getProperty(Config.METADATA_URL_PENDING);
+        boolean success = false;
+        try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
+            success = mInstaller.install(inputStream, version);
+        } catch (IOException e) {
+            Log.e(TAG, "Could not install new content", e);
+            return;
+        }
+
+        if (success) {
+            // Update information about the stored version on successful install.
+            mDataStore.setProperty(Config.VERSION, version);
+            mDataStore.setProperty(Config.CONTENT_URL, contentUrl);
+            mDataStore.setProperty(Config.METADATA_URL, metadataUrl);
+            mDataStore.store();
+        }
+    }
+
+    private long download(String url) {
+        try {
+            return mDownloadHelper.startDownload(url);
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "Download request failed", e);
+            return -1;
+        }
+    }
+
+    @VisibleForTesting
+    boolean isMetadataDownloadId(long downloadId) {
+        return mDataStore.getPropertyLong(Config.METADATA_URL_KEY, -1) == downloadId;
+    }
+
+    @VisibleForTesting
+    boolean isContentDownloadId(long downloadId) {
+        return mDataStore.getPropertyLong(Config.CONTENT_URL_KEY, -1) == downloadId;
+    }
+
+    private Uri getMetadataDownloadUri() {
+        return mDownloadHelper.getUri(mDataStore.getPropertyLong(Config.METADATA_URL_KEY, -1));
+    }
+
+    private Uri getContentDownloadUri() {
+        return mDownloadHelper.getUri(mDataStore.getPropertyLong(Config.CONTENT_URL_KEY, -1));
+    }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
index 8dd5951..fdac434 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
@@ -30,17 +30,25 @@
 /** Listener class for the Certificate Transparency Phenotype flags. */
 class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesChangedListener {
 
-    private static final String TAG = "CertificateTransparency";
+    private static final String TAG = "CertificateTransparencyFlagsListener";
 
-    private static final String VERSION = "version";
-    private static final String CONTENT_URL = "content_url";
-    private static final String METADATA_URL = "metadata_url";
+    private final DataStore mDataStore;
+    private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
 
-    CertificateTransparencyFlagsListener(Context context) {}
+    CertificateTransparencyFlagsListener(Context context) {
+        mDataStore = new DataStore(Config.PREFERENCES_FILE);
+        mCertificateTransparencyDownloader =
+                new CertificateTransparencyDownloader(context, mDataStore);
+    }
 
     void initialize() {
+        mDataStore.load();
+        mCertificateTransparencyDownloader.registerReceiver();
         DeviceConfig.addOnPropertiesChangedListener(
                 NAMESPACE_TETHERING, Executors.newSingleThreadExecutor(), this);
+        if (Config.DEBUG) {
+            Log.d(TAG, "CertificateTransparencyFlagsListener initialized successfully");
+        }
         // TODO: handle property changes triggering on boot before registering this listener.
     }
 
@@ -50,18 +58,38 @@
             return;
         }
 
-        String newVersion = DeviceConfig.getString(NAMESPACE_TETHERING, VERSION, "");
-        String newContentUrl = DeviceConfig.getString(NAMESPACE_TETHERING, CONTENT_URL, "");
-        String newMetadataUrl = DeviceConfig.getString(NAMESPACE_TETHERING, METADATA_URL, "");
+        String newVersion = DeviceConfig.getString(NAMESPACE_TETHERING, Config.VERSION, "");
+        String newContentUrl = DeviceConfig.getString(NAMESPACE_TETHERING, Config.CONTENT_URL, "");
+        String newMetadataUrl =
+                DeviceConfig.getString(NAMESPACE_TETHERING, Config.METADATA_URL, "");
         if (TextUtils.isEmpty(newVersion)
                 || TextUtils.isEmpty(newContentUrl)
                 || TextUtils.isEmpty(newMetadataUrl)) {
             return;
         }
 
-        Log.d(TAG, "newVersion=" + newVersion);
-        Log.d(TAG, "newContentUrl=" + newContentUrl);
-        Log.d(TAG, "newMetadataUrl=" + newMetadataUrl);
-        // TODO: start download of URLs.
+        if (Config.DEBUG) {
+            Log.d(TAG, "newVersion=" + newVersion);
+            Log.d(TAG, "newContentUrl=" + newContentUrl);
+            Log.d(TAG, "newMetadataUrl=" + newMetadataUrl);
+        }
+
+        String oldVersion = mDataStore.getProperty(Config.VERSION);
+        String oldContentUrl = mDataStore.getProperty(Config.CONTENT_URL);
+        String oldMetadataUrl = mDataStore.getProperty(Config.METADATA_URL);
+
+        if (TextUtils.equals(newVersion, oldVersion)
+                && TextUtils.equals(newContentUrl, oldContentUrl)
+                && TextUtils.equals(newMetadataUrl, oldMetadataUrl)) {
+            Log.i(TAG, "No flag changed, ignoring update");
+            return;
+        }
+
+        mDataStore.setProperty(Config.VERSION_PENDING, newVersion);
+        mDataStore.setProperty(Config.CONTENT_URL_PENDING, newContentUrl);
+        mDataStore.setProperty(Config.METADATA_URL_PENDING, newMetadataUrl);
+        mDataStore.store();
+
+        mCertificateTransparencyDownloader.startMetadataDownload(newMetadataUrl);
     }
 }
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
new file mode 100644
index 0000000..82dcadf
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
@@ -0,0 +1,162 @@
+/*
+ * 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.server.net.ct;
+
+import android.annotation.SuppressLint;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+
+/** Installer of CT log lists. */
+public class CertificateTransparencyInstaller {
+
+    private static final String TAG = "CertificateTransparencyInstaller";
+    private static final String CT_DIR_NAME = "/data/misc/keychain/ct/";
+
+    static final String LOGS_DIR_PREFIX = "logs-";
+    static final String LOGS_LIST_FILE_NAME = "log_list.json";
+    static final String CURRENT_DIR_SYMLINK_NAME = "current";
+
+    private final File mCertificateTransparencyDir;
+    private final File mCurrentDirSymlink;
+
+    CertificateTransparencyInstaller(File certificateTransparencyDir) {
+        mCertificateTransparencyDir = certificateTransparencyDir;
+        mCurrentDirSymlink = new File(certificateTransparencyDir, CURRENT_DIR_SYMLINK_NAME);
+    }
+
+    CertificateTransparencyInstaller() {
+        this(new File(CT_DIR_NAME));
+    }
+
+    /**
+     * Install a new log list to use during SCT verification.
+     *
+     * @param newContent an input stream providing the log list
+     * @param version the version of the new log list
+     * @return true if the log list was installed successfully, false otherwise.
+     * @throws IOException if the list cannot be saved in the CT directory.
+     */
+    public boolean install(InputStream newContent, String version) throws IOException {
+        // To support atomically replacing the old configuration directory with the new there's a
+        // bunch of steps. We create a new directory with the logs and then do an atomic update of
+        // the current symlink to point to the new directory.
+        // 1. Ensure that the update dir exists and is readable.
+        makeDir(mCertificateTransparencyDir);
+
+        File newLogsDir = new File(mCertificateTransparencyDir, LOGS_DIR_PREFIX + version);
+        // 2. Handle the corner case where the new directory already exists.
+        if (newLogsDir.exists()) {
+            // If the symlink has already been updated then the update died between steps 6 and 7
+            // and so we cannot delete the directory since it is in use.
+            if (newLogsDir.getCanonicalPath().equals(mCurrentDirSymlink.getCanonicalPath())) {
+                deleteOldLogDirectories();
+                return false;
+            }
+            // If the symlink has not been updated then the previous installation failed and this is
+            // a re-attempt. Clean-up leftover files and try again.
+            deleteContentsAndDir(newLogsDir);
+        }
+        try {
+            // 3. Create /data/misc/keychain/ct/logs-<new_version>/ .
+            makeDir(newLogsDir);
+
+            // 4. Move the log list json file in logs-<new_version>/ .
+            File logListFile = new File(newLogsDir, LOGS_LIST_FILE_NAME);
+            if (Files.copy(newContent, logListFile.toPath()) == 0) {
+                throw new IOException("The log list appears empty");
+            }
+            setWorldReadable(logListFile);
+
+            // 5. Create temp symlink. We rename this to the target symlink to get an atomic update.
+            File tempSymlink = new File(mCertificateTransparencyDir, "new_symlink");
+            try {
+                Os.symlink(newLogsDir.getCanonicalPath(), tempSymlink.getCanonicalPath());
+            } catch (ErrnoException e) {
+                throw new IOException("Failed to create symlink", e);
+            }
+
+            // 6. Update the symlink target, this is the actual update step.
+            tempSymlink.renameTo(mCurrentDirSymlink.getAbsoluteFile());
+        } catch (IOException | RuntimeException e) {
+            deleteContentsAndDir(newLogsDir);
+            throw e;
+        }
+        Log.i(TAG, "CT log directory updated to " + newLogsDir.getAbsolutePath());
+        // 7. Cleanup
+        deleteOldLogDirectories();
+        return true;
+    }
+
+    private void makeDir(File dir) throws IOException {
+        dir.mkdir();
+        if (!dir.isDirectory()) {
+            throw new IOException("Unable to make directory " + dir.getCanonicalPath());
+        }
+        setWorldReadable(dir);
+    }
+
+    // CT files and directories are readable by all apps.
+    @SuppressLint("SetWorldReadable")
+    private void setWorldReadable(File file) throws IOException {
+        if (!file.setReadable(true, false)) {
+            throw new IOException("Failed to set " + file.getCanonicalPath() + " readable");
+        }
+    }
+
+    private void deleteOldLogDirectories() throws IOException {
+        if (!mCertificateTransparencyDir.exists()) {
+            return;
+        }
+        File currentTarget = mCurrentDirSymlink.getCanonicalFile();
+        for (File file : mCertificateTransparencyDir.listFiles()) {
+            if (!currentTarget.equals(file.getCanonicalFile())
+                    && file.getName().startsWith(LOGS_DIR_PREFIX)) {
+                deleteContentsAndDir(file);
+            }
+        }
+    }
+
+    static boolean deleteContentsAndDir(File dir) {
+        if (deleteContents(dir)) {
+            return dir.delete();
+        } else {
+            return false;
+        }
+    }
+
+    private static boolean deleteContents(File dir) {
+        File[] files = dir.listFiles();
+        boolean success = true;
+        if (files != null) {
+            for (File file : files) {
+                if (file.isDirectory()) {
+                    success &= deleteContents(file);
+                }
+                if (!file.delete()) {
+                    Log.w(TAG, "Failed to delete " + file);
+                    success = false;
+                }
+            }
+        }
+        return success;
+    }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
index 406a57f..52478c0 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -19,7 +19,6 @@
 import android.content.Context;
 import android.net.ct.ICertificateTransparencyManager;
 import android.os.Build;
-import android.util.Log;
 
 import com.android.net.ct.flags.Flags;
 import com.android.net.module.util.DeviceConfigUtils;
@@ -29,7 +28,6 @@
 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
 
-    private static final String TAG = "CertificateTransparency";
     private static final String CERTIFICATE_TRANSPARENCY_ENABLED =
             "certificate_transparency_service_enabled";
 
@@ -59,7 +57,6 @@
 
         switch (phase) {
             case SystemService.PHASE_BOOT_COMPLETED:
-                Log.d(TAG, "setting up flags listeners");
                 mFlagsListener.initialize();
                 break;
             default:
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
new file mode 100644
index 0000000..04b7dac
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -0,0 +1,45 @@
+/*
+ * 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.server.net.ct;
+
+import android.content.ApexEnvironment;
+
+import com.android.net.module.util.DeviceConfigUtils;
+
+import java.io.File;
+
+/** Class holding the constants used by the CT feature. */
+final class Config {
+
+    static final boolean DEBUG = false;
+
+    // preferences file
+    private static final File DEVICE_PROTECTED_DATA_DIR =
+            ApexEnvironment.getApexEnvironment(DeviceConfigUtils.TETHERING_MODULE_NAME)
+                    .getDeviceProtectedDataDir();
+    private static final String PREFERENCES_FILE_NAME = "ct.preferences";
+    static final File PREFERENCES_FILE = new File(DEVICE_PROTECTED_DATA_DIR, PREFERENCES_FILE_NAME);
+
+    // flags and properties names
+    static final String VERSION_PENDING = "version_pending";
+    static final String VERSION = "version";
+    static final String CONTENT_URL_PENDING = "content_url_pending";
+    static final String CONTENT_URL = "content_url";
+    static final String CONTENT_URL_KEY = "content_url_key";
+    static final String METADATA_URL_PENDING = "metadata_url_pending";
+    static final String METADATA_URL = "metadata_url";
+    static final String METADATA_URL_KEY = "metadata_url_key";
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/DataStore.java b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
new file mode 100644
index 0000000..cd6aebf
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
@@ -0,0 +1,67 @@
+/*
+ * 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.server.net.ct;
+
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Optional;
+import java.util.Properties;
+
+/** Class to persist data needed by CT. */
+class DataStore extends Properties {
+
+    private static final String TAG = "CertificateTransparency";
+
+    private final File mPropertyFile;
+
+    DataStore(File file) {
+        super();
+        mPropertyFile = file;
+    }
+
+    void load() {
+        if (!mPropertyFile.exists()) {
+            return;
+        }
+        try (InputStream in = new FileInputStream(mPropertyFile)) {
+            load(in);
+        } catch (IOException e) {
+            Log.e(TAG, "Error loading property store", e);
+        }
+    }
+
+    void store() {
+        try (OutputStream out = new FileOutputStream(mPropertyFile)) {
+            store(out, "");
+        } catch (IOException e) {
+            Log.e(TAG, "Error storing property store", e);
+        }
+    }
+
+    long getPropertyLong(String key, long defaultValue) {
+        return Optional.ofNullable(getProperty(key)).map(Long::parseLong).orElse(defaultValue);
+    }
+
+    Object setPropertyLong(String key, long value) {
+        return setProperty(key, Long.toString(value));
+    }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java b/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java
new file mode 100644
index 0000000..cc8c4c0
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java
@@ -0,0 +1,90 @@
+/*
+ * 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.server.net.ct;
+
+import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.app.DownloadManager.Request;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+import androidx.annotation.VisibleForTesting;
+
+/** Class to handle downloads for Certificate Transparency. */
+public class DownloadHelper {
+
+    private final DownloadManager mDownloadManager;
+
+    @VisibleForTesting
+    DownloadHelper(DownloadManager downloadManager) {
+        mDownloadManager = downloadManager;
+    }
+
+    DownloadHelper(Context context) {
+        this(context.getSystemService(DownloadManager.class));
+    }
+
+    /**
+     * Sends a request to start the download of a provided url.
+     *
+     * @param url the url to download
+     * @return a downloadId if the request was created successfully, -1 otherwise.
+     */
+    public long startDownload(String url) {
+        return mDownloadManager.enqueue(
+                new Request(Uri.parse(url))
+                        .setAllowedOverRoaming(false)
+                        .setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
+                        .setRequiresCharging(true));
+    }
+
+    /**
+     * Returns true if the specified download completed successfully.
+     *
+     * @param downloadId the download.
+     * @return true if the download completed successfully.
+     */
+    public boolean isSuccessful(long downloadId) {
+        try (Cursor cursor = mDownloadManager.query(new Query().setFilterById(downloadId))) {
+            if (cursor == null) {
+                return false;
+            }
+            if (cursor.moveToFirst()) {
+                int status =
+                        cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
+                if (DownloadManager.STATUS_SUCCESSFUL == status) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns the URI of the specified download, or null if the download did not complete
+     * successfully.
+     *
+     * @param downloadId the download.
+     * @return the {@link Uri} if the download completed successfully, null otherwise.
+     */
+    public Uri getUri(long downloadId) {
+        if (downloadId == -1) {
+            return null;
+        }
+        return mDownloadManager.getUriForDownloadedFile(downloadId);
+    }
+}
diff --git a/networksecurity/tests/unit/Android.bp b/networksecurity/tests/unit/Android.bp
new file mode 100644
index 0000000..639f644
--- /dev/null
+++ b/networksecurity/tests/unit/Android.bp
@@ -0,0 +1,44 @@
+// 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 {
+    default_team: "trendy_team_platform_security",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "NetworkSecurityUnitTests",
+    defaults: ["mts-target-sdk-version-current"],
+    test_suites: [
+        "general-tests",
+        "mts-tethering",
+    ],
+
+    srcs: ["src/**/*.java"],
+
+    libs: [
+        "android.test.base",
+        "android.test.mock",
+        "android.test.runner",
+    ],
+    static_libs: [
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "junit",
+        "mockito-target-minus-junit4",
+        "service-networksecurity-pre-jarjar",
+        "truth",
+    ],
+
+    sdk_version: "test_current",
+}
diff --git a/networksecurity/tests/unit/AndroidManifest.xml b/networksecurity/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..7a3f4b7
--- /dev/null
+++ b/networksecurity/tests/unit/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.net.ct">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.net.ct"
+        android:label="NetworkSecurity Mainline Module Tests" />
+</manifest>
diff --git a/networksecurity/tests/unit/AndroidTest.xml b/networksecurity/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..3c94df7
--- /dev/null
+++ b/networksecurity/tests/unit/AndroidTest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+<configuration description="Runs NetworkSecurity Mainline unit Tests.">
+    <option name="test-tag" value="NetworkSecurityUnitTests" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="test-file-name" value="NetworkSecurityUnitTests.apk" />
+    </target_preparer>
+
+    <option name="config-descriptor:metadata" key="mainline-param"
+            value="com.google.android.tethering.next.apex" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.net.ct" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </test>
+
+    <!-- Only run in MTS if the Tethering Mainline module is installed. -->
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.tethering" />
+    </object>
+</configuration>
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
new file mode 100644
index 0000000..5131a71
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.server.net.ct;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.DownloadManager;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.IOException;
+
+/** Tests for the {@link CertificateTransparencyDownloader}. */
+@RunWith(JUnit4.class)
+public class CertificateTransparencyDownloaderTest {
+
+    @Mock private DownloadHelper mDownloadHelper;
+    @Mock private CertificateTransparencyInstaller mCertificateTransparencyInstaller;
+
+    private Context mContext;
+    private File mTempFile;
+    private DataStore mDataStore;
+    private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
+
+    @Before
+    public void setUp() throws IOException {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mTempFile = File.createTempFile("datastore-test", ".properties");
+        mDataStore = new DataStore(mTempFile);
+        mDataStore.load();
+
+        mCertificateTransparencyDownloader =
+                new CertificateTransparencyDownloader(
+                        mContext, mDataStore, mDownloadHelper, mCertificateTransparencyInstaller);
+    }
+
+    @After
+    public void tearDown() {
+        mTempFile.delete();
+    }
+
+    @Test
+    public void testDownloader_startMetadataDownload() {
+        String metadataUrl = "http://test-metadata.org";
+        long downloadId = 666;
+        when(mDownloadHelper.startDownload(metadataUrl)).thenReturn(downloadId);
+
+        assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isFalse();
+        mCertificateTransparencyDownloader.startMetadataDownload(metadataUrl);
+        assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isTrue();
+    }
+
+    @Test
+    public void testDownloader_startContentDownload() {
+        String contentUrl = "http://test-content.org";
+        long downloadId = 666;
+        when(mDownloadHelper.startDownload(contentUrl)).thenReturn(downloadId);
+
+        assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isFalse();
+        mCertificateTransparencyDownloader.startContentDownload(contentUrl);
+        assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isTrue();
+    }
+
+    @Test
+    public void testDownloader_handleMetadataCompleteSuccessful() {
+        long metadataId = 123;
+        mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+        when(mDownloadHelper.isSuccessful(metadataId)).thenReturn(true);
+
+        long contentId = 666;
+        String contentUrl = "http://test-content.org";
+        mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUrl);
+        when(mDownloadHelper.startDownload(contentUrl)).thenReturn(contentId);
+
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeDownloadCompleteIntent(metadataId));
+
+        assertThat(mCertificateTransparencyDownloader.isContentDownloadId(contentId)).isTrue();
+    }
+
+    @Test
+    public void testDownloader_handleMetadataCompleteFailed() {
+        long metadataId = 123;
+        mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+        when(mDownloadHelper.isSuccessful(metadataId)).thenReturn(false);
+
+        String contentUrl = "http://test-content.org";
+        mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUrl);
+
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeDownloadCompleteIntent(metadataId));
+
+        verify(mDownloadHelper, never()).startDownload(contentUrl);
+    }
+
+    @Test
+    public void testDownloader_handleContentCompleteInstallSuccessful() throws IOException {
+        String version = "666";
+        mDataStore.setProperty(Config.VERSION_PENDING, version);
+
+        long metadataId = 123;
+        mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+        Uri metadataUri = Uri.fromFile(File.createTempFile("log_list-metadata", "txt"));
+        mDataStore.setProperty(Config.METADATA_URL_PENDING, metadataUri.toString());
+        when(mDownloadHelper.getUri(metadataId)).thenReturn(metadataUri);
+
+        long contentId = 666;
+        mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, contentId);
+        when(mDownloadHelper.isSuccessful(contentId)).thenReturn(true);
+        Uri contentUri = Uri.fromFile(File.createTempFile("log_list", "json"));
+        mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUri.toString());
+        when(mDownloadHelper.getUri(contentId)).thenReturn(contentUri);
+
+        when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(true);
+
+        assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
+        assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
+        assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
+
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeDownloadCompleteIntent(contentId));
+
+        verify(mCertificateTransparencyInstaller, times(1)).install(any(), eq(version));
+        assertThat(mDataStore.getProperty(Config.VERSION)).isEqualTo(version);
+        assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isEqualTo(contentUri.toString());
+        assertThat(mDataStore.getProperty(Config.METADATA_URL)).isEqualTo(metadataUri.toString());
+    }
+
+    @Test
+    public void testDownloader_handleContentCompleteInstallFails() throws IOException {
+        String version = "666";
+        mDataStore.setProperty(Config.VERSION_PENDING, version);
+
+        long metadataId = 123;
+        mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+        Uri metadataUri = Uri.fromFile(File.createTempFile("log_list-metadata", "txt"));
+        mDataStore.setProperty(Config.METADATA_URL_PENDING, metadataUri.toString());
+        when(mDownloadHelper.getUri(metadataId)).thenReturn(metadataUri);
+
+        long contentId = 666;
+        mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, contentId);
+        when(mDownloadHelper.isSuccessful(contentId)).thenReturn(true);
+        Uri contentUri = Uri.fromFile(File.createTempFile("log_list", "json"));
+        mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUri.toString());
+        when(mDownloadHelper.getUri(contentId)).thenReturn(contentUri);
+
+        when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(false);
+
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeDownloadCompleteIntent(contentId));
+
+        assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
+        assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
+        assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
+    }
+
+    private Intent makeDownloadCompleteIntent(long downloadId) {
+        return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
+                .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
+    }
+}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
new file mode 100644
index 0000000..bfb8bdf
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.server.net.ct;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.system.ErrnoException;
+import android.system.Os;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/** Tests for the {@link CertificateTransparencyInstaller}. */
+@RunWith(JUnit4.class)
+public class CertificateTransparencyInstallerTest {
+
+    private File mTestDir =
+            new File(
+                    InstrumentationRegistry.getInstrumentation().getContext().getFilesDir(),
+                    "test-dir");
+    private File mTestSymlink =
+            new File(mTestDir, CertificateTransparencyInstaller.CURRENT_DIR_SYMLINK_NAME);
+    private CertificateTransparencyInstaller mCertificateTransparencyInstaller =
+            new CertificateTransparencyInstaller(mTestDir);
+
+    @Before
+    public void setUp() {
+        CertificateTransparencyInstaller.deleteContentsAndDir(mTestDir);
+    }
+
+    @Test
+    public void testCertificateTransparencyInstaller_installSuccessfully() throws IOException {
+        String content = "i_am_a_certificate_and_i_am_transparent";
+        String version = "666";
+        boolean success = false;
+
+        try (InputStream inputStream = asStream(content)) {
+            success = mCertificateTransparencyInstaller.install(inputStream, version);
+        }
+
+        assertThat(success).isTrue();
+        assertThat(mTestDir.exists()).isTrue();
+        assertThat(mTestDir.isDirectory()).isTrue();
+        assertThat(mTestSymlink.exists()).isTrue();
+        assertThat(mTestSymlink.isDirectory()).isTrue();
+
+        File logsDir =
+                new File(mTestDir, CertificateTransparencyInstaller.LOGS_DIR_PREFIX + version);
+        assertThat(logsDir.exists()).isTrue();
+        assertThat(logsDir.isDirectory()).isTrue();
+        assertThat(mTestSymlink.getCanonicalPath()).isEqualTo(logsDir.getCanonicalPath());
+
+        File logsListFile = new File(logsDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME);
+        assertThat(logsListFile.exists()).isTrue();
+        assertThat(readAsString(logsListFile)).isEqualTo(content);
+    }
+
+    @Test
+    public void testCertificateTransparencyInstaller_versionIsAlreadyInstalled()
+            throws IOException, ErrnoException {
+        String existingVersion = "666";
+        String existingContent = "i_was_already_installed_successfully";
+        File existingLogDir =
+                new File(
+                        mTestDir,
+                        CertificateTransparencyInstaller.LOGS_DIR_PREFIX + existingVersion);
+        assertThat(mTestDir.mkdir()).isTrue();
+        assertThat(existingLogDir.mkdir()).isTrue();
+        Os.symlink(existingLogDir.getCanonicalPath(), mTestSymlink.getCanonicalPath());
+        File logsListFile =
+                new File(existingLogDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME);
+        logsListFile.createNewFile();
+        writeToFile(logsListFile, existingContent);
+        boolean success = false;
+
+        try (InputStream inputStream = asStream("i_will_be_ignored")) {
+            success = mCertificateTransparencyInstaller.install(inputStream, existingVersion);
+        }
+
+        assertThat(success).isFalse();
+        assertThat(readAsString(logsListFile)).isEqualTo(existingContent);
+    }
+
+    @Test
+    public void testCertificateTransparencyInstaller_versionInstalledFailed()
+            throws IOException, ErrnoException {
+        String existingVersion = "666";
+        String existingContent = "somebody_tried_to_install_me_but_failed_halfway_through";
+        String newContent = "i_am_the_real_certificate";
+        File existingLogDir =
+                new File(
+                        mTestDir,
+                        CertificateTransparencyInstaller.LOGS_DIR_PREFIX + existingVersion);
+        assertThat(mTestDir.mkdir()).isTrue();
+        assertThat(existingLogDir.mkdir()).isTrue();
+        File logsListFile =
+                new File(existingLogDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME);
+        logsListFile.createNewFile();
+        writeToFile(logsListFile, existingContent);
+        boolean success = false;
+
+        try (InputStream inputStream = asStream(newContent)) {
+            success = mCertificateTransparencyInstaller.install(inputStream, existingVersion);
+        }
+
+        assertThat(success).isTrue();
+        assertThat(mTestSymlink.getCanonicalPath()).isEqualTo(existingLogDir.getCanonicalPath());
+        assertThat(readAsString(logsListFile)).isEqualTo(newContent);
+    }
+
+    private static InputStream asStream(String string) throws IOException {
+        return new ByteArrayInputStream(string.getBytes());
+    }
+
+    private static String readAsString(File file) throws IOException {
+        return new String(new FileInputStream(file).readAllBytes());
+    }
+
+    private static void writeToFile(File file, String string) throws IOException {
+        try (OutputStream out = new FileOutputStream(file)) {
+            out.write(string.getBytes());
+        }
+    }
+}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java
new file mode 100644
index 0000000..3e670d4
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.server.net.ct;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.IOException;
+
+/** Tests for the {@link DataStore}. */
+@RunWith(JUnit4.class)
+public class DataStoreTest {
+
+    private File mTempFile;
+    private DataStore mDataStore;
+
+    @Before
+    public void setUp() throws IOException {
+        mTempFile = File.createTempFile("datastore-test", ".properties");
+        mDataStore = new DataStore(mTempFile);
+    }
+
+    @After
+    public void tearDown() {
+        mTempFile.delete();
+    }
+
+    @Test
+    public void testDataStore_propertyFileCreatedSuccessfully() {
+        assertThat(mTempFile.exists()).isTrue();
+        assertThat(mDataStore.isEmpty()).isTrue();
+    }
+
+    @Test
+    public void testDataStore_propertySet() {
+        String stringProperty = "prop1";
+        String stringValue = "i_am_a_string";
+        String longProperty = "prop3";
+        long longValue = 9000;
+
+        assertThat(mDataStore.getProperty(stringProperty)).isNull();
+        assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(-1);
+
+        mDataStore.setProperty(stringProperty, stringValue);
+        mDataStore.setPropertyLong(longProperty, longValue);
+
+        assertThat(mDataStore.getProperty(stringProperty)).isEqualTo(stringValue);
+        assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(longValue);
+    }
+
+    @Test
+    public void testDataStore_propertyStore() {
+        String stringProperty = "prop1";
+        String stringValue = "i_am_a_string";
+        String longProperty = "prop3";
+        long longValue = 9000;
+
+        mDataStore.setProperty(stringProperty, stringValue);
+        mDataStore.setPropertyLong(longProperty, longValue);
+        mDataStore.store();
+
+        mDataStore.clear();
+        assertThat(mDataStore.getProperty(stringProperty)).isNull();
+        assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(-1);
+
+        mDataStore.load();
+        assertThat(mDataStore.getProperty(stringProperty)).isEqualTo(stringValue);
+        assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(longValue);
+    }
+}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/DownloadHelperTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/DownloadHelperTest.java
new file mode 100644
index 0000000..0b65e3c
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/DownloadHelperTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.server.net.ct;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import android.app.DownloadManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for the {@link DownloadHelper}. */
+@RunWith(JUnit4.class)
+public class DownloadHelperTest {
+
+    @Mock private DownloadManager mDownloadManager;
+
+    private DownloadHelper mDownloadHelper;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mDownloadHelper = new DownloadHelper(mDownloadManager);
+    }
+
+    @Test
+    public void testDownloadHelper_scheduleDownload() {
+        long downloadId = 666;
+        when(mDownloadManager.enqueue(any())).thenReturn(downloadId);
+
+        assertThat(mDownloadHelper.startDownload("http://test.org")).isEqualTo(downloadId);
+    }
+
+    @Test
+    public void testDownloadHelper_wrongUri() {
+        when(mDownloadManager.enqueue(any())).thenReturn(666L);
+
+        assertThrows(
+                IllegalArgumentException.class, () -> mDownloadHelper.startDownload("not_a_uri"));
+    }
+}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index e00b7cf..32dbcaa 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -105,7 +105,6 @@
     },
     srcs: [
         "src/com/android/server/connectivity/mdns/**/*.java",
-        ":framework-connectivity-t-mdns-standalone-build-sources",
         ":service-mdns-droidstubs",
     ],
     exclude_srcs: [
diff --git a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
index 450f380..241d5fa 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
@@ -39,7 +39,7 @@
                                          uint32_t poll_ms) {
   // Always schedule another run of ourselves to recursively poll periodically.
   // The task runner is sequential so these can't run on top of each other.
-  runner->PostDelayedTask([=]() { PollAndSchedule(runner, poll_ms); }, poll_ms);
+  runner->PostDelayedTask([=, this]() { PollAndSchedule(runner, poll_ms); }, poll_ms);
 
   if (mMutex.try_lock()) {
     ConsumeAllLocked();
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
index 5c02767..cfeca5d 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
@@ -78,7 +78,7 @@
 
         final long timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
                 mLastScheduledQueryTaskArgs.config, now, minRemainingTtl, lastSentTime,
-                numOfQueriesBeforeBackoff);
+                numOfQueriesBeforeBackoff, false /* forceEnableBackoff */);
 
         if (timeToRun <= mLastScheduledQueryTaskArgs.timeToRun) {
             return null;
@@ -102,14 +102,16 @@
             long lastSentTime,
             long sessionId,
             int queryMode,
-            int numOfQueriesBeforeBackoff) {
+            int numOfQueriesBeforeBackoff,
+            boolean forceEnableBackoff) {
         final QueryTaskConfig nextRunConfig = currentConfig.getConfigForNextRun(queryMode);
-        final long timeToRun;
-        if (mLastScheduledQueryTaskArgs == null) {
+        long timeToRun;
+        if (mLastScheduledQueryTaskArgs == null && !forceEnableBackoff) {
             timeToRun = now + nextRunConfig.delayUntilNextTaskWithoutBackoffMs;
         } else {
             timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
-                    nextRunConfig, now, minRemainingTtl, lastSentTime, numOfQueriesBeforeBackoff);
+                    nextRunConfig, now, minRemainingTtl, lastSentTime, numOfQueriesBeforeBackoff,
+                    forceEnableBackoff);
         }
         mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(nextRunConfig, timeToRun,
                 minRemainingTtl + now,
@@ -128,11 +130,12 @@
         return mLastScheduledQueryTaskArgs;
     }
 
-    private static long calculateTimeToRun(@NonNull ScheduledQueryTaskArgs taskArgs,
+    private static long calculateTimeToRun(@Nullable ScheduledQueryTaskArgs taskArgs,
             QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime,
-            int numOfQueriesBeforeBackoff) {
+            int numOfQueriesBeforeBackoff, boolean forceEnableBackoff) {
         final long baseDelayInMs = queryTaskConfig.delayUntilNextTaskWithoutBackoffMs;
-        if (!queryTaskConfig.shouldUseQueryBackoff(numOfQueriesBeforeBackoff)) {
+        if (!(forceEnableBackoff
+                || queryTaskConfig.shouldUseQueryBackoff(numOfQueriesBeforeBackoff))) {
             return lastSentTime + baseDelayInMs;
         }
         if (minRemainingTtl <= 0) {
@@ -141,7 +144,7 @@
             return lastSentTime + baseDelayInMs;
         }
         // If the next TTL expiration time hasn't changed, then use previous calculated timeToRun.
-        if (lastSentTime < now
+        if (lastSentTime < now && taskArgs != null
                 && taskArgs.minTtlExpirationTimeWhenScheduled == now + minRemainingTtl) {
             // Use the original scheduling time if the TTL has not changed, to avoid continuously
             // rescheduling to 80% of the remaining TTL as time passes
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 8959c1b..4b55ea9 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -16,6 +16,7 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
 import static com.android.server.connectivity.mdns.MdnsServiceCache.ServiceExpiredCallback;
 import static com.android.server.connectivity.mdns.MdnsServiceCache.findMatchedResponse;
 import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
@@ -182,7 +183,8 @@
                                     lastSentTime,
                                     sentResult.taskArgs.sessionId,
                                     searchOptions.getQueryMode(),
-                                    searchOptions.numOfQueriesBeforeBackoff()
+                                    searchOptions.numOfQueriesBeforeBackoff(),
+                                    false /* forceEnableBackoff */
                             );
                     dependencies.sendMessageDelayed(
                             handler,
@@ -396,11 +398,13 @@
         }
         // Remove the next scheduled periodical task.
         removeScheduledTask();
-        mdnsQueryScheduler.cancelScheduledRun();
-        // Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
-        // interested anymore.
-        final QueryTaskConfig taskConfig = new QueryTaskConfig(
-                searchOptions.getQueryMode());
+        final boolean forceEnableBackoff =
+                (searchOptions.getQueryMode() == AGGRESSIVE_QUERY_MODE && hadReply);
+        // Keep the latest scheduled run for rescheduling if there is a service in the cache.
+        if (!(forceEnableBackoff)) {
+            mdnsQueryScheduler.cancelScheduledRun();
+        }
+        final QueryTaskConfig taskConfig = new QueryTaskConfig(searchOptions.getQueryMode());
         final long now = clock.elapsedRealtime();
         if (lastSentTime == 0) {
             lastSentTime = now;
@@ -415,7 +419,8 @@
                             lastSentTime,
                             currentSessionId,
                             searchOptions.getQueryMode(),
-                            searchOptions.numOfQueriesBeforeBackoff()
+                            searchOptions.numOfQueriesBeforeBackoff(),
+                            forceEnableBackoff
                     );
             dependencies.sendMessageDelayed(
                     handler,
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 11343d2..9b7af49 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -313,7 +313,7 @@
     static final int DEFAULT_TRAFFIC_STATS_CACHE_MAX_ENTRIES = 400;
     /**
      * The delay time between to network stats update intents.
-     * Added to fix intent spams (b/3115462)
+     * Added to fix intent spams (b/343844995)
      */
     @VisibleForTesting(visibility = PRIVATE)
     static final int BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS = 1000;
@@ -488,7 +488,6 @@
     @GuardedBy("mStatsLock")
     private long mLatestNetworkStatsUpdatedBroadcastScheduledTime = Long.MIN_VALUE;
 
-
     private final TrafficStatsRateLimitCache mTrafficStatsTotalCache;
     private final TrafficStatsRateLimitCache mTrafficStatsIfaceCache;
     private final TrafficStatsRateLimitCache mTrafficStatsUidCache;
@@ -499,7 +498,6 @@
     private final boolean mAlwaysUseTrafficStatsRateLimitCache;
     private final int mTrafficStatsRateLimitCacheExpiryDuration;
     private final int mTrafficStatsRateLimitCacheMaxEntries;
-
     private final boolean mBroadcastNetworkStatsUpdatedRateLimitEnabled;
 
 
@@ -724,15 +722,6 @@
     @VisibleForTesting
     public static class Dependencies {
         /**
-         * Get broadcast network stats updated delay time in ms
-         * @return
-         */
-        @NonNull
-        public long getBroadcastNetworkStatsUpdateDelayMs() {
-            return BROADCAST_NETWORK_STATS_UPDATED_DELAY_MS;
-        }
-
-        /**
          * Get legacy platform stats directory.
          */
         @NonNull
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 0fe24a2..9af250b 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -4439,9 +4439,8 @@
         pw.println();
         pw.println("Multicast routing supported: " +
                 (mMulticastRoutingCoordinatorService != null));
-
-        pw.println();
         pw.println("Background firewall chain enabled: " + mBackgroundFirewallChainEnabled);
+        pw.println("IngressToVpnAddressFiltering: " + mIngressToVpnAddressFiltering);
     }
 
     private void dumpNetworks(IndentingPrintWriter pw) {
@@ -6002,7 +6001,7 @@
             // TODO : The only way out of this is to diff old defaults and new defaults, and only
             // remove ranges for those requests that won't have a replacement
             final NetworkAgentInfo satisfier = nri.getSatisfier();
-            if (null != satisfier) {
+            if (null != satisfier && !satisfier.isDestroyed()) {
                 try {
                     mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig(
                             satisfier.network.getNetId(),
diff --git a/service/src/com/android/server/connectivity/DscpPolicyValue.java b/service/src/com/android/server/connectivity/DscpPolicyValue.java
index 7b11eda..7162a4a 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyValue.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyValue.java
@@ -55,13 +55,17 @@
     @Field(order = 7, type = Type.S8)
     public final byte dscp;
 
-    @Field(order = 8, type = Type.U8, padding = 3)
-    public final short mask;
+    @Field(order = 8, type = Type.Bool)
+    public final boolean match_src_ip;
 
-    private static final int SRC_IP_MASK = 0x1;
-    private static final int DST_IP_MASK = 0x02;
-    private static final int SRC_PORT_MASK = 0x4;
-    private static final int PROTO_MASK = 0x8;
+    @Field(order = 9, type = Type.Bool)
+    public final boolean match_dst_ip;
+
+    @Field(order = 10, type = Type.Bool)
+    public final boolean match_src_port;
+
+    @Field(order = 11, type = Type.Bool)
+    public final boolean match_proto;
 
     private boolean ipEmpty(final byte[] ip) {
         for (int i = 0; i < ip.length; i++) {
@@ -98,24 +102,6 @@
     private static final byte[] EMPTY_ADDRESS_FIELD =
             InetAddress.parseNumericAddress("::").getAddress();
 
-    private short makeMask(final byte[] src46, final byte[] dst46, final int srcPort,
-            final int dstPortStart, final short proto, final byte dscp) {
-        short mask = 0;
-        if (src46 != EMPTY_ADDRESS_FIELD) {
-            mask |= SRC_IP_MASK;
-        }
-        if (dst46 != EMPTY_ADDRESS_FIELD) {
-            mask |=  DST_IP_MASK;
-        }
-        if (srcPort != -1) {
-            mask |=  SRC_PORT_MASK;
-        }
-        if (proto != -1) {
-            mask |=  PROTO_MASK;
-        }
-        return mask;
-    }
-
     private DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int ifIndex,
             final int srcPort, final int dstPortStart, final int dstPortEnd, final short proto,
             final byte dscp) {
@@ -131,9 +117,10 @@
         this.proto = proto != -1 ? proto : 0;
 
         this.dscp = dscp;
-        // Use member variables for IP since byte[] is needed and api variables for everything else
-        // so -1 is passed into mask if parameter is not present.
-        this.mask = makeMask(this.src46, this.dst46, srcPort, dstPortStart, proto, dscp);
+        this.match_src_ip = (this.src46 != EMPTY_ADDRESS_FIELD);
+        this.match_dst_ip = (this.dst46 != EMPTY_ADDRESS_FIELD);
+        this.match_src_port = (srcPort != -1);
+        this.match_proto = (proto != -1);
     }
 
     public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int ifIndex,
diff --git a/staticlibs/device/com/android/net/module/util/Struct.java b/staticlibs/device/com/android/net/module/util/Struct.java
index ff7a711..69ca678 100644
--- a/staticlibs/device/com/android/net/module/util/Struct.java
+++ b/staticlibs/device/com/android/net/module/util/Struct.java
@@ -105,6 +105,7 @@
  */
 public class Struct {
     public enum Type {
+        Bool,        // bool,           size = 1 byte
         U8,          // unsigned byte,  size = 1 byte
         U16,         // unsigned short, size = 2 bytes
         U32,         // unsigned int,   size = 4 bytes
@@ -169,6 +170,9 @@
 
     private static void checkAnnotationType(final Field annotation, final Class fieldType) {
         switch (annotation.type()) {
+            case Bool:
+                if (fieldType == Boolean.TYPE) return;
+                break;
             case U8:
             case S16:
                 if (fieldType == Short.TYPE) return;
@@ -218,6 +222,7 @@
     private static int getFieldLength(final Field annotation) {
         int length = 0;
         switch (annotation.type()) {
+            case Bool:
             case U8:
             case S8:
                 length = 1;
@@ -357,6 +362,9 @@
         final Object value;
         checkAnnotationType(fieldInfo.annotation, fieldInfo.field.getType());
         switch (fieldInfo.annotation.type()) {
+            case Bool:
+                value = buf.get() != 0;
+                break;
             case U8:
                 value = (short) (buf.get() & 0xFF);
                 break;
@@ -457,6 +465,9 @@
     private static void putFieldValue(final ByteBuffer output, final FieldInfo fieldInfo,
             final Object value) throws BufferUnderflowException {
         switch (fieldInfo.annotation.type()) {
+            case Bool:
+                output.put((byte) (value != null && (boolean) value ? 1 : 0));
+                break;
             case U8:
                 output.put((byte) (((short) value) & 0xFF));
                 break;
@@ -748,6 +759,16 @@
         return sb.toString();
     }
 
+    /** A simple Struct which only contains a bool field. */
+    public static class Bool extends Struct {
+        @Struct.Field(order = 0, type = Struct.Type.Bool)
+        public final boolean val;
+
+        public Bool(final boolean val) {
+            this.val = val;
+        }
+    }
+
     /** A simple Struct which only contains a u8 field. */
     public static class U8 extends Struct {
         @Struct.Field(order = 0, type = Struct.Type.U8)
diff --git a/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
index f6bee69..28c33f3 100644
--- a/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
+++ b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
@@ -16,6 +16,8 @@
 
 package com.android.net.module.util;
 
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+
 import android.Manifest;
 import android.annotation.IntDef;
 import android.annotation.Nullable;
@@ -112,47 +114,9 @@
      *
      * @return {@link LocationPermissionCheckStatus} the result of the location permission check.
      */
-    public @LocationPermissionCheckStatus int checkLocationPermissionWithDetailInfo(
+    @VisibleForTesting(visibility = PRIVATE)
+    public @LocationPermissionCheckStatus int checkLocationPermissionInternal(
             String pkgName, @Nullable String featureId, int uid, @Nullable String message) {
-        final int result = checkLocationPermissionInternal(pkgName, featureId, uid, message);
-        switch (result) {
-            case ERROR_LOCATION_MODE_OFF:
-                Log.e(TAG, "Location mode is disabled for the device");
-                break;
-            case ERROR_LOCATION_PERMISSION_MISSING:
-                Log.e(TAG, "UID " + uid + " has no location permission");
-                break;
-        }
-        return result;
-    }
-
-    /**
-     * Enforce the caller has location permission.
-     *
-     * This API determines if the location mode enabled for the caller and the caller has
-     * ACCESS_COARSE_LOCATION permission is targetSDK<29, otherwise, has ACCESS_FINE_LOCATION.
-     * SecurityException is thrown if the caller has no permission or the location mode is disabled.
-     *
-     * @param pkgName package name of the application requesting access
-     * @param featureId The feature in the package
-     * @param uid The uid of the package
-     * @param message A message describing why the permission was checked. Only needed if this is
-     *                not inside of a two-way binder call from the data receiver
-     */
-    public void enforceLocationPermission(String pkgName, @Nullable String featureId, int uid,
-            @Nullable String message) throws SecurityException {
-        final int result = checkLocationPermissionInternal(pkgName, featureId, uid, message);
-
-        switch (result) {
-            case ERROR_LOCATION_MODE_OFF:
-                throw new SecurityException("Location mode is disabled for the device");
-            case ERROR_LOCATION_PERMISSION_MISSING:
-                throw new SecurityException("UID " + uid + " has no location permission");
-        }
-    }
-
-    private int checkLocationPermissionInternal(String pkgName, @Nullable String featureId,
-            int uid, @Nullable String message) {
         checkPackage(uid, pkgName);
 
         // Apps with NETWORK_SETTINGS, NETWORK_SETUP_WIZARD, NETWORK_STACK & MAINLINE_NETWORK_STACK
@@ -221,7 +185,7 @@
     /**
      * Retrieves a handle to LocationManager (if not already done) and check if location is enabled.
      */
-    public boolean isLocationModeEnabled() {
+    private boolean isLocationModeEnabled() {
         final LocationManager LocationManager = mContext.getSystemService(LocationManager.class);
         try {
             return LocationManager.isLocationEnabledForUser(UserHandle.of(
@@ -278,7 +242,7 @@
     /**
      * Returns true if the |uid| holds NETWORK_SETTINGS permission.
      */
-    public boolean checkNetworkSettingsPermission(int uid) {
+    private boolean checkNetworkSettingsPermission(int uid) {
         return getUidPermission(android.Manifest.permission.NETWORK_SETTINGS, uid)
                 == PackageManager.PERMISSION_GRANTED;
     }
@@ -286,7 +250,7 @@
     /**
      * Returns true if the |uid| holds NETWORK_SETUP_WIZARD permission.
      */
-    public boolean checkNetworkSetupWizardPermission(int uid) {
+    private boolean checkNetworkSetupWizardPermission(int uid) {
         return getUidPermission(android.Manifest.permission.NETWORK_SETUP_WIZARD, uid)
                 == PackageManager.PERMISSION_GRANTED;
     }
@@ -294,7 +258,7 @@
     /**
      * Returns true if the |uid| holds NETWORK_STACK permission.
      */
-    public boolean checkNetworkStackPermission(int uid) {
+    private boolean checkNetworkStackPermission(int uid) {
         return getUidPermission(android.Manifest.permission.NETWORK_STACK, uid)
                 == PackageManager.PERMISSION_GRANTED;
     }
@@ -302,7 +266,7 @@
     /**
      * Returns true if the |uid| holds MAINLINE_NETWORK_STACK permission.
      */
-    public boolean checkMainlineNetworkStackPermission(int uid) {
+    private boolean checkMainlineNetworkStackPermission(int uid) {
         return getUidPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, uid)
                 == PackageManager.PERMISSION_GRANTED;
     }
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java b/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
index 41a9428..28ff770 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStatsUtils.java
@@ -16,23 +16,12 @@
 
 package com.android.net.module.util;
 
-import android.app.usage.NetworkStats;
-
-import com.android.internal.annotations.VisibleForTesting;
-
 /**
  * Various utilities used for NetworkStats related code.
  *
  * @hide
  */
 public class NetworkStatsUtils {
-    // These constants must be synced with the definition in android.net.NetworkStats.
-    // TODO: update to formal APIs once all downstreams have these APIs.
-    private static final int SET_ALL = -1;
-    private static final int METERED_ALL = -1;
-    private static final int ROAMING_ALL = -1;
-    private static final int DEFAULT_NETWORK_ALL = -1;
-
     /**
      * Safely multiple a value by a rational.
      * <p>
@@ -99,77 +88,4 @@
         if (low > high) throw new IllegalArgumentException("low(" + low + ") > high(" + high + ")");
         return amount < low ? low : (amount > high ? high : amount);
     }
-
-    /**
-     * Convert structure from android.app.usage.NetworkStats to android.net.NetworkStats.
-     */
-    public static android.net.NetworkStats fromPublicNetworkStats(
-            NetworkStats publiceNetworkStats) {
-        android.net.NetworkStats stats = new android.net.NetworkStats(0L, 0);
-        while (publiceNetworkStats.hasNextBucket()) {
-            NetworkStats.Bucket bucket = new NetworkStats.Bucket();
-            publiceNetworkStats.getNextBucket(bucket);
-            final android.net.NetworkStats.Entry entry = fromBucket(bucket);
-            stats = stats.addEntry(entry);
-        }
-        return stats;
-    }
-
-    @VisibleForTesting
-    public static android.net.NetworkStats.Entry fromBucket(NetworkStats.Bucket bucket) {
-        return new android.net.NetworkStats.Entry(
-                null /* IFACE_ALL */, bucket.getUid(), convertBucketState(bucket.getState()),
-                convertBucketTag(bucket.getTag()), convertBucketMetered(bucket.getMetered()),
-                convertBucketRoaming(bucket.getRoaming()),
-                convertBucketDefaultNetworkStatus(bucket.getDefaultNetworkStatus()),
-                bucket.getRxBytes(), bucket.getRxPackets(),
-                bucket.getTxBytes(), bucket.getTxPackets(), 0 /* operations */);
-    }
-
-    private static int convertBucketState(int networkStatsSet) {
-        switch (networkStatsSet) {
-            case NetworkStats.Bucket.STATE_ALL: return SET_ALL;
-            case NetworkStats.Bucket.STATE_DEFAULT: return android.net.NetworkStats.SET_DEFAULT;
-            case NetworkStats.Bucket.STATE_FOREGROUND:
-                return android.net.NetworkStats.SET_FOREGROUND;
-        }
-        return 0;
-    }
-
-    private static int convertBucketTag(int tag) {
-        switch (tag) {
-            case NetworkStats.Bucket.TAG_NONE: return android.net.NetworkStats.TAG_NONE;
-        }
-        return tag;
-    }
-
-    private static int convertBucketMetered(int metered) {
-        switch (metered) {
-            case NetworkStats.Bucket.METERED_ALL: return METERED_ALL;
-            case NetworkStats.Bucket.METERED_NO: return android.net.NetworkStats.METERED_NO;
-            case NetworkStats.Bucket.METERED_YES: return android.net.NetworkStats.METERED_YES;
-        }
-        return 0;
-    }
-
-    private static int convertBucketRoaming(int roaming) {
-        switch (roaming) {
-            case NetworkStats.Bucket.ROAMING_ALL: return ROAMING_ALL;
-            case NetworkStats.Bucket.ROAMING_NO: return android.net.NetworkStats.ROAMING_NO;
-            case NetworkStats.Bucket.ROAMING_YES: return android.net.NetworkStats.ROAMING_YES;
-        }
-        return 0;
-    }
-
-    private static int convertBucketDefaultNetworkStatus(int defaultNetworkStatus) {
-        switch (defaultNetworkStatus) {
-            case NetworkStats.Bucket.DEFAULT_NETWORK_ALL:
-                return DEFAULT_NETWORK_ALL;
-            case NetworkStats.Bucket.DEFAULT_NETWORK_NO:
-                return android.net.NetworkStats.DEFAULT_NETWORK_NO;
-            case NetworkStats.Bucket.DEFAULT_NETWORK_YES:
-                return android.net.NetworkStats.DEFAULT_NETWORK_YES;
-        }
-        return 0;
-    }
 }
diff --git a/staticlibs/native/README.md b/staticlibs/native/README.md
index 1f505c4..7e0e963 100644
--- a/staticlibs/native/README.md
+++ b/staticlibs/native/README.md
@@ -27,4 +27,4 @@
   library (`.so`) file, and different versions of the library loaded in the same process by
   different modules will in general have different versions. It's important that each of these
   libraries loads the common function from its own library. Static linkage should guarantee this
-  because static linkage resolves symbols at build time, not runtime.
\ No newline at end of file
+  because static linkage resolves symbols at build time, not runtime.
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 91f94b5..61f41f7 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -70,6 +70,7 @@
     ],
     main: "host/python/run_tests.py",
     libs: [
+        "absl-py",
         "mobly",
         "net-tests-utils-host-python-common",
     ],
@@ -81,10 +82,4 @@
     test_options: {
         unit_test: false,
     },
-    // Needed for applying VirtualEnv.
-    version: {
-        py3: {
-            embedded_launcher: false,
-        },
-    },
 }
diff --git a/staticlibs/tests/unit/host/python/test_config.xml b/staticlibs/tests/unit/host/python/test_config.xml
index d3b200a..fed9d11 100644
--- a/staticlibs/tests/unit/host/python/test_config.xml
+++ b/staticlibs/tests/unit/host/python/test_config.xml
@@ -14,9 +14,6 @@
      limitations under the License.
 -->
 <configuration description="Config for NetworkStaticLibHostPythonTests">
-    <target_preparer class="com.android.tradefed.targetprep.PythonVirtualenvPreparer">
-        <option name="dep-module" value="absl-py" />
-    </target_preparer>
     <test class="com.android.tradefed.testtype.mobly.MoblyBinaryHostTest" >
         <option name="mobly-par-file-name" value="NetworkStaticLibHostPythonTests" />
         <option name="mobly-test-timeout" value="3m" />
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java
index 84018a5..c8f8656 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/LocationPermissionCheckerTest.java
@@ -25,10 +25,11 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 import android.Manifest;
 import android.app.AppOpsManager;
@@ -106,17 +107,18 @@
     }
 
     private void setupMocks() throws Exception {
-        when(mMockPkgMgr.getApplicationInfoAsUser(eq(TEST_PKG_NAME), eq(0), any()))
-                .thenReturn(mMockApplInfo);
-        when(mMockContext.getPackageManager()).thenReturn(mMockPkgMgr);
-        when(mMockAppOps.noteOp(AppOpsManager.OPSTR_WIFI_SCAN, mUid, TEST_PKG_NAME,
-                TEST_FEATURE_ID, null)).thenReturn(mWifiScanAllowApps);
-        when(mMockAppOps.noteOp(eq(AppOpsManager.OPSTR_COARSE_LOCATION), eq(mUid),
-                eq(TEST_PKG_NAME), eq(TEST_FEATURE_ID), nullable(String.class)))
-                .thenReturn(mAllowCoarseLocationApps);
-        when(mMockAppOps.noteOp(eq(AppOpsManager.OPSTR_FINE_LOCATION), eq(mUid),
-                eq(TEST_PKG_NAME), eq(TEST_FEATURE_ID), nullable(String.class)))
-                .thenReturn(mAllowFineLocationApps);
+        doReturn(mMockApplInfo).when(mMockPkgMgr)
+                .getApplicationInfoAsUser(eq(TEST_PKG_NAME), eq(0), any());
+        doReturn(mMockPkgMgr).when(mMockContext).getPackageManager();
+        doReturn(mWifiScanAllowApps).when(mMockAppOps).noteOp(
+                AppOpsManager.OPSTR_WIFI_SCAN, mUid, TEST_PKG_NAME,
+                TEST_FEATURE_ID, null);
+        doReturn(mAllowCoarseLocationApps).when(mMockAppOps).noteOp(
+                eq(AppOpsManager.OPSTR_COARSE_LOCATION), eq(mUid),
+                eq(TEST_PKG_NAME), eq(TEST_FEATURE_ID), nullable(String.class));
+        doReturn(mAllowFineLocationApps).when(mMockAppOps).noteOp(
+                eq(AppOpsManager.OPSTR_FINE_LOCATION), eq(mUid),
+                eq(TEST_PKG_NAME), eq(TEST_FEATURE_ID), nullable(String.class));
         if (mThrowSecurityException) {
             doThrow(new SecurityException("Package " + TEST_PKG_NAME + " doesn't belong"
                     + " to application bound to user " + mUid))
@@ -128,10 +130,10 @@
     }
 
     private <T> void mockSystemService(String name, Class<T> clazz, T service) {
-        when(mMockContext.getSystemService(name)).thenReturn(service);
-        when(mMockContext.getSystemServiceName(clazz)).thenReturn(name);
+        doReturn(service).when(mMockContext).getSystemService(name);
+        doReturn(name).when(mMockContext).getSystemServiceName(clazz);
         // Do not use mockito extended final method mocking
-        when(mMockContext.getSystemService(clazz)).thenCallRealMethod();
+        doCallRealMethod().when(mMockContext).getSystemService(clazz);
     }
 
     private void setupTestCase() throws Exception {
@@ -167,16 +169,17 @@
         Binder.restoreCallingIdentity((((long) mUid) << 32) | Binder.getCallingPid());
         doAnswer(mReturnPermission).when(mMockContext).checkPermission(
                 anyString(), anyInt(), anyInt());
-        when(mMockUserManager.isSameProfileGroup(UserHandle.SYSTEM,
-                UserHandle.getUserHandleForUid(MANAGED_PROFILE_UID)))
-                .thenReturn(true);
-        when(mMockContext.checkPermission(mManifestStringCoarse, -1, mUid))
-                .thenReturn(mCoarseLocationPermission);
-        when(mMockContext.checkPermission(mManifestStringFine, -1, mUid))
-                .thenReturn(mFineLocationPermission);
-        when(mMockContext.checkPermission(NETWORK_SETTINGS, -1, mUid))
-                .thenReturn(mNetworkSettingsPermission);
-        when(mLocationManager.isLocationEnabledForUser(any())).thenReturn(mIsLocationEnabled);
+        doReturn(true).when(mMockUserManager)
+                .isSameProfileGroup(UserHandle.SYSTEM,
+                UserHandle.getUserHandleForUid(MANAGED_PROFILE_UID));
+        doReturn(mCoarseLocationPermission).when(mMockContext)
+                .checkPermission(mManifestStringCoarse, -1, mUid);
+        doReturn(mFineLocationPermission).when(mMockContext)
+                .checkPermission(mManifestStringFine, -1, mUid);
+        doReturn(mNetworkSettingsPermission).when(mMockContext)
+                .checkPermission(NETWORK_SETTINGS, -1, mUid);
+        doReturn(mIsLocationEnabled).when(mLocationManager)
+                .isLocationEnabledForUser(any());
     }
 
     private Answer<Integer> createPermissionAnswer() {
@@ -208,7 +211,7 @@
         setupTestCase();
 
         final int result =
-                mChecker.checkLocationPermissionWithDetailInfo(
+                mChecker.checkLocationPermissionInternal(
                         TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
         assertEquals(LocationPermissionChecker.SUCCEEDED, result);
     }
@@ -225,7 +228,7 @@
         setupTestCase();
 
         final int result =
-                mChecker.checkLocationPermissionWithDetailInfo(
+                mChecker.checkLocationPermissionInternal(
                         TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
         assertEquals(LocationPermissionChecker.SUCCEEDED, result);
     }
@@ -240,7 +243,7 @@
         setupTestCase();
 
         assertThrows(SecurityException.class,
-                () -> mChecker.checkLocationPermissionWithDetailInfo(
+                () -> mChecker.checkLocationPermissionInternal(
                         TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null));
     }
 
@@ -251,7 +254,7 @@
         setupTestCase();
 
         final int result =
-                mChecker.checkLocationPermissionWithDetailInfo(
+                mChecker.checkLocationPermissionInternal(
                         TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
         assertEquals(LocationPermissionChecker.ERROR_LOCATION_PERMISSION_MISSING, result);
     }
@@ -267,7 +270,7 @@
         setupTestCase();
 
         final int result =
-                mChecker.checkLocationPermissionWithDetailInfo(
+                mChecker.checkLocationPermissionInternal(
                         TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
         assertEquals(LocationPermissionChecker.ERROR_LOCATION_PERMISSION_MISSING, result);
         verify(mMockAppOps, never()).noteOp(anyInt(), anyInt(), anyString());
@@ -284,7 +287,7 @@
         setupTestCase();
 
         final int result =
-                mChecker.checkLocationPermissionWithDetailInfo(
+                mChecker.checkLocationPermissionInternal(
                         TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
         assertEquals(LocationPermissionChecker.ERROR_LOCATION_MODE_OFF, result);
     }
@@ -298,7 +301,7 @@
         setupTestCase();
 
         final int result =
-                mChecker.checkLocationPermissionWithDetailInfo(
+                mChecker.checkLocationPermissionInternal(
                         TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
         assertEquals(LocationPermissionChecker.SUCCEEDED, result);
     }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
index baadad0..9981b6a 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/NetworkStatsUtilsTest.kt
@@ -16,16 +16,12 @@
 
 package com.android.net.module.util
 
-import android.net.NetworkStats
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
-import com.android.testutils.assertEntryEquals
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
@@ -75,48 +71,4 @@
         assertEquals(11, NetworkStatsUtils.constrain(11, 11, 11))
         assertEquals(11, NetworkStatsUtils.constrain(1, 11, 11))
     }
-
-    @Test
-    fun testBucketToEntry() {
-        val bucket = makeMockBucket(android.app.usage.NetworkStats.Bucket.UID_ALL,
-                android.app.usage.NetworkStats.Bucket.TAG_NONE,
-                android.app.usage.NetworkStats.Bucket.STATE_DEFAULT,
-                android.app.usage.NetworkStats.Bucket.METERED_YES,
-                android.app.usage.NetworkStats.Bucket.ROAMING_NO,
-                android.app.usage.NetworkStats.Bucket.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12)
-        val entry = NetworkStatsUtils.fromBucket(bucket)
-        val expectedEntry = NetworkStats.Entry(null /* IFACE_ALL */, NetworkStats.UID_ALL,
-            NetworkStats.SET_DEFAULT, NetworkStats.TAG_NONE, NetworkStats.METERED_YES,
-            NetworkStats.ROAMING_NO, NetworkStats.DEFAULT_NETWORK_ALL, 1024, 8, 2048, 12,
-            0 /* operations */)
-
-        assertEntryEquals(expectedEntry, entry)
-    }
-
-    private fun makeMockBucket(
-        uid: Int,
-        tag: Int,
-        state: Int,
-        metered: Int,
-        roaming: Int,
-        defaultNetwork: Int,
-        rxBytes: Long,
-        rxPackets: Long,
-        txBytes: Long,
-        txPackets: Long
-    ): android.app.usage.NetworkStats.Bucket {
-        val ret: android.app.usage.NetworkStats.Bucket =
-                mock(android.app.usage.NetworkStats.Bucket::class.java)
-        doReturn(uid).`when`(ret).getUid()
-        doReturn(tag).`when`(ret).getTag()
-        doReturn(state).`when`(ret).getState()
-        doReturn(metered).`when`(ret).getMetered()
-        doReturn(roaming).`when`(ret).getRoaming()
-        doReturn(defaultNetwork).`when`(ret).getDefaultNetworkStatus()
-        doReturn(rxBytes).`when`(ret).getRxBytes()
-        doReturn(rxPackets).`when`(ret).getRxPackets()
-        doReturn(txBytes).`when`(ret).getTxBytes()
-        doReturn(txPackets).`when`(ret).getTxPackets()
-        return ret
-    }
 }
\ No newline at end of file
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
index a39b7a3..0c2605f 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
@@ -32,6 +32,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.Struct.Bool;
 import com.android.net.module.util.Struct.Field;
 import com.android.net.module.util.Struct.Type;
 
@@ -133,6 +134,29 @@
         verifyHeaderParsing(msg);
     }
 
+    @Test
+    public void testBoolStruct() {
+        assertEquals(1, Struct.getSize(Bool.class));
+
+        assertEquals(false, Struct.parse(Bool.class, toByteBuffer("00")).val);
+        assertEquals(true,  Struct.parse(Bool.class, toByteBuffer("01")).val);
+        // maybe these should throw instead, but currently only 0 is false...
+        assertEquals(true,  Struct.parse(Bool.class, toByteBuffer("02")).val);
+        assertEquals(true,  Struct.parse(Bool.class, toByteBuffer("7F")).val);
+        assertEquals(true,  Struct.parse(Bool.class, toByteBuffer("80")).val);
+        assertEquals(true,  Struct.parse(Bool.class, toByteBuffer("FF")).val);
+
+        final var f = new Bool(false);
+        final var t = new Bool(true);
+        assertEquals(f.val, false);
+        assertEquals(t.val, true);
+
+        assertArrayEquals(toByteBuffer("00").array(), f.writeToBytes(ByteOrder.BIG_ENDIAN));
+        assertArrayEquals(toByteBuffer("00").array(), f.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+        assertArrayEquals(toByteBuffer("01").array(), t.writeToBytes(ByteOrder.BIG_ENDIAN));
+        assertArrayEquals(toByteBuffer("01").array(), t.writeToBytes(ByteOrder.LITTLE_ENDIAN));
+    }
+
     public static class HeaderMsgWithoutConstructor extends Struct {
         static int sType;
         static int sLength;
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
index a014834..d00ae52 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -61,8 +61,10 @@
     private val shouldThreadLeakFailTest = klass.isAnnotationPresent(MonitorThreadLeak::class.java)
     private val restoreDefaultNetworkDesc =
             Description.createTestDescription(klass, "RestoreDefaultNetwork")
-    private val restoreDefaultNetwork = klass.isAnnotationPresent(RestoreDefaultNetwork::class.java)
     val ctx = ApplicationProvider.getApplicationContext<Context>()
+    private val restoreDefaultNetwork =
+            klass.isAnnotationPresent(RestoreDefaultNetwork::class.java) &&
+            !ctx.applicationInfo.isInstantApp()
 
     // Inference correctly infers Runner & Filterable & Sortable for |baseRunner|, but the
     // Java bytecode doesn't have a way to express this. Give this type a name by wrapping it.
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
index f2b14f5..26bdb49 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NetworkStatsUtils.kt
@@ -16,7 +16,13 @@
 
 package com.android.testutils
 
+import android.app.usage.NetworkStatsManager
+import android.content.Context
+import android.net.INetworkStatsService
+import android.net.INetworkStatsSession
 import android.net.NetworkStats
+import android.net.NetworkTemplate
+import android.net.NetworkTemplate.MATCH_MOBILE
 import android.text.TextUtils
 import com.android.modules.utils.build.SdkLevel
 import kotlin.test.assertTrue
@@ -112,3 +118,32 @@
 fun assertParcelingIsLossless(stats: NetworkStats) {
     assertParcelingIsLossless(stats) { a, b -> orderInsensitiveEquals(a, b) }
 }
+
+/**
+ * Make a {@link android.app.usage.NetworkStats} instance from
+ * a {@link android.net.NetworkStats} instance.
+ */
+// It's not possible to directly create a mocked `NetworkStats` instance
+// because of limitations with `NetworkStats#getNextBucket`.
+// As a workaround for testing, create a mock by controlling the return values
+// from the mocked service that provides the `NetworkStats` data.
+// Notes:
+//   1. The order of records in the final `NetworkStats` object might change or
+//      some records might be merged if there are items with duplicate keys.
+//   2. The interface and operations fields will be empty since there is
+//      no such field in the {@link android.app.usage.NetworkStats}.
+fun makePublicStatsFromAndroidNetStats(androidNetStats: NetworkStats):
+        android.app.usage.NetworkStats {
+    val mockService = Mockito.mock(INetworkStatsService::class.java)
+    val manager = NetworkStatsManager(Mockito.mock(Context::class.java), mockService)
+    val mockStatsSession = Mockito.mock(INetworkStatsSession::class.java)
+
+    Mockito.doReturn(mockStatsSession).`when`(mockService)
+            .openSessionForUsageStats(anyInt(), any())
+    Mockito.doReturn(androidNetStats).`when`(mockStatsSession).getSummaryForAllUid(
+            any(NetworkTemplate::class.java), anyLong(), anyLong(), anyBoolean())
+    return manager.querySummary(
+            NetworkTemplate.Builder(MATCH_MOBILE).build(),
+            Long.MIN_VALUE, Long.MAX_VALUE
+    )
+}
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 919e025..798cf98 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -44,6 +44,7 @@
         "general-tests",
         "sts",
     ],
+    min_sdk_version: "31",
 }
 
 android_test_helper_app {
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 88c2d5a..b62db04 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -1055,7 +1055,7 @@
 
     @AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
     @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
-    public void testIsPrivateDnsBroken() throws InterruptedException {
+    public void testIsPrivateDnsBroken() throws Exception {
         final String invalidPrivateDnsServer = "invalidhostname.example.com";
         final String goodPrivateDnsServer = "dns.google";
         mCtsNetUtils.storePrivateDnsSetting();
@@ -1077,11 +1077,9 @@
                     .isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
         } finally {
             mCtsNetUtils.restorePrivateDnsSetting();
-            // Toggle network to make sure it is re-validated
-            mCm.reportNetworkConnectivity(networkForPrivateDns, true);
-            cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, NETWORK_CALLBACK_TIMEOUT_MS,
-                    entry -> !(((CallbackEntry.CapabilitiesChanged) entry).getCaps()
-                    .isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
+            // Toggle networks to make sure they are re-validated
+            mCtsNetUtils.reconnectWifiIfSupported();
+            mCtsNetUtils.reconnectCellIfSupported();
         }
     }
 
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index 8794847..1454d9a 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -22,6 +22,9 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_NCM;
 import static android.net.TetheringManager.TETHERING_USB;
 import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.net.TetheringManager.TETHERING_WIFI_P2P;
@@ -34,6 +37,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -60,7 +64,9 @@
 import android.net.cts.util.CtsTetheringUtils;
 import android.net.cts.util.CtsTetheringUtils.StartTetheringCallback;
 import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
+import android.net.wifi.SoftApConfiguration;
 import android.net.wifi.WifiManager;
+import android.net.wifi.WifiSsid;
 import android.os.Bundle;
 import android.os.PersistableBundle;
 import android.os.ResultReceiver;
@@ -71,6 +77,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.build.SdkLevel;
 import com.android.testutils.ParcelUtils;
 
 import org.junit.After;
@@ -78,6 +85,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -216,12 +224,26 @@
 
     @Test
     public void testTetheringRequest() {
-        final TetheringRequest tr = new TetheringRequest.Builder(TETHERING_WIFI).build();
+        SoftApConfiguration softApConfiguration;
+        if (SdkLevel.isAtLeastT()) {
+            softApConfiguration = new SoftApConfiguration.Builder()
+                    .setWifiSsid(WifiSsid.fromBytes(
+                            "This is an SSID!".getBytes(StandardCharsets.UTF_8)))
+                    .build();
+        } else {
+            softApConfiguration = new SoftApConfiguration.Builder()
+                    .setSsid("This is an SSID!")
+                    .build();
+        }
+        final TetheringRequest tr = new TetheringRequest.Builder(TETHERING_WIFI)
+                .setSoftApConfiguration(softApConfiguration)
+                .build();
         assertEquals(TETHERING_WIFI, tr.getTetheringType());
         assertNull(tr.getLocalIpv4Address());
         assertNull(tr.getClientStaticIpv4Address());
         assertFalse(tr.isExemptFromEntitlementCheck());
         assertTrue(tr.getShouldShowEntitlementUi());
+        assertEquals(softApConfiguration, tr.getSoftApConfiguration());
 
         final LinkAddress localAddr = new LinkAddress("192.168.24.5/24");
         final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24");
@@ -244,15 +266,57 @@
     }
 
     @Test
+    public void testTetheringRequestSetSoftApConfigurationFailsWhenNotWifi() {
+        final SoftApConfiguration softApConfiguration;
+        if (SdkLevel.isAtLeastT()) {
+            softApConfiguration = new SoftApConfiguration.Builder()
+                    .setWifiSsid(WifiSsid.fromBytes(
+                            "This is an SSID!".getBytes(StandardCharsets.UTF_8)))
+                    .build();
+        } else {
+            softApConfiguration = new SoftApConfiguration.Builder()
+                    .setSsid("This is an SSID!")
+                    .build();
+        }
+        for (int type : List.of(TETHERING_USB, TETHERING_BLUETOOTH, TETHERING_WIFI_P2P,
+                TETHERING_NCM, TETHERING_ETHERNET)) {
+            try {
+                new TetheringRequest.Builder(type).setSoftApConfiguration(softApConfiguration);
+                fail("Was able to set SoftApConfiguration for tethering type " + type);
+            } catch (IllegalArgumentException e) {
+                // Success
+            }
+        }
+    }
+
+    @Test
     public void testTetheringRequestParcelable() {
+        final SoftApConfiguration softApConfiguration;
+        if (SdkLevel.isAtLeastT()) {
+            softApConfiguration = new SoftApConfiguration.Builder()
+                    .setWifiSsid(WifiSsid.fromBytes(
+                            "This is an SSID!".getBytes(StandardCharsets.UTF_8)))
+                    .build();
+        } else {
+            softApConfiguration = new SoftApConfiguration.Builder()
+                    .setSsid("This is an SSID!")
+                    .build();
+        }
         final LinkAddress localAddr = new LinkAddress("192.168.24.5/24");
         final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24");
-        final TetheringRequest unparceled = new TetheringRequest.Builder(TETHERING_USB)
+        final TetheringRequest withConfig = new TetheringRequest.Builder(TETHERING_WIFI)
+                .setSoftApConfiguration(softApConfiguration)
                 .setStaticIpv4Addresses(localAddr, clientAddr)
                 .setExemptFromEntitlementCheck(true)
                 .setShouldShowEntitlementUi(false).build();
-        final TetheringRequest parceled = ParcelUtils.parcelingRoundTrip(unparceled);
-        assertEquals(unparceled, parceled);
+        final TetheringRequest withoutConfig = new TetheringRequest.Builder(TETHERING_WIFI)
+                .setStaticIpv4Addresses(localAddr, clientAddr)
+                .setExemptFromEntitlementCheck(true)
+                .setShouldShowEntitlementUi(false).build();
+        assertEquals(withConfig, ParcelUtils.parcelingRoundTrip(withConfig));
+        assertEquals(withoutConfig, ParcelUtils.parcelingRoundTrip(withoutConfig));
+        assertNotEquals(withConfig, ParcelUtils.parcelingRoundTrip(withoutConfig));
+        assertNotEquals(withoutConfig, ParcelUtils.parcelingRoundTrip(withConfig));
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index 569f4d7..da0bc88 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -2056,6 +2056,64 @@
         assertTrue(hasAnswer(knownAnswersQueryPacket, MdnsRecord.TYPE_PTR, subtypeLabels));
     }
 
+    @Test
+    public void sendQueries_AggressiveQueryMode_ServiceInCache() {
+        final int numOfQueriesBeforeBackoff = 11;
+        final MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+                .setQueryMode(AGGRESSIVE_QUERY_MODE)
+                .setNumOfQueriesBeforeBackoff(numOfQueriesBeforeBackoff)
+                .build();
+        startSendAndReceive(mockListenerOne, searchOptions);
+        verify(mockDeps, times(1)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+        int burstCounter = 0;
+        int betweenBurstTime = 0;
+        for (int i = 0; i < numOfQueriesBeforeBackoff; i += 3) {
+            verifyAndSendQuery(i, betweenBurstTime, /* expectsUnicastResponse= */ true);
+            verifyAndSendQuery(i + 1, /* timeInMs= */ 0, /* expectsUnicastResponse= */ false);
+            verifyAndSendQuery(i + 2, TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS,
+                    /* expectsUnicastResponse= */ false);
+            betweenBurstTime = Math.min(
+                    INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS * (int) Math.pow(2, burstCounter),
+                    MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS);
+            burstCounter++;
+        }
+        // In backoff mode, the current scheduled task will be canceled and reschedule if the
+        // 0.8 * smallestRemainingTtl is larger than time to next run.
+        long currentTime = TEST_TTL / 2 + TEST_ELAPSED_REALTIME;
+        doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+        doReturn(true).when(mockDeps).hasMessages(any(), eq(EVENT_START_QUERYTASK));
+        processResponse(createResponse(
+                "service-instance-1", "192.0.2.123", 5353,
+                SERVICE_TYPE_LABELS,
+                Collections.emptyMap(), TEST_TTL), socketKey);
+        verify(mockDeps, times(2)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+        assertNotNull(delayMessage);
+        assertEquals((long) (TEST_TTL / 2 * 0.8), latestDelayMs);
+
+        // Register another listener. There is a service in cache, the query time should be
+        // rescheduled with previous run.
+        currentTime += (long) ((TEST_TTL / 2 * 0.8) - 500L);
+        doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+        startSendAndReceive(mockListenerTwo, searchOptions);
+        verify(mockDeps, times(3)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+        assertNotNull(delayMessage);
+        assertEquals(500L, latestDelayMs);
+
+        // Stop all listeners
+        stopSendAndReceive(mockListenerOne);
+        stopSendAndReceive(mockListenerTwo);
+        verify(mockDeps, times(4)).removeMessages(any(), eq(EVENT_START_QUERYTASK));
+
+        // Register a new listener. There is a service in cache, the query time should be
+        // rescheduled with remaining ttl.
+        currentTime += 400L;
+        doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
+        startSendAndReceive(mockListenerOne, searchOptions);
+        assertNotNull(delayMessage);
+        assertEquals(9680L, latestDelayMs);
+    }
+
     private static MdnsServiceInfo matchServiceName(String name) {
         return argThat(info -> info.getServiceInstanceName().equals(name));
     }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
index 88c2738..5ca7fcc 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
@@ -54,6 +54,7 @@
 import org.mockito.ArgumentMatchers.any
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
 import kotlin.test.assertEquals
 import kotlin.test.assertTrue
 
@@ -162,6 +163,43 @@
         doTestSatelliteNeverBecomeDefaultNetwork(restricted = false)
     }
 
+    private fun doTestUnregisterAfterReplacementSatisfier(destroyed: Boolean) {
+        val satelliteAgent = createSatelliteAgent("satellite0")
+        satelliteAgent.connect()
+
+        val uids = setOf(TEST_PACKAGE_UID)
+        updateSatelliteNetworkFallbackUids(uids)
+
+        if (destroyed) {
+            satelliteAgent.unregisterAfterReplacement(timeoutMs = 5000)
+        }
+
+        updateSatelliteNetworkFallbackUids(setOf())
+        if (destroyed) {
+            // If the network is already destroyed, networkRemoveUidRangesParcel should not be
+            // called.
+            verify(netd, never()).networkRemoveUidRangesParcel(any())
+        } else {
+            verify(netd).networkRemoveUidRangesParcel(
+                    NativeUidRangeConfig(
+                            satelliteAgent.network.netId,
+                            toUidRangeStableParcels(uidRangesForUids(uids)),
+                            PREFERENCE_ORDER_SATELLITE_FALLBACK
+                    )
+            )
+        }
+    }
+
+    @Test
+    fun testUnregisterAfterReplacementSatisfier_destroyed() {
+        doTestUnregisterAfterReplacementSatisfier(destroyed = true)
+    }
+
+    @Test
+    fun testUnregisterAfterReplacementSatisfier_notDestroyed() {
+        doTestUnregisterAfterReplacementSatisfier(destroyed = false)
+    }
+
     private fun assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(uids: Set<Int>) {
         val nris: Set<ConnectivityService.NetworkRequestInfo> =
             service.createMultiLayerNrisFromSatelliteNetworkFallbackUids(uids)
diff --git a/thread/README.md b/thread/README.md
index 41b73ac..f2bd3b2 100644
--- a/thread/README.md
+++ b/thread/README.md
@@ -16,3 +16,7 @@
 
 Open `https://localhost:8443/` in your web browser, you can find the Thread
 demoapp (with the Thread logo) in the cuttlefish instance. Open it and have fun with Thread!
+
+## More docs
+
+- [Make your Android Border Router](./docs/make-your-android-border-router.md)
diff --git a/thread/docs/android-thread-arch.png b/thread/docs/android-thread-arch.png
new file mode 100644
index 0000000..ea408fa
--- /dev/null
+++ b/thread/docs/android-thread-arch.png
Binary files differ
diff --git a/thread/docs/build-an-android-border-router.md b/thread/docs/build-an-android-border-router.md
new file mode 100644
index 0000000..257999b
--- /dev/null
+++ b/thread/docs/build-an-android-border-router.md
@@ -0,0 +1,526 @@
+# Build an Android Border Router
+
+If you are not an Android device or Thread chip vendor, you can stop reading
+now.
+
+This document walks you through the steps to build a new Android-based Thread
+Border Router device with the latest AOSP source code. By following this
+document, you will learn:
+
+1. [the overall architecture and status of Thread support in Android](#architecture)
+2. [how to create your own Thread HAL service](#build-your-thread-hal-service)
+3. [how to make your device compatible with Google Home](#be-compatible-with-google-home)
+4. [how to test your Thread Border Router](#testing)
+
+If you need support, file an issue in
+[GitHub](https://github.com/openthread/ot-br-posix/issues) or open a
+[Dicussion](https://github.com/orgs/openthread/discussions) if you have any
+questions.
+
+Note: Before creating an issue or discussion, search to see if it has already
+been reported or asked.
+
+## Overview
+
+The Android Thread stack is based on OpenThread and `ot-br-posix` which are
+open-sourced by Google in GitHub. The same way OpenThread is developed in a
+public GitHub repository, so the Android Thread stack is developed in the
+public [AOSP codebase](https://cs.android.com/). All features and bug fixes
+are submitted first in AOSP. This allows vendors to start adopting the latest
+Thread versions without waiting for regular Android releases.
+
+### Architecture
+
+The whole Android Thread stack consists of two major components: the core
+Thread stack in a generic system partition and the Thread HAL service in a
+vendor partition. Device vendors typically need only to take care and build the
+HAL service.
+
+![android-thread-arch](./android-thread-arch.png)
+
+Here is a brief summary of how the Android Thread stack works:
+- There is a Java Thread system service in the system server which manages the
+  whole stack - provides the Thread system API, creates the `thread-wpan`
+  tunnel interface, registers the Thread network to the
+  [Connectivity service](https://developer.android.com/reference/android/content/Context#CONNECTIVITY_SERVICE)
+  and implements the Border Routing and Advertising Proxy functionalities.
+- The core Thread / OpenThread stack is hosted in a non-privileged standalone
+  native process which is named `ot-daemon`. `ot-daemon` is directly managed
+  by the Java system service via private AIDL APIs and it accesses the Thread
+  hardware radio through the Thread HAL API.
+- A vendor-provided Thread HAL service MUST implement the Thread HAL API. It
+  typically works as an
+  [RCP](https://openthread.io/platforms/co-processor#radio_co-processor_rcp)
+  and implements the
+  [spinel](https://openthread.io/platforms/co-processor#spinel_protocol)
+  protocol.
+
+Note: Both the Java system service and ot-daemon are delivered in a
+[Tethering](https://source.android.com/docs/core/ota/modular-system/tethering#overview)
+mainline module. For mobile devices, the binary is managed by Google and
+delivered to devices via Google Play monthly. For non-mobile devices such as
+TV, vendors are free to build from source or use a prebuilt.
+
+### Where is the code?
+
+- The Android Thread framework / API and service: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Connectivity/thread/
+- The Thread HAL API and default service implementation: https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/threadnetwork/
+- Imported OpenThread repo: https://cs.android.com/android/platform/superproject/main/+/main:external/openthread/
+- Imported ot-br-posix repo: https://cs.android.com/android/platform/superproject/main/+/main:external/ot-br-posix/
+
+## Set up development environment
+
+Android device vendors who have already established an Android development
+environment for the device can skip this section.
+
+If you are new to Android ecosystem or you are a silicon vendor who wants to
+make your Thread chip compatible with Android and provide support for device
+vendors, keep reading.
+
+### Follow the Android developer codelab
+
+To set up your Android development environment for the first time, use the
+following codelab: https://source.android.com/docs/setup/start. At the end of
+this codelab, you will be able to build and run a simulated Cuttlefish device
+from source code.
+
+## Build your Thread HAL service
+
+### Try Thread in Cuttlefish
+
+[Cuttlefish](https://source.android.com/docs/devices/Cuttlefish) is the virtual
+Android device. Before starting building your own HAL service, it's better to
+try Thread in Cuttlefish to understand how HAL works.
+
+A default Thread HAL service is provided in Cuttlefish and it's implemented
+with the [simulated RCP](https://github.com/openthread/openthread/tree/main/examples/platforms/simulation)
+which transceives packets via UDP socket to and from a simulated
+Thread (802.15.4) radio.
+
+In the Cuttlefish instance, a "ThreadNetworkDemoApp" is pre-installed. Open
+that app to join the Cuttlefish device into a default Thread network.
+
+![demoapp-screenshot](./demoapp-screenshot.png)
+
+Note: The Border Router functionalities will be started (and OMR address will
+be created) only when the Cuttlefish device is connected to a virtual Wi-Fi
+network. You can connect the Wi-Fi network in Settings.
+
+There are also the `ot-ctl` and `ot-cli-ftd` command line tools provided to
+configure your Thread network for testing. Those tools support all the
+OpenThread CLI commands that you may be familiar with already.
+
+You can grep for logs of the Cuttlefish Thread HAL service by:
+
+```
+$ adb logcat | egrep -i threadnetwork-service
+
+07-21 10:43:05.048     0     0 I init    : Parsing file /apex/com.android.hardware.threadnetwork/etc/threadnetwork-service.rc...
+07-21 10:59:27.233   580   580 W android.hardware.threadnetwork-service: ThreadChip binder is unlinked
+07-21 10:59:27.233   580   580 I android.hardware.threadnetwork-service: Close IThreadChip successfully
+07-21 10:59:27.385   580   580 I android.hardware.threadnetwork-service: Open IThreadChip successfully
+```
+
+Or grep for ot-daemon logs by:
+
+```
+$ adb logcat | egrep -i ot-daemon
+07-21 10:43:48.741     0     0 I init    : starting service 'ot-daemon'...
+07-21 10:43:48.742     0     0 I init    : Created socket '/dev/socket/ot-daemon/thread-wpan.sock', mode 660, user 1084, group 1084
+07-21 10:43:48.762     0     0 I init    : ... started service 'ot-daemon' has pid 2473
+07-21 10:46:26.320  2473  2473 I ot-daemon: [I] P-Daemon------: Session socket is ready
+07-21 10:46:30.290  2473  2473 W ot-daemon: [W] P-Daemon------: Daemon read: Connection reset by peer
+07-21 10:48:07.264  2473  2473 I ot-daemon: [INFO]-BINDER--: Start joining...
+07-21 10:48:07.267  2473  2473 I ot-daemon: [I] Settings------: Saved ActiveDataset
+07-21 10:48:07.267  2473  2473 I ot-daemon: [I] DatasetManager: Active dataset set
+07-21 10:48:07.273  2473  2473 I ot-daemon: [I] DnssdServer---: Started
+07-21 10:48:07.273  2473  2473 I ot-daemon: [N] Mle-----------: Role disabled -> detached
+07-21 10:48:07.273  2473  2473 I ot-daemon: [I] Mle-----------: AttachState Idle -> Start
+07-21 10:48:07.273  2473  2473 I ot-daemon: [I] Notifier------: StateChanged (0x111fd11d) [Ip6+ Role LLAddr MLAddr KeySeqCntr Ip6Mult+ Channel PanId NetName ExtPanId ...
+07-21 10:48:07.273  2473  2473 I ot-daemon: [I] Notifier------: StateChanged (0x111fd11d) ... NetworkKey PSKc SecPolicy NetifState ActDset]
+```
+
+Note: You can also capture Thread system server log with the tag
+"ThreadNetworkService".
+
+The Cuttlefish Thread HAL service uses the default Thread HAL service plus the
+OpenThread simulated RCP binary, see the next section for how it works.
+
+### The default HAL service
+
+A [default HAL service](https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/threadnetwork/aidl/default/)
+is included along with the Thread HAL API. The default HAL service supports
+both simulated and real RCP devices. It receives an optional RCP device URL and
+if the URL is not provided, it defaults to the simulated RCP device.
+
+In file `hardware/interfaces/threadnetwork/aidl/default/threadnetwork-service.rc`:
+
+```
+service vendor.threadnetwork_hal /apex/com.android.hardware.threadnetwork/bin/hw/android.hardware.threadnetwork-service
+    class hal
+    user thread_network
+```
+
+This is equivalent to:
+
+```
+service vendor.threadnetwork_hal /apex/com.android.hardware.threadnetwork/bin/hw/android.hardware.threadnetwork-service spinel+hdlc+forkpty:///apex/com.android.hardware.threadnetwork/bin/ot-rcp?forkpty-arg=1
+    class hal
+    user thread_network
+```
+
+For real RCP devices, it supports both SPI and UART interace and you can
+specify the device with the schema `spinel+spi://`, `spinel+hdlc+uart://` and
+`spinel+socket://` respectively.
+
+Note: `spinel+socket://` is a new spinel interface added in the default Thread
+HAL, it supports transmitting spinel frame via an Unix socket. A full
+socket-based radio URL may be like
+`spinel+socket:///data/vendor/threadnetwork/thread_spinel_socket`.
+
+#### Understand the vendor APEX
+
+Similar to the Thread stack in the Tethering mainline module, the default Thread
+HAL service in Cuttlefish is packaged in an APEX module as well. But it's a
+vendor APEX module which will be installed to `/vendor/apex/` (The artifacts in
+the module will be unzipped to `/apex/com.android.hardware.threadnetwork/`).
+
+```aidl
+apex {
+    name: "com.android.hardware.threadnetwork",
+    manifest: "manifest.json",
+    file_contexts: "file_contexts",
+    key: "com.android.hardware.key",
+    certificate: ":com.android.hardware.certificate",
+    updatable: false,
+    vendor: true,
+
+    binaries: [
+        "android.hardware.threadnetwork-service",
+        "ot-rcp",
+    ],
+
+    prebuilts: [
+        "threadnetwork-default.xml", // vintf_fragment
+        "threadnetwork-service.rc", // init_rc
+        "android.hardware.thread_network.prebuilt.xml", // permission
+    ],
+}
+```
+
+There are a few important configurations that you will need to pay attention or
+make changes to when building your own HAL APEX module:
+
+- `file_contexts`: This describes the binary / data files delivered in this
+  APEX module or files the HAL service need to access (for example, the RCP
+  device). This allows you to specify specific sepolicy rules for your HAL
+  service to access the hardware RCP device.
+- `binaries`: The binary file delivered in this APEX module
+- `threadnetwork-service.rc`: How the HAL service will be started. You need to
+  specify the RCP device path here.
+- `android.hardware.thread_network.prebuilt.xml`: Defines the
+  `android.hardware.thread_network` hardware feature. This is required for the
+  Android system to know that your device does have Thread hardware support.
+  Otherwise, the Android Thread stack won't be enabled.
+
+### Create your HAL service
+
+Whether you are an Android device developer or a silicon vendor, you should be
+familiar with building OT RCP firmware for your Thread chip. The following
+instructions assume that the hardware chip is correctly wired and
+validated.
+
+The simplest way to build your HAL APEX is to create a new APEX with the
+binaries and prebuilts of the default HAL APEX. For example, if your company is
+Banana and the RCP device on your device is `/dev/ttyACM0`, your Thread HAL
+APEX will look like this:
+
+- `Android.bp`:
+  ```
+  prebuilt_etc {
+    name: "banana-threadnetwork-service.rc",
+    src: "banana-threadnetwork-service.rc",
+    installable: false,
+  }
+
+  apex {
+    name: "com.banana.android.hardware.threadnetwork",
+    manifest: "manifest.json",
+    file_contexts: "file_contexts",
+    key: "com.android.hardware.key",
+    certificate: ":com.android.hardware.certificate",
+    updatable: false,
+    vendor: true,
+
+    binaries: [
+        "android.hardware.threadnetwork-service",
+    ],
+
+    prebuilts: [
+        "banana-threadnetwork-service.rc",
+        "threadnetwork-default.xml",
+        "android.hardware.thread_network.prebuilt.xml",
+    ],
+  }
+  ```
+- `file_contexts`:
+  ```
+  (/.*)?                                                      u:object_r:vendor_file:s0
+  /etc(/.*)?                                                  u:object_r:vendor_configs_file:s0
+  /bin/hw/android\.hardware\.threadnetwork-service            u:object_r:hal_threadnetwork_default_exec:s0
+  /dev/ttyACM0                                                u:object_r:threadnetwork_rcp_device:s0
+  ```
+  The file paths in the first column are related to `/apex/com.android.hardware.threadnetwork/`.
+- `threadnetwork-service.rc`:
+  ```
+  service vendor.threadnetwork_hal /apex/com.android.hardware.threadnetwork/bin/hw/android.hardware.threadnetwork-service spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=115200
+    class hal
+    user root
+  ```
+- `manifest.json`:
+  ```
+  {
+    "name": "com.android.hardware.threadnetwork",
+    "version": 1
+  }
+  ```
+
+Note: The default Thread HAL service is not designed to be a generic system
+component which works for all Android devices. If the default implementation
+can't support your device, you are free to make a copy and change it for your
+needs. In this case, it's just simpler to create a new APEX module without
+overriding the default one.
+
+Assuming you are making a new device named Orange, your device specific
+configuration directory will be like:
+
+```
+device/banana/orange/threadnetwork/
+    sepolicy/
+    Android.bp
+    file_contexts
+    manifest.json
+    threadnetwork-default.xml
+    threadnetwork-service.rc
+```
+
+See the next section for what sepolicy rules should be added in the `sepolicy/`
+sub-directory.
+
+#### Sepolicy rules for RCP device
+
+By default, your Thread HAL service doesn't have access to the RCP device (for
+example `/dev/ttyACM0`), custom sepolicy rules need to be added to the
+`sepolicy/` directory.
+
+Create a new `sepolicy/threadnetwork_hal.te` file with below content:
+
+```
+type threadnetwork_rcp_device, dev_type;
+
+# Allows the Thread HAL service to read / write the Thread RCP device
+allow hal_threadnetwork_default threadnetwork_rcp_device:chr_file rw_file_perms;
+```
+
+#### Put together
+
+Now you have finished almost all the code needs for adding Thread, the last
+step is to add the Thread HAL APEX and sepolicy rules to your device's image.
+
+You can do this by adding below code to your device's `Makefile` (for example,
+`device.mk`):
+
+```
+PRODUCT_PACKAGES += com.banana.hardware.threadnetwork
+BOARD_SEPOLICY_DIRS += device/banana/orange/threadnetwork/sepolicy
+```
+
+Note: Unfortunately, APEX module doesn't support sepolicy rules, so you need
+to explicitly specify the sepolicy directory separately.
+
+If everything works, now you will be able to see the Thread HAL service log similar to:
+
+```
+$ adb logcat | egrep -i threadnetwork-service
+08-13 13:26:41.751   477   477 I android.hardware.threadnetwork-service: ServiceName: android.hardware.threadnetwork.IThreadChip/chip0, Url: spinel+spi
+08-13 13:26:41.751   477   477 I android.hardware.threadnetwork-service: Thread Network HAL is running
+08-13 13:26:55.165   477   477 I android.hardware.threadnetwork-service: Open IThreadChip successfully
+```
+
+And the `ot-daemon` log will be like:
+
+```
+$ adb logcat -s ot-daemon
+08-13 13:26:55.157  1019  1019 I ot-daemon: [NOTE]-AGENT---: Running OTBR_AGENT/Unknown
+08-13 13:26:55.157  1019  1019 I ot-daemon: [NOTE]-AGENT---: Thread version: 1.3.0
+08-13 13:26:55.157  1019  1019 I ot-daemon: [NOTE]-AGENT---: Thread interface: thread-wpan
+08-13 13:26:55.157  1019  1019 I ot-daemon: [NOTE]-AGENT---: Backbone interface is not specified
+08-13 13:26:55.157  1019  1019 I ot-daemon: [NOTE]-AGENT---: Radio URL: threadnetwork_hal://binder?none
+08-13 13:26:55.157  1019  1019 I ot-daemon: [NOTE]-ILS-----: Infra link selected:
+08-13 13:26:55.160  1019  1019 I ot-daemon: [I] Platform------: [HAL] Wait for getting the service android.hardware.threadnetwork.IThreadChip/chip0 ...
+08-13 13:26:55.165  1019  1019 I ot-daemon: [I] Platform------: [HAL] Successfully got the service android.hardware.threadnetwork.IThreadChip/chip0
+08-13 13:26:55.275  1019  1019 I ot-daemon: [I] P-RadioSpinel-: RCP reset: RESET_UNKNOWN
+08-13 13:26:55.276  1019  1019 I ot-daemon: [I] P-RadioSpinel-: Software reset RCP successfully
+08-13 13:26:55.277  1019  1019 I ot-daemon: [I] P-RadioSpinel-: RCP reset: RESET_POWER_ON
+08-13 13:26:55.322  1019  1019 I ot-daemon: [I] ChildSupervsn-: Timeout: 0 -> 190
+08-13 13:26:55.324  1019  1019 I ot-daemon: [I] RoutingManager: Initializing - InfraIfIndex:0
+08-13 13:26:55.324  1019  1019 I ot-daemon: [I] InfraIf-------: Init infra netif 0
+08-13 13:26:55.324  1019  1019 I ot-daemon: [I] Settings------: Read BrUlaPrefix fd7b:cc45:ff06::/48
+08-13 13:26:55.324  1019  1019 I ot-daemon: [N] RoutingManager: BR ULA prefix: fd7b:cc45:ff06::/48 (loaded)
+08-13 13:26:55.324  1019  1019 I ot-daemon: [I] RoutingManager: Generated local OMR prefix: fd7b:cc45:ff06:1::/64
+08-13 13:26:55.324  1019  1019 I ot-daemon: [N] RoutingManager: Local on-link prefix: fdde:ad00:beef:cafe::/64
+08-13 13:26:55.324  1019  1019 I ot-daemon: [I] RoutingManager: Enabling
+```
+
+## Customization
+
+The Thread mainline module (it's actually a part of the "Tethering" module)
+provides a few [overlayable configurations](https://source.android.com/docs/core/runtime/rros)
+which can be specified by vendors to customize the stack behavior. See
+[config_thread.xml](https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Connectivity/service/ServiceConnectivityResources/res/values/config_thread.xml)
+for the full list.
+
+Typically, you must change the `config_thread_vendor_name`,
+`config_thread_vendor_oui` and `config_thread_model_name` to your vendor or
+product values. Those values will be included in the `_meshcop._udp` mDNS
+service which is always advertised by a Thread Border Router.
+
+To add the overlay, you need to create a new `ConnectivityOverlayOrange`
+runtime_resource_overlay target for your Orange device. Create a new
+`ConnectivityOverlay/` directory under `device/banana/orange/rro_overlays` and
+create below contents in it:
+
+```
+device/banana/orange/rro_overlays/ConnectivityOverlay/
+  res
+    values
+      config_thread.xml
+  Android.bp
+  AndroidManifest.xml
+```
+
+- `Android.bp`:
+  ```
+  package {
+      default_applicable_licenses: ["Android-Apache-2.0"],
+  }
+
+  runtime_resource_overlay {
+      name: "ConnectivityOverlayOrange",
+      manifest: "AndroidManifest.xml",
+      resource_dirs: ["res"],
+      certificate: "platform",
+      product_specific: true,
+      sdk_version: "current",
+  }
+  ```
+- `AndroidManifest.xml`:
+  ```
+  <!-- Orange overlays for the Connectivity module -->
+  <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.banana.android.connectivity.resources.orange"
+      android:versionCode="1"
+      android:versionName="1.0">
+      <application android:hasCode="false" />
+
+      <!-- If your device uses google-signed mainline modules, the targetPackage
+      needs to be "com.google.android.connectivity.resources", otherise, it
+      should be "com.android.connectivity.resources"
+      -->
+      <overlay
+          android:targetPackage="com.google.android.connectivity.resources"
+          android:targetName="ServiceConnectivityResourcesConfig"
+          android:isStatic="true"
+          android:priority="1"/>
+  </manifest>
+  ```
+- `config_thread.xml`:
+  ```
+  <string translatable="false" name="config_thread_vendor_name">Banana Inc.</string>
+  <string translatable="false" name="config_thread_vendor_oui">AC:DE:48</string>
+  <string translatable="false" name="config_thread_model_name">Orange</string>
+  ```
+
+Similar to the HAL APEX, you need to add the overlay app to your `device.mk`
+file:
+
+```
+PRODUCT_PACKAGES += \
+    ConnectivityOverlayOrange
+```
+
+If everything works, you will see that `ot-daemon` logs the vendor and model name
+at the very beginning of the log:
+```
+$ adb logcat -s ot-daemon
+07-22 15:31:37.693  1472  1472 I ot-daemon: [I] P-Daemon------: Session socket is ready
+07-22 15:31:37.693  1472  1472 I ot-daemon: [I] Cli-----------: Input: state
+07-22 15:31:37.693  1472  1472 I ot-daemon: [I] Cli-----------: Output: disabled
+07-22 15:31:37.693  1472  1472 I ot-daemon: [I] Cli-----------: Output: Done
+07-22 15:31:37.693  1472  1472 W ot-daemon: [W] P-Daemon------: Daemon read: Connection reset by peer
+07-22 15:31:50.091  1472  1472 I ot-daemon: [I] P-Daemon------: Session socket is ready
+07-22 15:31:50.091  1472  1472 I ot-daemon: [I] Cli-----------: Input: factoryreset
+07-22 15:31:50.092  1472  1472 I ot-daemon: [I] Settings------: Wiped all info
+07-22 15:31:50.092  1472  1472 I ot-daemon: [INFO]-ADPROXY-: Stopped
+07-22 15:31:50.092  1472  1472 I ot-daemon: [INFO]-DPROXY--: Stopped
+07-22 15:31:50.092  1472  1472 I ot-daemon: [INFO]-BA------: Stop Thread Border Agent
+07-22 15:31:50.092  1472  1472 I ot-daemon: [INFO]-BA------: Unpublish meshcop service Banana Inc. Orange #4833._meshcop._udp.local
+07-22 15:31:50.092  1472  1472 I ot-daemon: [INFO]-MDNS----: Removing service Banana Inc. Orange #4833._meshcop._udp
+07-22 15:31:50.092  1472  1472 I ot-daemon: [INFO]-MDNS----: Unpublishing service Banana Inc. Orange #4833._meshcop._udp listener ID = 0
+```
+
+Note: In case the overlay doesn't work, check https://source.android.com/docs/core/runtime/rro-troubleshoot
+for troubleshooting instructions.
+
+### Be compatible with Google Home
+
+Additionally, if you want to make your Border Router be used by the Google Home
+ecosystem, you can specify this configuration in `config_thread.xml`:
+
+```
+<string-array name="config_thread_mdns_vendor_specific_txts">
+  <item>vgh=1</item>
+</string-array>
+```
+
+## Testing
+
+Your device should be compatible with the Thread 1.3+ Border Router
+specification now. Before sending it to the Thread certification program, there
+are a few Android xTS tests should be exercised to ensure the compatibility.
+
+- The VTS test makes sure Thread HAL service work as expected on your device.
+  You can run the tests with command
+  ```
+  atest VtsHalThreadNetworkTargetTest
+  ```
+- The CTS test makes sure Thread APIs work as expected on your device. You can
+  run the tests with command
+  ```
+  atest CtsThreadNetworkTestCases
+  ```
+- The integration test provides more quality guarantee of how the Thread
+  mainline code works on your device. You can run the tests with command
+  ```
+  atest ThreadNetworkIntegrationTests
+  ```
+
+You can also find more instructions of how to run VTS/CTS/MTS tests with those
+released test suites:
+
+- https://source.android.com/docs/core/tests/vts
+- https://source.android.com/docs/compatibility/cts/run
+- https://docs.partner.android.com/mainline/test/mts (you need to be a partner to access this link)
+
+### Test with the Thread demo app
+
+Similar to the Cuttlefish device, you can add the Thread demo app to your system image:
+
+```
+# ThreadNetworkDemoApp for testing
+PRODUCT_PACKAGES_DEBUG += ThreadNetworkDemoApp
+```
+
+Note that you should add it to only the debug / eng variant (for example,
+`PRODUCT_PACKAGES_DEBUG`) given this is not supposed to be included in user
+build for end consumers.
diff --git a/thread/docs/demoapp-screenshot.png b/thread/docs/demoapp-screenshot.png
new file mode 100644
index 0000000..fa7f079
--- /dev/null
+++ b/thread/docs/demoapp-screenshot.png
Binary files differ
diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
index 22457f5..1b50ba7 100644
--- a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
+++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
@@ -35,6 +35,7 @@
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.thread.flags.Flags;
 
 import java.io.ByteArrayOutputStream;
 import java.net.Inet6Address;
@@ -69,7 +70,7 @@
  *
  * @hide
  */
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
 @SystemApi
 public final class ActiveOperationalDataset implements Parcelable {
     /** The maximum length of the Active Operational Dataset TLV array in bytes. */
diff --git a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
index cecb4e9..489f941 100644
--- a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
+++ b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
@@ -26,6 +26,8 @@
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 
+import com.android.net.thread.flags.Flags;
+
 import java.nio.ByteBuffer;
 import java.time.Instant;
 import java.util.Objects;
@@ -37,7 +39,7 @@
  * @see PendingOperationalDataset
  * @hide
  */
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
 @SystemApi
 public final class OperationalDatasetTimestamp {
     /** @hide */
diff --git a/thread/framework/java/android/net/thread/PendingOperationalDataset.java b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
index c1351af..235e563 100644
--- a/thread/framework/java/android/net/thread/PendingOperationalDataset.java
+++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
@@ -27,6 +27,8 @@
 import android.os.Parcelable;
 import android.util.SparseArray;
 
+import com.android.net.thread.flags.Flags;
+
 import java.io.ByteArrayOutputStream;
 import java.nio.ByteBuffer;
 import java.time.Duration;
@@ -42,7 +44,7 @@
  * @see ThreadNetworkController#scheduleMigration
  * @hide
  */
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
 @SystemApi
 public final class PendingOperationalDataset implements Parcelable {
     // Value defined in Thread spec 8.10.1.16
diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.java b/thread/framework/java/android/net/thread/ThreadConfiguration.java
index be2632c..1c25535 100644
--- a/thread/framework/java/android/net/thread/ThreadConfiguration.java
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.java
@@ -21,6 +21,8 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import com.android.net.thread.flags.Flags;
+
 import java.util.Objects;
 
 /**
@@ -39,7 +41,7 @@
  * @see ThreadNetworkController#unregisterConfigurationCallback
  * @hide
  */
-@FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+@FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED)
 @SystemApi
 public final class ThreadConfiguration implements Parcelable {
     private final boolean mNat64Enabled;
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index b4e581c..ecaefd0 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -26,6 +26,7 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.Size;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.os.Binder;
 import android.os.OutcomeReceiver;
@@ -34,6 +35,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.thread.flags.Flags;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -51,7 +53,7 @@
  *
  * @hide
  */
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
 @SystemApi
 public final class ThreadNetworkController {
     private static final String TAG = "ThreadNetworkController";
@@ -101,11 +103,12 @@
     /** Thread standard version 1.3. */
     public static final int THREAD_VERSION_1_3 = 4;
 
-    /** Minimum value of max power in unit of 0.01dBm. @hide */
-    private static final int POWER_LIMITATION_MIN = -32768;
-
-    /** Maximum value of max power in unit of 0.01dBm. @hide */
-    private static final int POWER_LIMITATION_MAX = 32767;
+    /** The value of max power to disable the Thread channel. */
+    // This constant can never change. It has "max" in the name not because it indicates
+    // maximum power, but because it's passed to an API that sets the maximum power to
+    // disabled the Thread channel.
+    @SuppressLint("MinMaxConstant")
+    public static final int MAX_POWER_CHANNEL_DISABLED = Integer.MIN_VALUE;
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -626,7 +629,7 @@
      * @param callback the callback to receive Thread configuration changes
      * @throws IllegalArgumentException if {@code callback} has already been registered
      */
-    @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+    @FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED)
     @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
     public void registerConfigurationCallback(
             @NonNull @CallbackExecutor Executor executor,
@@ -656,7 +659,7 @@
      *     #registerConfigurationCallback}
      * @throws IllegalArgumentException if {@code callback} hasn't been registered
      */
-    @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+    @FlaggedApi(Flags.FLAG_CONFIGURATION_ENABLED)
     @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
     public void unregisterConfigurationCallback(@NonNull Consumer<ThreadConfiguration> callback) {
         requireNonNull(callback, "callback cannot be null");
@@ -703,6 +706,10 @@
     /**
      * Sets max power of each channel.
      *
+     * <p>This method sets the max power for the given channel. The platform sets the actual
+     * output power to be less than or equal to the {@code channelMaxPowers} and as close as
+     * possible to the {@code channelMaxPowers}.
+     *
      * <p>If not set, the default max power is set by the Thread HAL service or the Thread radio
      * chip firmware.
      *
@@ -711,22 +718,27 @@
      * OutcomeReceiver#onError} will be called with a specific error:
      *
      * <ul>
-     *   <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_OPERATION} the operation is no
-     *       supported by the platform.
+     *   <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_FEATURE} the feature is not supported
+     *       by the platform.
      * </ul>
      *
      * @param channelMaxPowers SparseIntArray (key: channel, value: max power) consists of channel
      *     and corresponding max power. Valid channel values should be between {@link
      *     ActiveOperationalDataset#CHANNEL_MIN_24_GHZ} and {@link
-     *     ActiveOperationalDataset#CHANNEL_MAX_24_GHZ}. The unit of the max power is 0.01dBm. Max
-     *     power values should be between INT16_MIN (-32768) and INT16_MAX (32767). If the max power
-     *     is set to INT16_MAX, the corresponding channel is not supported.
+     *     ActiveOperationalDataset#CHANNEL_MAX_24_GHZ}. The unit of the max power is 0.01dBm. For
+     *     example, 1000 means 0.01W and 2000 means 0.1W. If the power value of
+     *     {@code channelMaxPowers} is lower than the minimum output power supported by the
+     *     platform, the output power will be set to the minimum output power supported by the
+     *     platform. If the power value of {@code channelMaxPowers} is higher than the maximum
+     *     output power supported by the platform, the output power will be set to the maximum
+     *     output power supported by the platform. If the power value of {@code channelMaxPowers}
+     *     is set to {@link #MAX_POWER_CHANNEL_DISABLED}, the corresponding channel is disabled.
      * @param executor the executor to execute {@code receiver}.
      * @param receiver the receiver to receive the result of this operation.
      * @throws IllegalArgumentException if the size of {@code channelMaxPowers} is smaller than 1,
      *     or invalid channel or max power is configured.
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_CHANNEL_MAX_POWERS_ENABLED)
     @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
     public final void setChannelMaxPowers(
             @NonNull @Size(min = 1) SparseIntArray channelMaxPowers,
@@ -755,19 +767,6 @@
                                 + ActiveOperationalDataset.CHANNEL_MAX_24_GHZ
                                 + "]");
             }
-
-            if ((maxPower < POWER_LIMITATION_MIN) || (maxPower > POWER_LIMITATION_MAX)) {
-                throw new IllegalArgumentException(
-                        "Channel power ({channel: "
-                                + channel
-                                + ", maxPower: "
-                                + maxPower
-                                + "}) exceeds allowed range ["
-                                + POWER_LIMITATION_MIN
-                                + ", "
-                                + POWER_LIMITATION_MAX
-                                + "]");
-            }
         }
 
         try {
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java
index f699c30..1ea2459 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkException.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -23,6 +23,8 @@
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 
+import com.android.net.thread.flags.Flags;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -31,7 +33,7 @@
  *
  * @hide
  */
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
 @SystemApi
 public class ThreadNetworkException extends Exception {
     /** @hide */
@@ -139,16 +141,14 @@
     public static final int ERROR_THREAD_DISABLED = 12;
 
     /**
-     * The operation failed because it is not supported by the platform. For example, some platforms
-     * may not support setting the target power of each channel. The caller should not retry and may
-     * return an error to the user.
-     *
-     * @hide
+     * The operation failed because the feature is not supported by the platform. For example, some
+     * platforms may not support setting the target power of each channel. The caller should not
+     * retry and may return an error to the user.
      */
-    public static final int ERROR_UNSUPPORTED_OPERATION = 13;
+    public static final int ERROR_UNSUPPORTED_FEATURE = 13;
 
     private static final int ERROR_MIN = ERROR_INTERNAL_ERROR;
-    private static final int ERROR_MAX = ERROR_UNSUPPORTED_OPERATION;
+    private static final int ERROR_MAX = ERROR_UNSUPPORTED_FEATURE;
 
     private final int mErrorCode;
 
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
deleted file mode 100644
index 691bbf5..0000000
--- a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2023 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 android.net.thread;
-
-/**
- * Container for flag constants defined in the "thread_network" namespace.
- *
- * @hide
- */
-// TODO: replace this class with auto-generated "com.android.net.thread.flags.Flags" once the
-// flagging infra is fully supported for mainline modules.
-public final class ThreadNetworkFlags {
-    /** @hide */
-    public static final String FLAG_THREAD_ENABLED = "com.android.net.thread.flags.thread_enabled";
-
-    /** @hide */
-    public static final String FLAG_CONFIGURATION_ENABLED =
-            "com.android.net.thread.flags.configuration_enabled";
-
-    private ThreadNetworkFlags() {}
-}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
index 150b759..bca8b6e 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkManager.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -26,6 +26,7 @@
 import android.os.RemoteException;
 
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.thread.flags.Flags;
 
 import java.util.Collections;
 import java.util.List;
@@ -35,7 +36,7 @@
  *
  * @hide
  */
-@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@FlaggedApi(Flags.FLAG_THREAD_ENABLED)
 @SystemApi
 @SystemService(ThreadNetworkManager.SERVICE_NAME)
 public final class ThreadNetworkManager {
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index b621a6a..93b2f70 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -40,7 +40,7 @@
 import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
 import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT;
 import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
-import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_FEATURE;
 import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
 import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
 
@@ -120,15 +120,16 @@
 import com.android.server.ServiceManagerWrapper;
 import com.android.server.connectivity.ConnectivityResources;
 import com.android.server.thread.openthread.BackboneRouterState;
-import com.android.server.thread.openthread.BorderRouterConfiguration;
 import com.android.server.thread.openthread.DnsTxtAttribute;
 import com.android.server.thread.openthread.IChannelMasksReceiver;
 import com.android.server.thread.openthread.IOtDaemon;
 import com.android.server.thread.openthread.IOtDaemonCallback;
 import com.android.server.thread.openthread.IOtStatusReceiver;
+import com.android.server.thread.openthread.InfraLinkState;
 import com.android.server.thread.openthread.Ipv6AddressInfo;
 import com.android.server.thread.openthread.MeshcopTxtAttributes;
 import com.android.server.thread.openthread.OnMeshPrefixConfig;
+import com.android.server.thread.openthread.OtDaemonConfiguration;
 import com.android.server.thread.openthread.OtDaemonState;
 
 import libcore.util.HexEncoding;
@@ -213,7 +214,8 @@
     private boolean mUserRestricted;
     private boolean mForceStopOtDaemonEnabled;
 
-    private BorderRouterConfiguration mBorderRouterConfig;
+    private OtDaemonConfiguration mOtDaemonConfig;
+    private InfraLinkState mInfraLinkState;
 
     @VisibleForTesting
     ThreadNetworkControllerService(
@@ -238,11 +240,8 @@
         mInfraIfController = infraIfController;
         mUpstreamNetworkRequest = newUpstreamNetworkRequest();
         mNetworkToInterface = new HashMap<Network, String>();
-        mBorderRouterConfig =
-                new BorderRouterConfiguration.Builder()
-                        .setIsBorderRoutingEnabled(true)
-                        .setInfraInterfaceName(null)
-                        .build();
+        mOtDaemonConfig = new OtDaemonConfiguration.Builder().build();
+        mInfraLinkState = new InfraLinkState.Builder().build();
         mPersistentSettings = persistentSettings;
         mNsdPublisher = nsdPublisher;
         mUserManager = userManager;
@@ -571,6 +570,7 @@
                 }
             }
         }
+        // TODO: set the configuration at ot-daemon
     }
 
     @Override
@@ -1055,7 +1055,7 @@
             case OT_ERROR_BUSY:
                 return ERROR_BUSY;
             case OT_ERROR_NOT_IMPLEMENTED:
-                return ERROR_UNSUPPORTED_OPERATION;
+                return ERROR_UNSUPPORTED_FEATURE;
             case OT_ERROR_NO_BUFS:
                 return ERROR_RESOURCE_EXHAUSTED;
             case OT_ERROR_PARSE:
@@ -1232,54 +1232,45 @@
         }
     }
 
-    private void configureBorderRouter(BorderRouterConfiguration borderRouterConfig) {
-        if (mBorderRouterConfig.equals(borderRouterConfig)) {
+    private void setInfraLinkState(InfraLinkState infraLinkState) {
+        if (mInfraLinkState.equals(infraLinkState)) {
             return;
         }
-        Log.i(
-                TAG,
-                "Configuring Border Router: " + mBorderRouterConfig + " -> " + borderRouterConfig);
-        mBorderRouterConfig = borderRouterConfig;
+        Log.i(TAG, "Infra link state changed: " + mInfraLinkState + " -> " + infraLinkState);
+        mInfraLinkState = infraLinkState;
         ParcelFileDescriptor infraIcmp6Socket = null;
-        if (mBorderRouterConfig.infraInterfaceName != null) {
+        if (mInfraLinkState.interfaceName != null) {
             try {
                 infraIcmp6Socket =
-                        mInfraIfController.createIcmp6Socket(
-                                mBorderRouterConfig.infraInterfaceName);
+                        mInfraIfController.createIcmp6Socket(mInfraLinkState.interfaceName);
             } catch (IOException e) {
                 Log.i(TAG, "Failed to create ICMPv6 socket on infra network interface", e);
             }
         }
         try {
             getOtDaemon()
-                    .configureBorderRouter(
-                            mBorderRouterConfig,
+                    .setInfraLinkState(
+                            mInfraLinkState,
                             infraIcmp6Socket,
-                            new ConfigureBorderRouterStatusReceiver());
+                            new setInfraLinkStateStatusReceiver());
         } catch (RemoteException | ThreadNetworkException e) {
-            Log.w(TAG, "Failed to configure border router " + mBorderRouterConfig, e);
+            Log.w(TAG, "Failed to configure border router " + mOtDaemonConfig, e);
         }
     }
 
     private void enableBorderRouting(String infraIfName) {
-        BorderRouterConfiguration borderRouterConfig =
-                newBorderRouterConfigBuilder(mBorderRouterConfig)
-                        .setIsBorderRoutingEnabled(true)
-                        .setInfraInterfaceName(infraIfName)
-                        .build();
+        InfraLinkState infraLinkState =
+                newInfraLinkStateBuilder(mInfraLinkState).setInterfaceName(infraIfName).build();
         Log.i(TAG, "Enable border routing on AIL: " + infraIfName);
-        configureBorderRouter(borderRouterConfig);
+        setInfraLinkState(infraLinkState);
     }
 
     private void disableBorderRouting() {
         mUpstreamNetwork = null;
-        BorderRouterConfiguration borderRouterConfig =
-                newBorderRouterConfigBuilder(mBorderRouterConfig)
-                        .setIsBorderRoutingEnabled(false)
-                        .setInfraInterfaceName(null)
-                        .build();
+        InfraLinkState infraLinkState =
+                newInfraLinkStateBuilder(mInfraLinkState).setInterfaceName(null).build();
         Log.i(TAG, "Disabling border routing");
-        configureBorderRouter(borderRouterConfig);
+        setInfraLinkState(infraLinkState);
     }
 
     private void handleThreadInterfaceStateChanged(boolean isUp) {
@@ -1380,11 +1371,13 @@
         return builder.build();
     }
 
-    private static BorderRouterConfiguration.Builder newBorderRouterConfigBuilder(
-            BorderRouterConfiguration brConfig) {
-        return new BorderRouterConfiguration.Builder()
-                .setIsBorderRoutingEnabled(brConfig.isBorderRoutingEnabled)
-                .setInfraInterfaceName(brConfig.infraInterfaceName);
+    private static OtDaemonConfiguration.Builder newOtDaemonConfigBuilder(
+            OtDaemonConfiguration config) {
+        return new OtDaemonConfiguration.Builder();
+    }
+
+    private static InfraLinkState.Builder newInfraLinkStateBuilder(InfraLinkState infraLinkState) {
+        return new InfraLinkState.Builder().setInterfaceName(infraLinkState.interfaceName);
     }
 
     private static final class CallbackMetadata {
@@ -1408,8 +1401,9 @@
         }
     }
 
-    private static final class ConfigureBorderRouterStatusReceiver extends IOtStatusReceiver.Stub {
-        public ConfigureBorderRouterStatusReceiver() {}
+    private static final class setOtDaemonConfigurationStatusReceiver
+            extends IOtStatusReceiver.Stub {
+        public setOtDaemonConfigurationStatusReceiver() {}
 
         @Override
         public void onSuccess() {
@@ -1418,7 +1412,21 @@
 
         @Override
         public void onError(int i, String s) {
-            Log.w(TAG, String.format("Failed to configure border router: %d %s", i, s));
+            Log.w(TAG, String.format("Failed to set configurations: %d %s", i, s));
+        }
+    }
+
+    private static final class setInfraLinkStateStatusReceiver extends IOtStatusReceiver.Stub {
+        public setInfraLinkStateStatusReceiver() {}
+
+        @Override
+        public void onSuccess() {
+            Log.i(TAG, "Set the infra link state successfully");
+        }
+
+        @Override
+        public void onError(int i, String s) {
+            Log.w(TAG, String.format("Failed to set the infra link state: %d %s", i, s));
         }
     }
 
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 6db7c9c..6572755 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -40,6 +40,7 @@
     static_libs: [
         "androidx.test.ext.junit",
         "compatibility-device-util-axt",
+        "com.android.net.thread.flags-aconfig-java",
         "ctstestrunner-axt",
         "guava",
         "guava-android-testlib",
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 1a101b6..c048394 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -35,6 +35,7 @@
 import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
 import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
 import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_FEATURE;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
@@ -70,12 +71,15 @@
 import android.os.Build;
 import android.os.HandlerThread;
 import android.os.OutcomeReceiver;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.util.SparseIntArray;
 
 import androidx.annotation.NonNull;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.LargeTest;
 
 import com.android.net.module.util.ArrayTrackRecord;
+import com.android.net.thread.flags.Flags;
 import com.android.testutils.FunctionalUtils.ThrowingRunnable;
 
 import org.junit.After;
@@ -116,11 +120,20 @@
     private static final int SET_CONFIGURATION_TIMEOUT_MILLIS = 1_000;
     private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 30_000;
     private static final int SERVICE_LOST_TIMEOUT_MILLIS = 20_000;
+    private static final int VALID_POWER = 32_767;
+    private static final int VALID_CHANNEL = 20;
+    private static final int INVALID_CHANNEL = 10;
     private static final String MESHCOP_SERVICE_TYPE = "_meshcop._udp";
     private static final String THREAD_NETWORK_PRIVILEGED =
             "android.permission.THREAD_NETWORK_PRIVILEGED";
     private static final ThreadConfiguration DEFAULT_CONFIG =
             new ThreadConfiguration.Builder().build();
+    private static final SparseIntArray CHANNEL_MAX_POWERS =
+            new SparseIntArray() {
+                {
+                    put(VALID_CHANNEL, VALID_POWER);
+                }
+            };
 
     @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
 
@@ -1460,4 +1473,52 @@
         @Override
         public void onServiceInfoCallbackUnregistered() {}
     }
+
+    @Test
+    @RequiresFlagsEnabled({Flags.FLAG_CHANNEL_MAX_POWERS_ENABLED})
+    public void setChannelMaxPowers_withPrivilegedPermission_success() throws Exception {
+        CompletableFuture<Void> powerFuture = new CompletableFuture<>();
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () ->
+                        mController.setChannelMaxPowers(
+                                CHANNEL_MAX_POWERS, mExecutor, newOutcomeReceiver(powerFuture)));
+
+        try {
+            assertThat(powerFuture.get()).isNull();
+        } catch (ExecutionException exception) {
+            ThreadNetworkException thrown = (ThreadNetworkException) exception.getCause();
+            assertThat(thrown.getErrorCode()).isEqualTo(ERROR_UNSUPPORTED_FEATURE);
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled({Flags.FLAG_CHANNEL_MAX_POWERS_ENABLED})
+    public void setChannelMaxPowers_withoutPrivilegedPermission_throwsSecurityException()
+            throws Exception {
+        dropAllPermissions();
+
+        assertThrows(
+                SecurityException.class,
+                () -> mController.setChannelMaxPowers(CHANNEL_MAX_POWERS, mExecutor, v -> {}));
+    }
+
+    @Test
+    @RequiresFlagsEnabled({Flags.FLAG_CHANNEL_MAX_POWERS_ENABLED})
+    public void setChannelMaxPowers_invalidChannel_throwsIllegalArgumentException() {
+        final SparseIntArray INVALID_CHANNEL_ARRAY =
+                new SparseIntArray() {
+                    {
+                        put(INVALID_CHANNEL, VALID_POWER);
+                    }
+                };
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.setChannelMaxPowers(new SparseIntArray(), mExecutor, v -> {}));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.setChannelMaxPowers(INVALID_CHANNEL_ARRAY, mExecutor, v -> {}));
+    }
 }
diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java
deleted file mode 100644
index ba04348..0000000
--- a/thread/tests/integration/src/android/net/thread/ThreadNetworkControllerTest.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * 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 android.net.thread;
-
-import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
-
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-import static com.android.testutils.TestPermissionUtil.runAsShell;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import android.content.Context;
-import android.net.thread.utils.ThreadFeatureCheckerRule;
-import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
-import android.os.OutcomeReceiver;
-import android.util.SparseIntArray;
-
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-/** Tests for hide methods of {@link ThreadNetworkController}. */
-@LargeTest
-@RequiresThreadFeature
-@RunWith(AndroidJUnit4.class)
-public class ThreadNetworkControllerTest {
-    private static final int VALID_POWER = 32_767;
-    private static final int INVALID_POWER = 32_768;
-    private static final int VALID_CHANNEL = 20;
-    private static final int INVALID_CHANNEL = 10;
-    private static final String THREAD_NETWORK_PRIVILEGED =
-            "android.permission.THREAD_NETWORK_PRIVILEGED";
-
-    private static final SparseIntArray CHANNEL_MAX_POWERS =
-            new SparseIntArray() {
-                {
-                    put(20, 32767);
-                }
-            };
-
-    @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
-
-    private final Context mContext = ApplicationProvider.getApplicationContext();
-    private ExecutorService mExecutor;
-    private ThreadNetworkController mController;
-
-    @Before
-    public void setUp() throws Exception {
-        mController =
-                mContext.getSystemService(ThreadNetworkManager.class)
-                        .getAllThreadNetworkControllers()
-                        .get(0);
-
-        mExecutor = Executors.newSingleThreadExecutor();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        dropAllPermissions();
-    }
-
-    @Test
-    public void setChannelMaxPowers_withPrivilegedPermission_success() throws Exception {
-        CompletableFuture<Void> powerFuture = new CompletableFuture<>();
-
-        runAsShell(
-                THREAD_NETWORK_PRIVILEGED,
-                () ->
-                        mController.setChannelMaxPowers(
-                                CHANNEL_MAX_POWERS, mExecutor, newOutcomeReceiver(powerFuture)));
-
-        try {
-            assertThat(powerFuture.get()).isNull();
-        } catch (ExecutionException exception) {
-            ThreadNetworkException thrown = (ThreadNetworkException) exception.getCause();
-            assertThat(thrown.getErrorCode()).isEqualTo(ERROR_UNSUPPORTED_OPERATION);
-        }
-    }
-
-    @Test
-    public void setChannelMaxPowers_withoutPrivilegedPermission_throwsSecurityException()
-            throws Exception {
-        dropAllPermissions();
-
-        assertThrows(
-                SecurityException.class,
-                () -> mController.setChannelMaxPowers(CHANNEL_MAX_POWERS, mExecutor, v -> {}));
-    }
-
-    @Test
-    public void setChannelMaxPowers_emptyChannelMaxPower_throwsIllegalArgumentException() {
-        assertThrows(
-                IllegalArgumentException.class,
-                () -> mController.setChannelMaxPowers(new SparseIntArray(), mExecutor, v -> {}));
-    }
-
-    @Test
-    public void setChannelMaxPowers_invalidChannel_throwsIllegalArgumentException() {
-        final SparseIntArray INVALID_CHANNEL_ARRAY =
-                new SparseIntArray() {
-                    {
-                        put(INVALID_CHANNEL, VALID_POWER);
-                    }
-                };
-
-        assertThrows(
-                IllegalArgumentException.class,
-                () -> mController.setChannelMaxPowers(INVALID_CHANNEL_ARRAY, mExecutor, v -> {}));
-    }
-
-    @Test
-    public void setChannelMaxPowers_invalidPower_throwsIllegalArgumentException() {
-        final SparseIntArray INVALID_POWER_ARRAY =
-                new SparseIntArray() {
-                    {
-                        put(VALID_CHANNEL, INVALID_POWER);
-                    }
-                };
-
-        assertThrows(
-                IllegalArgumentException.class,
-                () -> mController.setChannelMaxPowers(INVALID_POWER_ARRAY, mExecutor, v -> {}));
-    }
-
-    private static void dropAllPermissions() {
-        getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
-    }
-
-    private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
-            CompletableFuture<V> future) {
-        return new OutcomeReceiver<V, ThreadNetworkException>() {
-            @Override
-            public void onResult(V result) {
-                future.complete(result);
-            }
-
-            @Override
-            public void onError(ThreadNetworkException e) {
-                future.completeExceptionally(e);
-            }
-        };
-    }
-}
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
index ac74372..0423578 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -19,7 +19,7 @@
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
 import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
 import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
-import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_OPERATION;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_FEATURE;
 import static android.os.Process.SYSTEM_UID;
 
 import static com.google.common.io.BaseEncoding.base16;
@@ -394,7 +394,7 @@
         doAnswer(
                         invoke -> {
                             getSetChannelMaxPowersReceiver(invoke)
-                                    .onError(ERROR_UNSUPPORTED_OPERATION, "");
+                                    .onError(ERROR_UNSUPPORTED_FEATURE, "");
                             return null;
                         })
                 .when(mMockService)