Merge "Add SoftApConfiguration to TetheringRequest" into main
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/common/flags.aconfig b/common/flags.aconfig
index 1b0da4e..45cbb78 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,5 @@
   namespace: "android_core_networking"
   description: "Flag for NetworkStats#addEntries API"
   bug: "335680025"
+  is_fixed_read_only: true
 }
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 0fe24a2..53b1eb2 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -6002,7 +6002,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/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/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