Merge "[Thread] move infra network utilities to IntegrationTestUtils.kt" into main
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 8d96066..3b197fc 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -114,7 +114,7 @@
         "current_sdkinfo",
         "netbpfload.33rc",
         "netbpfload.35rc",
-        "ot-daemon.init.34rc",
+        "ot-daemon.34rc",
     ],
     manifest: "manifest.json",
     key: "com.android.tethering.key",
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index 4d1e7ef..e6e99f4 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -359,6 +359,7 @@
         } catch (IllegalStateException e) {
             // Silent if the rule already exists. Note that the errno EEXIST was rethrown as
             // IllegalStateException. See BpfMap#insertEntry.
+            return false;
         }
         return true;
     }
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
index d28a397..026b1c3 100644
--- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -140,6 +140,8 @@
 
     /**
      * Adds a tethering IPv4 offload rule to appropriate BPF map.
+     *
+     * @return true iff the map was modified, false if the key already exists or there was an error.
      */
     public abstract boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key,
             @NonNull Tether4Value value);
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index d62f18f..13f4f2a 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -2082,6 +2082,7 @@
                     chooseUpstreamType(true);
                     mTryCell = false;
                 }
+                mTetheringMetrics.initUpstreamUsageBaseline();
             }
 
             @Override
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index 2202106..fc50faf 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -16,6 +16,8 @@
 
 package com.android.networkstack.tethering.metrics;
 
+import static android.app.usage.NetworkStats.Bucket.STATE_ALL;
+import static android.app.usage.NetworkStats.Bucket.TAG_NONE;
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
@@ -24,6 +26,7 @@
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
 import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
 import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkStats.UID_TETHERING;
 import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
 import static android.net.NetworkTemplate.MATCH_ETHERNET;
 import static android.net.NetworkTemplate.MATCH_MOBILE;
@@ -52,13 +55,19 @@
 import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
 
 import android.annotation.Nullable;
+import android.app.usage.NetworkStats;
+import android.app.usage.NetworkStatsManager;
 import android.content.Context;
 import android.net.NetworkCapabilities;
 import android.net.NetworkTemplate;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
 import android.stats.connectivity.DownstreamType;
 import android.stats.connectivity.ErrorCode;
 import android.stats.connectivity.UpstreamType;
 import android.stats.connectivity.UserType;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -75,6 +84,10 @@
 /**
  * Collection of utilities for tethering metrics.
  *
+ *  <p>This class is thread-safe. All accesses to this class will be either posting to the internal
+ *  handler thread for processing or checking whether the access is from the internal handler
+ *  thread. However, the constructor is an exception, as it is called on another thread.
+ *
  * To see if the logs are properly sent to statsd, execute following commands
  *
  * $ adb shell cmd stats print-logs
@@ -93,11 +106,16 @@
      */
     private static final String TETHER_UPSTREAM_DATA_USAGE_METRICS =
             "tether_upstream_data_usage_metrics";
+    @VisibleForTesting
+    static final DataUsage EMPTY = new DataUsage(0L /* txBytes */, 0L /* rxBytes */);
     private final SparseArray<NetworkTetheringReported.Builder> mBuilderMap = new SparseArray<>();
     private final SparseArray<Long> mDownstreamStartTime = new SparseArray<Long>();
     private final ArrayList<RecordUpstreamEvent> mUpstreamEventList = new ArrayList<>();
+    private final ArrayMap<UpstreamType, DataUsage> mUpstreamUsageBaseline = new ArrayMap<>();
     private final Context mContext;
     private final Dependencies mDependencies;
+    private final NetworkStatsManager mNetworkStatsManager;
+    private final Handler mHandler;
     private UpstreamType mCurrentUpstream = null;
     private Long mCurrentUpStreamStartTime = 0L;
 
@@ -136,6 +154,14 @@
             return SdkLevel.isAtLeastT() && DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
                     context, TETHER_UPSTREAM_DATA_USAGE_METRICS);
         }
+
+        /**
+         * @see Handler
+         */
+        @NonNull
+        public Handler createHandler(Looper looper) {
+            return new Handler(looper);
+        }
     }
 
     /**
@@ -150,24 +176,49 @@
     TetheringMetrics(Context context, Dependencies dependencies) {
         mContext = context;
         mDependencies = dependencies;
+        mNetworkStatsManager = mContext.getSystemService(NetworkStatsManager.class);
+        final HandlerThread thread = new HandlerThread(TAG);
+        thread.start();
+        mHandler = dependencies.createHandler(thread.getLooper());
     }
 
-    private static class DataUsage {
-        final long mTxBytes;
-        final long mRxBytes;
+    @VisibleForTesting
+    static class DataUsage {
+        public final long txBytes;
+        public final long rxBytes;
 
         DataUsage(long txBytes, long rxBytes) {
-            mTxBytes = txBytes;
-            mRxBytes = rxBytes;
+            this.txBytes = txBytes;
+            this.rxBytes = rxBytes;
         }
 
-        public long getTxBytes() {
-            return mTxBytes;
+        /*** Calculate the data usage delta from give new and old usage */
+        public static DataUsage subtract(DataUsage newUsage, DataUsage oldUsage) {
+            return new DataUsage(
+                    newUsage.txBytes - oldUsage.txBytes,
+                    newUsage.rxBytes - oldUsage.rxBytes);
         }
 
-        public long getRxBytes() {
-            return mRxBytes;
+        @Override
+        public int hashCode() {
+            return (int) (txBytes & 0xFFFFFFFF)
+                    + ((int) (txBytes >> 32) * 3)
+                    + ((int) (rxBytes & 0xFFFFFFFF) * 5)
+                    + ((int) (rxBytes >> 32) * 7);
         }
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof DataUsage)) {
+                return false;
+            }
+            return txBytes == ((DataUsage) other).txBytes
+                    && rxBytes == ((DataUsage) other).rxBytes;
+        }
+
     }
 
     private static class RecordUpstreamEvent {
@@ -194,6 +245,10 @@
      * @param callerPkg The package name of the caller.
      */
     public void createBuilder(final int downstreamType, final String callerPkg) {
+        mHandler.post(() -> handleCreateBuilder(downstreamType, callerPkg));
+    }
+
+    private void handleCreateBuilder(final int downstreamType, final String callerPkg) {
         NetworkTetheringReported.Builder statsBuilder = NetworkTetheringReported.newBuilder()
                 .setDownstreamType(downstreamTypeToEnum(downstreamType))
                 .setUserType(userTypeToEnum(callerPkg))
@@ -211,6 +266,10 @@
      * @param errCode The error code to set.
      */
     public void updateErrorCode(final int downstreamType, final int errCode) {
+        mHandler.post(() -> handleUpdateErrorCode(downstreamType, errCode));
+    }
+
+    private void handleUpdateErrorCode(final int downstreamType, final int errCode) {
         NetworkTetheringReported.Builder statsBuilder = mBuilderMap.get(downstreamType);
         if (statsBuilder == null) {
             Log.e(TAG, "Given downstreamType does not exist, this is a bug!");
@@ -219,13 +278,26 @@
         statsBuilder.setErrorCode(errorCodeToEnum(errCode));
     }
 
-    private DataUsage calculateDataUsage(@Nullable UpstreamType upstream) {
+    /**
+     * Calculates the data usage difference between the current and previous usage for the
+     * specified upstream type.
+     *
+     * @return A DataUsage object containing the calculated difference in transmitted (tx) and
+     *         received (rx) bytes.
+     */
+    private DataUsage calculateDataUsageDelta(@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);
+            final DataUsage oldUsage = mUpstreamUsageBaseline.getOrDefault(upstream, EMPTY);
+            if (oldUsage.equals(EMPTY)) {
+                Log.d(TAG, "No usage baseline for the upstream=" + upstream);
+                return EMPTY;
+            }
+            // TODO(b/352537247): Fix data usage which might be incorrect if the device uses
+            //  tethering with the same upstream for over 15 days.
+            return DataUsage.subtract(getCurrentDataUsageForUpstreamType(upstream), oldUsage);
         }
-        return new DataUsage(0L, 0L);
+        return EMPTY;
     }
 
     /**
@@ -234,12 +306,16 @@
      * @param ns The UpstreamNetworkState object representing the current upstream network state.
      */
     public void maybeUpdateUpstreamType(@Nullable final UpstreamNetworkState ns) {
+        mHandler.post(() -> handleMaybeUpdateUpstreamType(ns));
+    }
+
+    private void handleMaybeUpdateUpstreamType(@Nullable final UpstreamNetworkState ns) {
         UpstreamType upstream = transportTypeToUpstreamTypeEnum(ns);
         if (upstream.equals(mCurrentUpstream)) return;
 
         final long newTime = mDependencies.timeNow();
         if (mCurrentUpstream != null) {
-            final DataUsage dataUsage = calculateDataUsage(upstream);
+            final DataUsage dataUsage = calculateDataUsageDelta(mCurrentUpstream);
             mUpstreamEventList.add(new RecordUpstreamEvent(mCurrentUpStreamStartTime, newTime,
                     mCurrentUpstream, dataUsage));
         }
@@ -292,14 +368,14 @@
             final long startTime = Math.max(downstreamStartTime, event.mStartTime);
             // Handle completed upstream events.
             addUpstreamEvent(upstreamEventsBuilder, startTime, event.mStopTime,
-                    event.mUpstreamType, event.mDataUsage.mTxBytes, event.mDataUsage.mRxBytes);
+                    event.mUpstreamType, event.mDataUsage.txBytes, event.mDataUsage.rxBytes);
         }
         final long startTime = Math.max(downstreamStartTime, mCurrentUpStreamStartTime);
         final long stopTime = mDependencies.timeNow();
         // Handle the last upstream event.
-        final DataUsage dataUsage = calculateDataUsage(mCurrentUpstream);
+        final DataUsage dataUsage = calculateDataUsageDelta(mCurrentUpstream);
         addUpstreamEvent(upstreamEventsBuilder, startTime, stopTime, mCurrentUpstream,
-                dataUsage.mTxBytes, dataUsage.mRxBytes);
+                dataUsage.txBytes, dataUsage.rxBytes);
         statsBuilder.setUpstreamEvents(upstreamEventsBuilder);
         statsBuilder.setDurationMillis(stopTime - downstreamStartTime);
     }
@@ -315,6 +391,10 @@
      * @param downstreamType the type of downstream event to remove statistics for
      */
     public void sendReport(final int downstreamType) {
+        mHandler.post(() -> handleSendReport(downstreamType));
+    }
+
+    private void handleSendReport(final int downstreamType) {
         final NetworkTetheringReported.Builder statsBuilder = mBuilderMap.get(downstreamType);
         if (statsBuilder == null) {
             Log.e(TAG, "Given downstreamType does not exist, this is a bug!");
@@ -335,8 +415,7 @@
      *
      * @param reported a NetworkTetheringReported object containing statistics to write
      */
-    @VisibleForTesting
-    public void write(@NonNull final NetworkTetheringReported reported) {
+    private void write(@NonNull final NetworkTetheringReported reported) {
         final byte[] upstreamEvents = reported.getUpstreamEvents().toByteArray();
         mDependencies.write(reported);
         if (DBG) {
@@ -358,12 +437,67 @@
     }
 
     /**
+     * Initialize the upstream data usage baseline when tethering is turned on.
+     */
+    public void initUpstreamUsageBaseline() {
+        mHandler.post(() -> handleInitUpstreamUsageBaseline());
+    }
+
+    private void handleInitUpstreamUsageBaseline() {
+        if (!(mDependencies.isUpstreamDataUsageMetricsEnabled(mContext)
+                && mUpstreamUsageBaseline.isEmpty())) {
+            return;
+        }
+
+        for (UpstreamType type : UpstreamType.values()) {
+            if (!isUsageSupportedForUpstreamType(type)) continue;
+            mUpstreamUsageBaseline.put(type, getCurrentDataUsageForUpstreamType(type));
+        }
+    }
+
+    @VisibleForTesting
+    @NonNull
+    DataUsage getDataUsageFromUpstreamType(@NonNull UpstreamType type) {
+        if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+            throw new IllegalStateException(
+                    "Not running on Handler thread: " + Thread.currentThread().getName());
+        }
+        return mUpstreamUsageBaseline.getOrDefault(type, EMPTY);
+    }
+
+
+    /**
+     * Get the current usage for given upstream type.
+     */
+    @NonNull
+    private DataUsage getCurrentDataUsageForUpstreamType(@NonNull UpstreamType type) {
+        final NetworkStats stats = mNetworkStatsManager.queryDetailsForUidTagState(
+                buildNetworkTemplateForUpstreamType(type), Long.MIN_VALUE, Long.MAX_VALUE,
+                UID_TETHERING, TAG_NONE, STATE_ALL);
+
+        final NetworkStats.Bucket bucket = new NetworkStats.Bucket();
+        Long totalTxBytes = 0L;
+        Long totalRxBytes = 0L;
+        while (stats.hasNextBucket()) {
+            stats.getNextBucket(bucket);
+            totalTxBytes += bucket.getTxBytes();
+            totalRxBytes += bucket.getRxBytes();
+        }
+        return new DataUsage(totalTxBytes, totalRxBytes);
+    }
+
+    /**
      * Cleans up the variables related to upstream events when tethering is turned off.
      */
     public void cleanup() {
+        mHandler.post(() -> handleCleanup());
+    }
+
+    private void handleCleanup() {
         mUpstreamEventList.clear();
         mCurrentUpstream = null;
         mCurrentUpStreamStartTime = 0L;
+        mUpstreamUsageBaseline.clear();
     }
 
     private DownstreamType downstreamTypeToEnum(final int ifaceType) {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
index fbc2893..34689bc 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -16,12 +16,19 @@
 
 package com.android.networkstack.tethering.metrics;
 
+import static android.app.usage.NetworkStats.Bucket.STATE_ALL;
+import static android.app.usage.NetworkStats.Bucket.TAG_NONE;
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
+import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.ROAMING_NO;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.UID_TETHERING;
 import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
 import static android.net.NetworkTemplate.MATCH_ETHERNET;
 import static android.net.NetworkTemplate.MATCH_MOBILE;
@@ -49,29 +56,47 @@
 import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_TYPE;
 import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
 import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+import static android.stats.connectivity.UpstreamType.UT_BLUETOOTH;
+import static android.stats.connectivity.UpstreamType.UT_CELLULAR;
+import static android.stats.connectivity.UpstreamType.UT_ETHERNET;
+import static android.stats.connectivity.UpstreamType.UT_WIFI;
+
+import static com.android.networkstack.tethering.metrics.TetheringMetrics.EMPTY;
+import static com.android.testutils.NetworkStatsUtilsKt.makePublicStatsFromAndroidNetStats;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.verify;
 
+import android.app.usage.NetworkStatsManager;
 import android.content.Context;
 import android.net.NetworkCapabilities;
+import android.net.NetworkStats;
 import android.net.NetworkTemplate;
 import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.stats.connectivity.DownstreamType;
 import android.stats.connectivity.ErrorCode;
 import android.stats.connectivity.UpstreamType;
 import android.stats.connectivity.UserType;
+import android.util.ArrayMap;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.networkstack.tethering.UpstreamNetworkState;
+import com.android.networkstack.tethering.metrics.TetheringMetrics.DataUsage;
 import com.android.networkstack.tethering.metrics.TetheringMetrics.Dependencies;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.HandlerUtils;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -90,14 +115,19 @@
     private static final String GMS_PKG = "com.google.android.gms";
     private static final long TEST_START_TIME = 1670395936033L;
     private static final long SECOND_IN_MILLIS = 1_000L;
+    private static final long DEFAULT_TIMEOUT = 2000L;
     private static final int MATCH_NONE = -1;
 
     @Mock private Context mContext;
     @Mock private Dependencies mDeps;
+    @Mock private NetworkStatsManager mNetworkStatsManager;
 
     private TetheringMetrics mTetheringMetrics;
     private final NetworkTetheringReported.Builder mStatsBuilder =
             NetworkTetheringReported.newBuilder();
+    private final ArrayMap<UpstreamType, DataUsage> mMockUpstreamUsageBaseline = new ArrayMap<>();
+    private HandlerThread mThread;
+    private Handler mHandler;
 
     private long mElapsedRealtime;
 
@@ -124,10 +154,35 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         doReturn(TEST_START_TIME).when(mDeps).timeNow();
+        doReturn(mNetworkStatsManager).when(mContext).getSystemService(NetworkStatsManager.class);
+        mThread = new HandlerThread("TetheringMetricsTest");
+        mThread.start();
+        mHandler = new Handler(mThread.getLooper());
+        doReturn(mHandler).when(mDeps).createHandler(any());
+        // Set up the usage for upstream types.
+        mMockUpstreamUsageBaseline.put(UT_CELLULAR, new DataUsage(100L, 200L));
+        mMockUpstreamUsageBaseline.put(UT_WIFI, new DataUsage(400L, 800L));
+        mMockUpstreamUsageBaseline.put(UT_BLUETOOTH, new DataUsage(50L, 80L));
+        mMockUpstreamUsageBaseline.put(UT_ETHERNET, new DataUsage(0L, 0L));
+        doAnswer(inv -> {
+            final NetworkTemplate template = (NetworkTemplate) inv.getArguments()[0];
+            final DataUsage dataUsage = mMockUpstreamUsageBaseline.getOrDefault(
+                    matchRuleToUpstreamType(template.getMatchRule()), new DataUsage(0L, 0L));
+            return makeNetworkStatsWithTxRxBytes(dataUsage);
+        }).when(mNetworkStatsManager).queryDetailsForUidTagState(any(), eq(Long.MIN_VALUE),
+                eq(Long.MAX_VALUE), eq(UID_TETHERING), eq(TAG_NONE), eq(STATE_ALL));
         mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
         mElapsedRealtime = 0L;
     }
 
+    @After
+    public void tearDown() throws Exception {
+        if (mThread != null) {
+            mThread.quitSafely();
+            mThread.join();
+        }
+    }
+
     private void verifyReport(final DownstreamType downstream, final ErrorCode error,
             final UserType user, final UpstreamEvents.Builder upstreamEvents, final long duration)
             throws Exception {
@@ -142,9 +197,15 @@
         verify(mDeps).write(expectedReport);
     }
 
+    private void runAndWaitForIdle(Runnable r) {
+        r.run();
+        HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+    }
+
     private void updateErrorAndSendReport(final int downstream, final int error) {
         mTetheringMetrics.updateErrorCode(downstream, error);
         mTetheringMetrics.sendReport(downstream);
+        HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
     }
 
     private static NetworkCapabilities buildUpstreamCapabilities(final int[] transports) {
@@ -176,7 +237,7 @@
     private void runDownstreamTypesTest(final int type, final DownstreamType expectedResult)
             throws Exception {
         mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
-        mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG);
+        runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG));
         final long duration = 2 * SECOND_IN_MILLIS;
         incrementCurrentTime(duration);
         UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
@@ -202,14 +263,15 @@
     private void runErrorCodesTest(final int errorCode, final ErrorCode expectedResult)
             throws Exception {
         mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
-        mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
-        mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI));
+        runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG));
+        runAndWaitForIdle(() ->
+                mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
         final long duration = 2 * SECOND_IN_MILLIS;
         incrementCurrentTime(duration);
         updateErrorAndSendReport(TETHERING_WIFI, errorCode);
 
         UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
-        addUpstreamEvent(upstreamEvents, UpstreamType.UT_WIFI, duration, 0L, 0L);
+        addUpstreamEvent(upstreamEvents, UT_WIFI, duration, 0L, 0L);
         verifyReport(DownstreamType.DS_TETHERING_WIFI, expectedResult, UserType.USER_UNKNOWN,
                     upstreamEvents, getElapsedRealtime());
         clearElapsedRealtime();
@@ -243,7 +305,7 @@
     private void runUserTypesTest(final String callerPkg, final UserType expectedResult)
             throws Exception {
         mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
-        mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg);
+        runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg));
         final long duration = 1 * SECOND_IN_MILLIS;
         incrementCurrentTime(duration);
         updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
@@ -267,8 +329,8 @@
     private void runUpstreamTypesTest(final UpstreamNetworkState ns,
             final UpstreamType expectedResult) throws Exception {
         mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
-        mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
-        mTetheringMetrics.maybeUpdateUpstreamType(ns);
+        runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG));
+        runAndWaitForIdle(() -> mTetheringMetrics.maybeUpdateUpstreamType(ns));
         final long duration = 2 * SECOND_IN_MILLIS;
         incrementCurrentTime(duration);
         updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
@@ -283,10 +345,10 @@
     @Test
     public void testUpstreamTypes() throws Exception {
         runUpstreamTypesTest(null , UpstreamType.UT_NO_NETWORK);
-        runUpstreamTypesTest(buildUpstreamState(TRANSPORT_CELLULAR), UpstreamType.UT_CELLULAR);
-        runUpstreamTypesTest(buildUpstreamState(TRANSPORT_WIFI), UpstreamType.UT_WIFI);
-        runUpstreamTypesTest(buildUpstreamState(TRANSPORT_BLUETOOTH), UpstreamType.UT_BLUETOOTH);
-        runUpstreamTypesTest(buildUpstreamState(TRANSPORT_ETHERNET), UpstreamType.UT_ETHERNET);
+        runUpstreamTypesTest(buildUpstreamState(TRANSPORT_CELLULAR), UT_CELLULAR);
+        runUpstreamTypesTest(buildUpstreamState(TRANSPORT_WIFI), UT_WIFI);
+        runUpstreamTypesTest(buildUpstreamState(TRANSPORT_BLUETOOTH), UT_BLUETOOTH);
+        runUpstreamTypesTest(buildUpstreamState(TRANSPORT_ETHERNET), UT_ETHERNET);
         runUpstreamTypesTest(buildUpstreamState(TRANSPORT_WIFI_AWARE), UpstreamType.UT_WIFI_AWARE);
         runUpstreamTypesTest(buildUpstreamState(TRANSPORT_LOWPAN), UpstreamType.UT_LOWPAN);
         runUpstreamTypesTest(buildUpstreamState(TRANSPORT_CELLULAR, TRANSPORT_WIFI,
@@ -295,13 +357,13 @@
 
     @Test
     public void testMultiBuildersCreatedBeforeSendReport() throws Exception {
-        mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+        runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG));
         final long wifiTetheringStartTime = currentTimeMillis();
         incrementCurrentTime(1 * SECOND_IN_MILLIS);
-        mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG);
+        runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG));
         final long usbTetheringStartTime = currentTimeMillis();
         incrementCurrentTime(2 * SECOND_IN_MILLIS);
-        mTetheringMetrics.createBuilder(TETHERING_BLUETOOTH, GMS_PKG);
+        runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_BLUETOOTH, GMS_PKG));
         final long bluetoothTetheringStartTime = currentTimeMillis();
         incrementCurrentTime(3 * SECOND_IN_MILLIS);
         updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_DHCPSERVER_ERROR);
@@ -335,19 +397,20 @@
 
     @Test
     public void testUpstreamsWithMultipleDownstreams() throws Exception {
-        mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+        runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG));
         final long wifiTetheringStartTime = currentTimeMillis();
         incrementCurrentTime(1 * SECOND_IN_MILLIS);
-        mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI));
+        runAndWaitForIdle(() ->
+                mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
         final long wifiUpstreamStartTime = currentTimeMillis();
         incrementCurrentTime(5 * SECOND_IN_MILLIS);
-        mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG);
+        runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG));
         final long usbTetheringStartTime = currentTimeMillis();
         incrementCurrentTime(5 * SECOND_IN_MILLIS);
         updateErrorAndSendReport(TETHERING_USB, TETHER_ERROR_NO_ERROR);
 
         UpstreamEvents.Builder usbTetheringUpstreamEvents = UpstreamEvents.newBuilder();
-        addUpstreamEvent(usbTetheringUpstreamEvents, UpstreamType.UT_WIFI,
+        addUpstreamEvent(usbTetheringUpstreamEvents, UT_WIFI,
                 currentTimeMillis() - usbTetheringStartTime, 0L, 0L);
         verifyReport(DownstreamType.DS_TETHERING_USB, ErrorCode.EC_NO_ERROR,
                 UserType.USER_SYSTEMUI, usbTetheringUpstreamEvents,
@@ -356,7 +419,7 @@
         updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
 
         UpstreamEvents.Builder wifiTetheringUpstreamEvents = UpstreamEvents.newBuilder();
-        addUpstreamEvent(wifiTetheringUpstreamEvents, UpstreamType.UT_WIFI,
+        addUpstreamEvent(wifiTetheringUpstreamEvents, UT_WIFI,
                 currentTimeMillis() - wifiUpstreamStartTime, 0L, 0L);
         verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
                 UserType.USER_SETTINGS, wifiTetheringUpstreamEvents,
@@ -365,24 +428,27 @@
 
     @Test
     public void testSwitchingMultiUpstreams() throws Exception {
-        mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+        runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG));
         final long wifiTetheringStartTime = currentTimeMillis();
         incrementCurrentTime(1 * SECOND_IN_MILLIS);
-        mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI));
+        runAndWaitForIdle(() ->
+                mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
         final long wifiDuration = 5 * SECOND_IN_MILLIS;
         incrementCurrentTime(wifiDuration);
-        mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_BLUETOOTH));
+        runAndWaitForIdle(() ->
+                mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_BLUETOOTH)));
         final long bluetoothDuration = 15 * SECOND_IN_MILLIS;
         incrementCurrentTime(bluetoothDuration);
-        mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_CELLULAR));
+        runAndWaitForIdle(() ->
+                mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_CELLULAR)));
         final long celltoothDuration = 20 * SECOND_IN_MILLIS;
         incrementCurrentTime(celltoothDuration);
         updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
 
         UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
-        addUpstreamEvent(upstreamEvents, UpstreamType.UT_WIFI, wifiDuration, 0L, 0L);
-        addUpstreamEvent(upstreamEvents, UpstreamType.UT_BLUETOOTH, bluetoothDuration, 0L, 0L);
-        addUpstreamEvent(upstreamEvents, UpstreamType.UT_CELLULAR, celltoothDuration, 0L, 0L);
+        addUpstreamEvent(upstreamEvents, UT_WIFI, wifiDuration, 0L, 0L);
+        addUpstreamEvent(upstreamEvents, UT_BLUETOOTH, bluetoothDuration, 0L, 0L);
+        addUpstreamEvent(upstreamEvents, UT_CELLULAR, celltoothDuration, 0L, 0L);
 
         verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
                 UserType.USER_SETTINGS, upstreamEvents,
@@ -397,10 +463,10 @@
 
     @Test
     public void testUsageSupportedForUpstreamTypeTest() {
-        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_CELLULAR, true /* isSupported */);
-        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_WIFI, true /* isSupported */);
-        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_BLUETOOTH, true /* isSupported */);
-        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_ETHERNET, true /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UT_CELLULAR, true /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UT_WIFI, true /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UT_BLUETOOTH, true /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UT_ETHERNET, true /* isSupported */);
         runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_WIFI_AWARE, false /* isSupported */);
         runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_LOWPAN, false /* isSupported */);
         runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_UNKNOWN, false /* isSupported */);
@@ -420,12 +486,138 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testBuildNetworkTemplateForUpstreamType() {
-        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_CELLULAR, MATCH_MOBILE);
-        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_WIFI, MATCH_WIFI);
-        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_BLUETOOTH, MATCH_BLUETOOTH);
-        runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_ETHERNET, MATCH_ETHERNET);
+        runBuildNetworkTemplateForUpstreamType(UT_CELLULAR, MATCH_MOBILE);
+        runBuildNetworkTemplateForUpstreamType(UT_WIFI, MATCH_WIFI);
+        runBuildNetworkTemplateForUpstreamType(UT_BLUETOOTH, MATCH_BLUETOOTH);
+        runBuildNetworkTemplateForUpstreamType(UT_ETHERNET, MATCH_ETHERNET);
         runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_WIFI_AWARE, MATCH_NONE);
         runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_LOWPAN, MATCH_NONE);
         runBuildNetworkTemplateForUpstreamType(UpstreamType.UT_UNKNOWN, MATCH_NONE);
     }
+
+    private void verifyEmptyUsageForAllUpstreamTypes() {
+        mHandler.post(() -> {
+            for (UpstreamType type : UpstreamType.values()) {
+                assertEquals(EMPTY, mTetheringMetrics.getDataUsageFromUpstreamType(type));
+            }
+        });
+        HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+    }
+
+    @Test
+    public void testInitializeUpstreamDataUsageBeforeT() {
+        // Verify the usage is empty for all upstream types before initialization.
+        verifyEmptyUsageForAllUpstreamTypes();
+
+        // Verify the usage is still empty after initialization if sdk is lower than T.
+        doReturn(false).when(mDeps).isUpstreamDataUsageMetricsEnabled(any());
+        runAndWaitForIdle(() -> mTetheringMetrics.initUpstreamUsageBaseline());
+        verifyEmptyUsageForAllUpstreamTypes();
+    }
+
+    private android.app.usage.NetworkStats makeNetworkStatsWithTxRxBytes(DataUsage dataUsage) {
+        final NetworkStats testAndroidNetStats =
+                new NetworkStats(0L /* elapsedRealtime */, 1 /* initialSize */).addEntry(
+                        new NetworkStats.Entry("test", 10001, SET_DEFAULT, TAG_NONE,
+                                METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, dataUsage.rxBytes,
+                                10, dataUsage.txBytes, 10, 10));
+        return makePublicStatsFromAndroidNetStats(testAndroidNetStats);
+    }
+
+    private static UpstreamType matchRuleToUpstreamType(int matchRule) {
+        switch (matchRule) {
+            case MATCH_MOBILE:
+                return UT_CELLULAR;
+            case MATCH_WIFI:
+                return UT_WIFI;
+            case MATCH_BLUETOOTH:
+                return UT_BLUETOOTH;
+            case MATCH_ETHERNET:
+                return UT_ETHERNET;
+            default:
+                return UpstreamType.UT_UNKNOWN;
+        }
+    }
+
+    private void initializeUpstreamUsageBaseline() {
+        doReturn(true).when(mDeps).isUpstreamDataUsageMetricsEnabled(any());
+        runAndWaitForIdle(() -> mTetheringMetrics.initUpstreamUsageBaseline());
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testInitUpstreamUsageBaselineAndCleanup() {
+        // Verify the usage is empty for all upstream types before initialization.
+        verifyEmptyUsageForAllUpstreamTypes();
+
+        // Verify the usage has been initialized
+        initializeUpstreamUsageBaseline();
+
+        mHandler.post(() -> {
+            for (UpstreamType type : UpstreamType.values()) {
+                final DataUsage dataUsage = mTetheringMetrics.getDataUsageFromUpstreamType(type);
+                if (TetheringMetrics.isUsageSupportedForUpstreamType(type)) {
+                    assertEquals(mMockUpstreamUsageBaseline.get(type), dataUsage);
+                } else {
+                    assertEquals(EMPTY, dataUsage);
+                }
+            }
+        });
+        HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+
+        // Verify the usage is empty after clean up
+        runAndWaitForIdle(() -> mTetheringMetrics.cleanup());
+        verifyEmptyUsageForAllUpstreamTypes();
+    }
+
+    private void updateUpstreamDataUsage(UpstreamType type, long usageDiff) {
+        final DataUsage oldWifiUsage = mMockUpstreamUsageBaseline.get(type);
+        final DataUsage newWifiUsage = new DataUsage(
+                oldWifiUsage.txBytes + usageDiff,
+                oldWifiUsage.rxBytes + usageDiff);
+        mMockUpstreamUsageBaseline.put(type, newWifiUsage);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testDataUsageCalculation() throws Exception {
+        initializeUpstreamUsageBaseline();
+        runAndWaitForIdle(() -> mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG));
+        final long wifiTetheringStartTime = currentTimeMillis();
+        incrementCurrentTime(1 * SECOND_IN_MILLIS);
+
+        // Change the upstream to Wi-Fi and update the data usage
+        runAndWaitForIdle(() ->
+                mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
+        final long wifiDuration = 5 * SECOND_IN_MILLIS;
+        final long wifiUsageDiff = 100L;
+        incrementCurrentTime(wifiDuration);
+        updateUpstreamDataUsage(UT_WIFI, wifiUsageDiff);
+
+        // Change the upstream to bluetooth and update the data usage
+        runAndWaitForIdle(() ->
+                mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_BLUETOOTH)));
+        final long bluetoothDuration = 15 * SECOND_IN_MILLIS;
+        final long btUsageDiff = 50L;
+        incrementCurrentTime(bluetoothDuration);
+        updateUpstreamDataUsage(UT_BLUETOOTH, btUsageDiff);
+
+        // Change the upstream to cellular and update the data usage
+        runAndWaitForIdle(() ->
+                mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_CELLULAR)));
+        final long cellDuration = 20 * SECOND_IN_MILLIS;
+        final long cellUsageDiff = 500L;
+        incrementCurrentTime(cellDuration);
+        updateUpstreamDataUsage(UT_CELLULAR, cellUsageDiff);
+
+        // Stop tethering and verify that the data usage is uploaded.
+        updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
+        UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
+        addUpstreamEvent(upstreamEvents, UT_WIFI, wifiDuration, wifiUsageDiff, wifiUsageDiff);
+        addUpstreamEvent(upstreamEvents, UT_BLUETOOTH, bluetoothDuration, btUsageDiff, btUsageDiff);
+        addUpstreamEvent(upstreamEvents, UT_CELLULAR, cellDuration, cellUsageDiff, cellUsageDiff);
+        verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
+                UserType.USER_SETTINGS, upstreamEvents,
+                currentTimeMillis() - wifiTetheringStartTime);
+    }
 }
diff --git a/bpf/headers/include/bpf_helpers.h b/bpf/headers/include/bpf_helpers.h
index ca0ca76..ac5ffda 100644
--- a/bpf/headers/include/bpf_helpers.h
+++ b/bpf/headers/include/bpf_helpers.h
@@ -50,18 +50,21 @@
 // Note: this value (and the following +1u's) are hardcoded in NetBpfLoad.cpp
 #define BPFLOADER_MAINLINE_VERSION 42u
 
-// Android Mainline BpfLoader when running on Android T
+// Android Mainline BpfLoader when running on Android T (sdk=33)
 #define BPFLOADER_MAINLINE_T_VERSION (BPFLOADER_MAINLINE_VERSION + 1u)
 
-// Android Mainline BpfLoader when running on Android U
+// Android Mainline BpfLoader when running on Android U (sdk=34)
 #define BPFLOADER_MAINLINE_U_VERSION (BPFLOADER_MAINLINE_T_VERSION + 1u)
 
 // Android Mainline BpfLoader when running on Android U QPR3
 #define BPFLOADER_MAINLINE_U_QPR3_VERSION (BPFLOADER_MAINLINE_U_VERSION + 1u)
 
-// Android Mainline BpfLoader when running on Android V
+// Android Mainline BpfLoader when running on Android V (sdk=35)
 #define BPFLOADER_MAINLINE_V_VERSION (BPFLOADER_MAINLINE_U_QPR3_VERSION + 1u)
 
+// Android Mainline BpfLoader when running on Android W (sdk=36)
+#define BPFLOADER_MAINLINE_W_VERSION (BPFLOADER_MAINLINE_V_VERSION + 1u)
+
 /* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
  * before #include "bpf_helpers.h" to change which bpfloaders will
  * process the resulting .o file.
@@ -288,6 +291,12 @@
         bpf_ringbuf_submit_unsafe(v, 0);                                       \
     }
 
+#define DEFINE_BPF_RINGBUF(the_map, ValueType, size_bytes, usr, grp, md)                \
+    DEFINE_BPF_RINGBUF_EXT(the_map, ValueType, size_bytes, usr, grp, md,                \
+                           DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, \
+                           PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER,               \
+                           LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
 /* There exist buggy kernels with pre-T OS, that due to
  * kernel patch "[ALPS05162612] bpf: fix ubsan error"
  * do not support userspace writes into non-zero index of bpf map arrays.
@@ -346,11 +355,17 @@
 #error "Bpf Map UID must be left at default of AID_ROOT for BpfLoader prior to v0.28"
 #endif
 
-#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md)     \
-    DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md,         \
-                       DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, PRIVATE, \
-                       BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, LOAD_ON_ENG,                    \
-                       LOAD_ON_USER, LOAD_ON_USERDEBUG)
+// for maps not meant to be accessed from userspace
+#define DEFINE_BPF_MAP_KERNEL_INTERNAL(the_map, TYPE, KeyType, ValueType, num_entries)           \
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, AID_ROOT, AID_ROOT,       \
+                       0000, "fs_bpf_loader", "", PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
+                       LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
+#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md,     \
+                       DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR,      \
+                       PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER,                    \
+                       LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
 
 #define DEFINE_BPF_MAP(the_map, TYPE, KeyType, ValueType, num_entries) \
     DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
@@ -389,6 +404,7 @@
 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;
 
+// GPL only:
 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);
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index c058433..4767dfa 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -60,7 +60,14 @@
 #include "bpf_map_def.h"
 
 using android::base::EndsWith;
+using android::base::GetIntProperty;
+using android::base::GetProperty;
+using android::base::InitLogging;
+using android::base::KernelLogger;
+using android::base::SetProperty;
+using android::base::Split;
 using android::base::StartsWith;
+using android::base::Tokenize;
 using android::base::unique_fd;
 using std::ifstream;
 using std::ios;
@@ -90,6 +97,8 @@
     net_shared,         // (T+) fs_bpf_net_shared    /sys/fs/bpf/net_shared
     netd_readonly,      // (T+) fs_bpf_netd_readonly /sys/fs/bpf/netd_readonly
     netd_shared,        // (T+) fs_bpf_netd_shared   /sys/fs/bpf/netd_shared
+    loader,             // (U+) fs_bpf_loader        /sys/fs/bpf/loader
+                        // on T due to lack of sepolicy/genfscon rules it behaves simply as 'fs_bpf'
 };
 
 static constexpr domain AllDomains[] = {
@@ -99,6 +108,7 @@
     domain::net_shared,
     domain::netd_readonly,
     domain::netd_shared,
+    domain::loader,
 };
 
 static constexpr bool specified(domain d) {
@@ -112,7 +122,7 @@
 
 // Returns the build type string (from ro.build.type).
 const std::string& getBuildType() {
-    static std::string t = android::base::GetProperty("ro.build.type", "unknown");
+    static std::string t = GetProperty("ro.build.type", "unknown");
     return t;
 }
 
@@ -144,6 +154,7 @@
         case domain::net_shared:    return "fs_bpf_net_shared";
         case domain::netd_readonly: return "fs_bpf_netd_readonly";
         case domain::netd_shared:   return "fs_bpf_netd_shared";
+        case domain::loader:        return "fs_bpf_loader";
     }
 }
 
@@ -167,6 +178,7 @@
         case domain::net_shared:    return "net_shared/";
         case domain::netd_readonly: return "netd_readonly/";
         case domain::netd_shared:   return "netd_shared/";
+        case domain::loader:        return "loader/";
     }
 };
 
@@ -184,7 +196,7 @@
 
 static string pathToObjName(const string& path) {
     // extract everything after the final slash, ie. this is the filename 'foo@1.o' or 'bar.o'
-    string filename = android::base::Split(path, "/").back();
+    string filename = Split(path, "/").back();
     // strip off everything from the final period onwards (strip '.o' suffix), ie. 'foo@1' or 'bar'
     string name = filename.substr(0, filename.find_last_of('.'));
     // strip any potential @1 suffix, this will leave us with just 'foo' or 'bar'
@@ -1016,7 +1028,7 @@
 
             if (!fd.ok()) {
                 if (log_buf.size()) {
-                    vector<string> lines = android::base::Split(log_buf.data(), "\n");
+                    vector<string> lines = Split(log_buf.data(), "\n");
 
                     ALOGW("BPF_PROG_LOAD - BEGIN log_buf contents:");
                     for (const auto& line : lines) ALOGW("%s", line.c_str());
@@ -1247,7 +1259,7 @@
 // to include a newline to match 'echo "value" > /proc/sys/...foo' behaviour,
 // which is usually how kernel devs test the actual sysctl interfaces.
 static int writeProcSysFile(const char *filename, const char *value) {
-    base::unique_fd fd(open(filename, O_WRONLY | O_CLOEXEC));
+    unique_fd fd(open(filename, O_WRONLY | O_CLOEXEC));
     if (fd < 0) {
         const int err = errno;
         ALOGE("open('%s', O_WRONLY | O_CLOEXEC) -> %s", filename, strerror(err));
@@ -1324,7 +1336,7 @@
 }
 
 static bool hasGSM() {
-    static string ph = base::GetProperty("gsm.current.phone-type", "");
+    static string ph = GetProperty("gsm.current.phone-type", "");
     static bool gsm = (ph != "");
     static bool logged = false;
     if (!logged) {
@@ -1337,7 +1349,7 @@
 static bool isTV() {
     if (hasGSM()) return false;  // TVs don't do GSM
 
-    static string key = base::GetProperty("ro.oem.key1", "");
+    static string key = GetProperty("ro.oem.key1", "");
     static bool tv = StartsWith(key, "ATV00");
     static bool logged = false;
     if (!logged) {
@@ -1348,10 +1360,10 @@
 }
 
 static bool isWear() {
-    static string wearSdkStr = base::GetProperty("ro.cw_build.wear_sdk.version", "");
-    static int wearSdkInt = base::GetIntProperty("ro.cw_build.wear_sdk.version", 0);
-    static string buildChars = base::GetProperty("ro.build.characteristics", "");
-    static vector<string> v = base::Tokenize(buildChars, ",");
+    static string wearSdkStr = GetProperty("ro.cw_build.wear_sdk.version", "");
+    static int wearSdkInt = GetIntProperty("ro.cw_build.wear_sdk.version", 0);
+    static string buildChars = GetProperty("ro.build.characteristics", "");
+    static vector<string> v = Tokenize(buildChars, ",");
     static bool watch = (std::find(v.begin(), v.end(), "watch") != v.end());
     static bool wear = (wearSdkInt > 0) || watch;
     static bool logged = false;
@@ -1368,7 +1380,7 @@
 
     // Any released device will have codename REL instead of a 'real' codename.
     // For safety: default to 'REL' so we default to unreleased=false on failure.
-    const bool unreleased = (base::GetProperty("ro.build.version.codename", "REL") != "REL");
+    const bool unreleased = (GetProperty("ro.build.version.codename", "REL") != "REL");
 
     // goog/main device_api_level is bumped *way* before aosp/main api level
     // (the latter only gets bumped during the push of goog/main to aosp/main)
@@ -1397,6 +1409,9 @@
     const bool isAtLeastT = (effective_api_level >= __ANDROID_API_T__);
     const bool isAtLeastU = (effective_api_level >= __ANDROID_API_U__);
     const bool isAtLeastV = (effective_api_level >= __ANDROID_API_V__);
+    const bool isAtLeastW = (effective_api_level >  __ANDROID_API_V__);  // TODO: switch to W
+
+    const int first_api_level = GetIntProperty("ro.board.first_api_level", effective_api_level);
 
     // last in U QPR2 beta1
     const bool has_platform_bpfloader_rc = exists("/system/etc/init/bpfloader.rc");
@@ -1409,6 +1424,7 @@
     if (isAtLeastU) ++bpfloader_ver;     // [44] BPFLOADER_MAINLINE_U_VERSION
     if (runningAsRoot) ++bpfloader_ver;  // [45] BPFLOADER_MAINLINE_U_QPR3_VERSION
     if (isAtLeastV) ++bpfloader_ver;     // [46] BPFLOADER_MAINLINE_V_VERSION
+    if (isAtLeastW) ++bpfloader_ver;     // [47] BPFLOADER_MAINLINE_W_VERSION
 
     ALOGI("NetBpfLoad v0.%u (%s) api:%d/%d kver:%07x (%s) uid:%d rc:%d%d",
           bpfloader_ver, argv[0], android_get_device_api_level(), effective_api_level,
@@ -1458,6 +1474,12 @@
         if (!isTV()) return 1;
     }
 
+    // 6.6 is highest version supported by Android V, so this is effectively W+ (sdk=36+)
+    if (isKernel32Bit() && isAtLeastKernelVersion(6, 7, 0)) {
+        ALOGE("Android platform with 32 bit kernel version >= 6.7.0 is unsupported");
+        return 1;
+    }
+
     // Various known ABI layout issues, particularly wrt. bpf and ipsec/xfrm.
     if (isAtLeastV && isKernel32Bit() && isX86()) {
         ALOGE("Android V requires X86 kernel to be 64-bit.");
@@ -1492,33 +1514,54 @@
         }
     }
 
+    /* Android 14/U should only launch on 64-bit kernels
+     *   T launches on 5.10/5.15
+     *   U launches on 5.15/6.1
+     * So >=5.16 implies isKernel64Bit()
+     *
+     * We thus added a test to V VTS which requires 5.16+ devices to use 64-bit kernels.
+     *
+     * Starting with Android V, which is the first to support a post 6.1 Linux Kernel,
+     * we also require 64-bit userspace.
+     *
+     * There are various known issues with 32-bit userspace talking to various
+     * kernel interfaces (especially CAP_NET_ADMIN ones) on a 64-bit kernel.
+     * Some of these have userspace or kernel workarounds/hacks.
+     * Some of them don't...
+     * We're going to be removing the hacks.
+     * (for example "ANDROID: xfrm: remove in_compat_syscall() checks").
+     * Note: this check/enforcement only applies to *system* userspace code,
+     * it does not affect unprivileged apps, the 32-on-64 compatibility
+     * problems are AFAIK limited to various CAP_NET_ADMIN protected interfaces.
+     *
+     * Additionally the 32-bit kernel jit support is poor,
+     * and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
+     */
     if (isUserspace32bit() && isAtLeastKernelVersion(6, 2, 0)) {
-        /* Android 14/U should only launch on 64-bit kernels
-         *   T launches on 5.10/5.15
-         *   U launches on 5.15/6.1
-         * So >=5.16 implies isKernel64Bit()
-         *
-         * We thus added a test to V VTS which requires 5.16+ devices to use 64-bit kernels.
-         *
-         * Starting with Android V, which is the first to support a post 6.1 Linux Kernel,
-         * we also require 64-bit userspace.
-         *
-         * There are various known issues with 32-bit userspace talking to various
-         * kernel interfaces (especially CAP_NET_ADMIN ones) on a 64-bit kernel.
-         * Some of these have userspace or kernel workarounds/hacks.
-         * Some of them don't...
-         * We're going to be removing the hacks.
-         * (for example "ANDROID: xfrm: remove in_compat_syscall() checks").
-         * Note: this check/enforcement only applies to *system* userspace code,
-         * it does not affect unprivileged apps, the 32-on-64 compatibility
-         * problems are AFAIK limited to various CAP_NET_ADMIN protected interfaces.
-         *
-         * Additionally the 32-bit kernel jit support is poor,
-         * and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
-         */
-        ALOGE("64-bit userspace required on 6.2+ kernels.");
-        // Stuff won't work reliably, but exempt TVs & Arm Wear devices
-        if (!isTV() && !(isWear() && isArm())) return 1;
+        // Stuff won't work reliably, but...
+        if (isTV()) {
+            // exempt TVs... they don't really need functional advanced networking
+            ALOGW("[TV] 32-bit userspace unsupported on 6.2+ kernels.");
+        } else if (isWear() && isArm()) {
+            // exempt Arm Wear devices (arm32 ABI is far less problematic than x86-32)
+            ALOGW("[Arm Wear] 32-bit userspace unsupported on 6.2+ kernels.");
+        } else if (first_api_level <= __ANDROID_API_T__ && isArm()) {
+            // also exempt Arm devices upgrading with major kernel rev from T-
+            // might possibly be better for them to run with a newer kernel...
+            ALOGW("[Arm KernelUpRev] 32-bit userspace unsupported on 6.2+ kernels.");
+        } else if (isArm()) {
+            ALOGE("[Arm] 64-bit userspace required on 6.2+ kernels (%d).", first_api_level);
+            return 1;
+        } else { // x86 since RiscV cannot be 32-bit
+            ALOGE("[x86] 64-bit userspace required on 6.2+ kernels.");
+            return 1;
+        }
+    }
+
+    // Note: 6.6 is highest version supported by Android V (sdk=35), so this is for sdk=36+
+    if (isUserspace32bit() && isAtLeastKernelVersion(6, 7, 0)) {
+        ALOGE("64-bit userspace required on 6.7+ kernels.");
+        return 1;
     }
 
     // Ensure we can determine the Android build type.
@@ -1589,7 +1632,7 @@
 
     int key = 1;
     int value = 123;
-    base::unique_fd map(
+    unique_fd map(
             createMap(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 2, 0));
     if (writeToMapEntry(map, &key, &value, BPF_ANY)) {
         ALOGE("Critical kernel bug - failure to write into index 1 of 2 element bpf map array.");
@@ -1621,11 +1664,11 @@
 }  // namespace android
 
 int main(int argc, char** argv, char * const envp[]) {
-    android::base::InitLogging(argv, &android::base::KernelLogger);
+    InitLogging(argv, &KernelLogger);
 
     if (argc == 2 && !strcmp(argv[1], "done")) {
         // we're being re-exec'ed from platform bpfloader to 'finalize' things
-        if (!android::base::SetProperty("bpf.progs_loaded", "1")) {
+        if (!SetProperty("bpf.progs_loaded", "1")) {
             ALOGE("Failed to set bpf.progs_loaded property to 1.");
             return 125;
         }
diff --git a/bpf/progs/bpf_net_helpers.h b/bpf/progs/bpf_net_helpers.h
index a86c3e6..a5664ba 100644
--- a/bpf/progs/bpf_net_helpers.h
+++ b/bpf/progs/bpf_net_helpers.h
@@ -139,6 +139,24 @@
     if (skb->data_end - skb->data < len) bpf_skb_pull_data(skb, len);
 }
 
+// anti-compiler-optimizer no-op: explicitly force full calculation of 'v'
+//
+// The use for this is to force full calculation of a complex arithmetic (likely binary
+// bitops) value, and then check the result only once (thus likely reducing the number
+// of required conditional jump instructions that badly affect bpf verifier runtime)
+//
+// The compiler cannot look into the assembly statement, so it doesn't know it does nothing.
+// Since the statement takes 'v' as both input and output in a register (+r),
+// the compiler must fully calculate the precise value of 'v' before this,
+// and must use the (possibly modified) value of 'v' afterwards (thus cannot
+// do funky optimizations to use partial results from before the asm).
+//
+// As this is not flagged 'volatile' this may still be moved out of a loop,
+// or even entirely optimized out if 'v' is never used afterwards.
+//
+// See: https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
+#define COMPILER_FORCE_CALCULATION(v) asm ("" : "+r" (v))
+
 struct egress_bool { bool egress; };
 #define INGRESS ((struct egress_bool){ .egress = false })
 #define EGRESS ((struct egress_bool){ .egress = true })
diff --git a/bpf/progs/dscpPolicy.c b/bpf/progs/dscpPolicy.c
index 39f2961..de9723d 100644
--- a/bpf/progs/dscpPolicy.c
+++ b/bpf/progs/dscpPolicy.c
@@ -25,8 +25,8 @@
 
 // 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_KERNEL_INTERNAL(socket_policy_cache_map, PERCPU_ARRAY, uint32_t, RuleEntry,
+                               CACHE_MAP_SIZE)
 
 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)
@@ -113,14 +113,30 @@
     // 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) &&
-        v6_equal(dst_ip, existing_rule->dst_ip) &&
-        skb->ifindex == existing_rule->ifindex &&
-        sport == existing_rule->src_port &&
-        dport == existing_rule->dst_port &&
-        protocol == existing_rule->proto) {
-        if (existing_rule->dscp_val < 0) return;
+    if (!existing_rule) return; // impossible
+
+    uint64_t nomatch = 0;
+    nomatch |= v6_not_equal(src_ip, existing_rule->src_ip);
+    nomatch |= v6_not_equal(dst_ip, existing_rule->dst_ip);
+    nomatch |= (skb->ifindex ^ existing_rule->ifindex);
+    nomatch |= (sport ^ existing_rule->src_port);
+    nomatch |= (dport ^ existing_rule->dst_port);
+    nomatch |= (protocol ^ existing_rule->proto);
+    COMPILER_FORCE_CALCULATION(nomatch);
+
+    /*
+     * After the above funky bitwise arithmetic we have 'nomatch == 0' iff
+     *   src_ip == existing_rule->src_ip &&
+     *   dst_ip == existing_rule->dst_ip &&
+     *   skb->ifindex == existing_rule->ifindex &&
+     *   sport == existing_rule->src_port &&
+     *   dport == existing_rule->dst_port &&
+     *   protocol == existing_rule->proto
+     */
+
+    if (!nomatch) {
+        if (existing_rule->dscp_val < 0) return;  // cached no-op
+
         if (ipv4) {
             uint8_t newTos = UPDATE_TOS(existing_rule->dscp_val, tos);
             bpf_l3_csum_replace(skb, l2_header_size + IP4_OFFSET(check), htons(tos), htons(newTos),
@@ -132,7 +148,7 @@
             bpf_skb_store_bytes(skb, l2_header_size, &new_first_be32, sizeof(__be32),
                 BPF_F_RECOMPUTE_CSUM);
         }
-        return;
+        return;  // cached DSCP mutation
     }
 
     // Linear scan ipv4_dscp_policies_map since no stored params match skb.
@@ -187,7 +203,8 @@
         }
     }
 
-    RuleEntry value = {
+    // Update cache with found policy.
+    *existing_rule = (RuleEntry){
         .src_ip = src_ip,
         .dst_ip = dst_ip,
         .ifindex = skb->ifindex,
@@ -197,9 +214,6 @@
         .dscp_val = new_dscp,
     };
 
-    // Update cache with found policy.
-    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.
diff --git a/bpf/progs/dscpPolicy.h b/bpf/progs/dscpPolicy.h
index 6a6b711..413fb0f 100644
--- a/bpf/progs/dscpPolicy.h
+++ b/bpf/progs/dscpPolicy.h
@@ -28,9 +28,6 @@
 #define v6_not_equal(a, b) ((v6_hi_be64(a) ^ v6_hi_be64(b)) \
                           | (v6_lo_be64(a) ^ v6_lo_be64(b)))
 
-// Returns 'a == b' as boolean
-#define v6_equal(a, b) (!v6_not_equal((a), (b)))
-
 typedef struct {
     struct in6_addr src_ip;
     struct in6_addr dst_ip;
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 63a6cd2..1ebc4a3 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -6734,4 +6734,33 @@
             throw e.rethrowFromSystemServer();
         }
     }
+
+    /**
+     * Get the specified ConnectivityService feature status. This method is for test code to check
+     * whether the feature is enabled or not.
+     * Note that tests can not just read DeviceConfig since ConnectivityService reads flag at
+     * startup. For example, it's possible that the current flag value is "disable"(-1) but the
+     * feature is enabled since the flag value was "enable"(1) when ConnectivityService started up.
+     * If the ConnectivityManager needs to check the ConnectivityService feature status for non-test
+     * purpose, define feature in {@link ConnectivityManagerFeature} and use
+     * {@link #isFeatureEnabled} instead.
+     *
+     * @param featureFlag  target flag for feature
+     * @return {@code true} if the feature is enabled, {@code false} if the feature is disabled.
+     * @throws IllegalArgumentException if the flag is invalid
+     *
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_STACK,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+    })
+    public boolean isConnectivityServiceFeatureEnabledForTesting(final String featureFlag) {
+        try {
+            return mService.isConnectivityServiceFeatureEnabledForTesting(featureFlag);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index 988cc92..47b3316 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -262,4 +262,6 @@
     IBinder getRoutingCoordinatorService();
 
     long getEnabledConnectivityManagerFeatures();
+
+    boolean isConnectivityServiceFeatureEnabledForTesting(String featureFlag);
 }
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 9af250b..990f43e 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -14505,4 +14505,14 @@
         }
         return features;
     }
+
+    @Override
+    public boolean isConnectivityServiceFeatureEnabledForTesting(String featureFlag) {
+        switch (featureFlag) {
+            case INGRESS_TO_VPN_ADDRESS_FILTERING:
+                return mIngressToVpnAddressFiltering;
+            default:
+                throw new IllegalArgumentException("Unknown flag: " + featureFlag);
+        }
+    }
 }
diff --git a/service/src/com/android/server/connectivity/DscpPolicyValue.java b/service/src/com/android/server/connectivity/DscpPolicyValue.java
index 7162a4a..a9100ac 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyValue.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyValue.java
@@ -117,8 +117,8 @@
         this.proto = proto != -1 ? proto : 0;
 
         this.dscp = dscp;
-        this.match_src_ip = (this.src46 != EMPTY_ADDRESS_FIELD);
-        this.match_dst_ip = (this.dst46 != EMPTY_ADDRESS_FIELD);
+        this.match_src_ip = (src46 != null);
+        this.match_dst_ip = (dst46 != null);
         this.match_src_port = (srcPort != -1);
         this.match_proto = (proto != -1);
     }
diff --git a/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
index 28c33f3..e4d25cd 100644
--- a/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
+++ b/staticlibs/framework/com/android/net/module/util/LocationPermissionChecker.java
@@ -117,7 +117,11 @@
     @VisibleForTesting(visibility = PRIVATE)
     public @LocationPermissionCheckStatus int checkLocationPermissionInternal(
             String pkgName, @Nullable String featureId, int uid, @Nullable String message) {
-        checkPackage(uid, pkgName);
+        try {
+            checkPackage(uid, pkgName);
+        } catch (SecurityException e) {
+            return ERROR_LOCATION_PERMISSION_MISSING;
+        }
 
         // Apps with NETWORK_SETTINGS, NETWORK_SETUP_WIZARD, NETWORK_STACK & MAINLINE_NETWORK_STACK
         // are granted a bypass.
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 c8f8656..d773374 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
@@ -18,7 +18,6 @@
 import static android.Manifest.permission.NETWORK_SETTINGS;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
@@ -47,7 +46,6 @@
 
 import com.android.testutils.DevSdkIgnoreRule;
 
-import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -242,9 +240,9 @@
         mWifiScanAllowApps = AppOpsManager.MODE_ALLOWED;
         setupTestCase();
 
-        assertThrows(SecurityException.class,
-                () -> mChecker.checkLocationPermissionInternal(
-                        TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null));
+        final int result = mChecker.checkLocationPermissionInternal(
+                        TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
+        assertEquals(LocationPermissionChecker.ERROR_LOCATION_PERMISSION_MISSING, result);
     }
 
     @Test
@@ -305,14 +303,4 @@
                         TEST_PKG_NAME, TEST_FEATURE_ID, mUid, null);
         assertEquals(LocationPermissionChecker.SUCCEEDED, result);
     }
-
-
-    private static void assertThrows(Class<? extends Exception> exceptionClass, Runnable r) {
-        try {
-            r.run();
-            Assert.fail("Expected " + exceptionClass + " to be thrown.");
-        } catch (Exception exception) {
-            assertTrue(exceptionClass.isInstance(exception));
-        }
-    }
 }
diff --git a/tests/cts/hostside/AndroidTest.xml b/tests/cts/hostside/AndroidTest.xml
index ea6b078..03ea178 100644
--- a/tests/cts/hostside/AndroidTest.xml
+++ b/tests/cts/hostside/AndroidTest.xml
@@ -20,6 +20,9 @@
     <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
+    <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk" />
+    <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.tethering.apex" />
 
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.LocationCheck" />
 
@@ -46,4 +49,21 @@
         <option name="directory-keys" value="/sdcard/CtsHostsideNetworkTests" />
         <option name="collect-on-run-ended-only" value="true" />
     </metrics_collector>
+
+<!-- When this test is run in a Mainline context (e.g. with `mts-tradefed`), only enable it if
+    one of the Mainline modules below is present on the device used for testing. -->
+<object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+    <!-- Tethering Module (internal version). -->
+    <option name="mainline-module-package-name" value="com.google.android.tethering" />
+    <!-- Tethering Module (AOSP version). -->
+    <option name="mainline-module-package-name" value="com.android.tethering" />
+    <!-- NetworkStack Module (internal version). Should always be installed with CaptivePortalLogin. -->
+    <option name="mainline-module-package-name" value="com.google.android.networkstack" />
+    <!-- NetworkStack Module (AOSP version). Should always be installed with CaptivePortalLogin. -->
+    <option name="mainline-module-package-name" value="com.android.networkstack" />
+    <!-- Resolver Module (internal version). -->
+    <option name="mainline-module-package-name" value="com.google.android.resolv" />
+    <!-- Resolver Module (AOSP version). -->
+    <option name="mainline-module-package-name" value="com.android.resolv" />
+</object>
 </configuration>
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index e186c6b..2db1db5 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -213,6 +213,8 @@
 
     private static final String AUTOMATIC_ON_OFF_KEEPALIVE_VERSION =
                 "automatic_on_off_keepalive_version";
+    private static final String INGRESS_TO_VPN_ADDRESS_FILTERING =
+            "ingress_to_vpn_address_filtering";
     // Enabled since version 1 means it's always enabled because the version is always above 1
     private static final String AUTOMATIC_ON_OFF_KEEPALIVE_ENABLED = "1";
     private static final long TEST_TCP_POLLING_TIMER_EXPIRED_PERIOD_MS = 60_000L;
@@ -1949,6 +1951,9 @@
      */
     private void doTestDropPacketToVpnAddress(final boolean duplicatedAddress)
             throws Exception {
+        assumeTrue(mCM.isConnectivityServiceFeatureEnabledForTesting(
+                INGRESS_TO_VPN_ADDRESS_FILTERING));
+
         final NetworkRequest request = new NetworkRequest.Builder()
                 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
                 .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
index 12ea23b..3dde3ff 100644
--- a/tests/cts/hostside/app2/Android.bp
+++ b/tests/cts/hostside/app2/Android.bp
@@ -36,4 +36,5 @@
         "sts",
     ],
     sdk_version: "test_current",
+    min_sdk_version: "31",
 }
diff --git a/tests/cts/hostside/networkslicingtestapp/Android.bp b/tests/cts/hostside/networkslicingtestapp/Android.bp
index c220000..333195c 100644
--- a/tests/cts/hostside/networkslicingtestapp/Android.bp
+++ b/tests/cts/hostside/networkslicingtestapp/Android.bp
@@ -35,6 +35,7 @@
         "general-tests",
         "sts",
     ],
+    min_sdk_version: "31",
 }
 
 android_test_helper_app {
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index f73134a..041e6cb 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -298,7 +298,8 @@
     fun sendPacket(
         agent: TestableNetworkAgent,
         sendV6: Boolean,
-        dstPort: Int = 0
+        dstPort: Int = 0,
+        times: Int = 1
     ) {
         val testString = "test string"
         val testPacket = ByteBuffer.wrap(testString.toByteArray(Charsets.UTF_8))
@@ -308,9 +309,11 @@
                 IPPROTO_UDP)
         checkNotNull(agent.network).bindSocket(socket)
 
-        val originalPacket = testPacket.readAsArray()
-        Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size, 0 /* flags */,
+        val origPacket = testPacket.readAsArray()
+        repeat(times) {
+            Os.sendto(socket, origPacket, 0 /* bytesOffset */, origPacket.size, 0 /* flags */,
                 if (sendV6) TEST_TARGET_IPV6_ADDR else TEST_TARGET_IPV4_ADDR, dstPort)
+        }
         Os.close(socket)
     }
 
@@ -400,10 +403,11 @@
         agent: TestableNetworkAgent,
         sendV6: Boolean = false,
         dscpValue: Int = 0,
-        dstPort: Int = 0
+        dstPort: Int = 0,
+        times: Int = 1
     ) {
-        var packetFound = false
-        sendPacket(agent, sendV6, dstPort)
+        var packetFound = 0
+        sendPacket(agent, sendV6, dstPort, times)
         // TODO: grab source port from socket in sendPacket
 
         Log.e(TAG, "find DSCP value:" + dscpValue)
@@ -424,10 +428,23 @@
             if (parsePacketIp(buffer, sendV6) && parsePacketPort(buffer, 0, dstPort)) {
                 Log.e(TAG, "DSCP value found")
                 assertEquals(dscpValue, dscp)
-                packetFound = true
+                packetFound++
             }
         }
-        assertTrue(packetFound)
+        assertTrue(packetFound == times)
+    }
+
+    fun validatePackets(
+        agent: TestableNetworkAgent,
+        sendV6: Boolean = false,
+        dscpValue: Int = 0,
+        dstPort: Int = 0
+    ) {
+        // We send two packets from the same socket to verify
+        // socket caching works correctly.
+        validatePacket(agent, sendV6, dscpValue, dstPort, 2)
+        // Try one more time from a different socket.
+        validatePacket(agent, sendV6, dscpValue, dstPort, 1)
     }
 
     fun doRemovePolicyTest(
@@ -453,10 +470,7 @@
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
         }
-        validatePacket(agent, dscpValue = 1, dstPort = 4444)
-        // Send a second packet to validate that the stored BPF policy
-        // is correct for subsequent packets.
-        validatePacket(agent, dscpValue = 1, dstPort = 4444)
+        validatePackets(agent, dscpValue = 1, dstPort = 4444)
 
         agent.sendRemoveDscpPolicy(1)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -475,7 +489,7 @@
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
         }
 
-        validatePacket(agent, dscpValue = 4, dstPort = 5555)
+        validatePackets(agent, dscpValue = 4, dstPort = 5555)
 
         agent.sendRemoveDscpPolicy(1)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -494,10 +508,7 @@
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
         }
-        validatePacket(agent, true, dscpValue = 1, dstPort = 4444)
-        // Send a second packet to validate that the stored BPF policy
-        // is correct for subsequent packets.
-        validatePacket(agent, true, dscpValue = 1, dstPort = 4444)
+        validatePackets(agent, true, dscpValue = 1, dstPort = 4444)
 
         agent.sendRemoveDscpPolicy(1)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -515,7 +526,7 @@
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
         }
-        validatePacket(agent, true, dscpValue = 4, dstPort = 5555)
+        validatePackets(agent, true, dscpValue = 4, dstPort = 5555)
 
         agent.sendRemoveDscpPolicy(1)
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -533,7 +544,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            validatePacket(agent, dscpValue = 1, dstPort = 1111)
+            validatePackets(agent, dscpValue = 1, dstPort = 1111)
         }
 
         val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
@@ -541,7 +552,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            validatePacket(agent, dscpValue = 1, dstPort = 2222)
+            validatePackets(agent, dscpValue = 1, dstPort = 2222)
         }
 
         val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
@@ -549,16 +560,16 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            validatePacket(agent, dscpValue = 1, dstPort = 3333)
+            validatePackets(agent, dscpValue = 1, dstPort = 3333)
         }
 
         /* Remove Policies and check CE is no longer set */
         doRemovePolicyTest(agent, callback, 1)
-        validatePacket(agent, dscpValue = 0, dstPort = 1111)
+        validatePackets(agent, dscpValue = 0, dstPort = 1111)
         doRemovePolicyTest(agent, callback, 2)
-        validatePacket(agent, dscpValue = 0, dstPort = 2222)
+        validatePackets(agent, dscpValue = 0, dstPort = 2222)
         doRemovePolicyTest(agent, callback, 3)
-        validatePacket(agent, dscpValue = 0, dstPort = 3333)
+        validatePackets(agent, dscpValue = 0, dstPort = 3333)
     }
 
     @Test
@@ -569,7 +580,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            validatePacket(agent, dscpValue = 1, dstPort = 1111)
+            validatePackets(agent, dscpValue = 1, dstPort = 1111)
         }
         doRemovePolicyTest(agent, callback, 1)
 
@@ -578,7 +589,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            validatePacket(agent, dscpValue = 1, dstPort = 2222)
+            validatePackets(agent, dscpValue = 1, dstPort = 2222)
         }
         doRemovePolicyTest(agent, callback, 2)
 
@@ -587,7 +598,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            validatePacket(agent, dscpValue = 1, dstPort = 3333)
+            validatePackets(agent, dscpValue = 1, dstPort = 3333)
         }
         doRemovePolicyTest(agent, callback, 3)
     }
@@ -601,7 +612,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            validatePacket(agent, dscpValue = 1, dstPort = 1111)
+            validatePackets(agent, dscpValue = 1, dstPort = 1111)
         }
 
         val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
@@ -609,7 +620,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            validatePacket(agent, dscpValue = 1, dstPort = 2222)
+            validatePackets(agent, dscpValue = 1, dstPort = 2222)
         }
 
         val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
@@ -617,7 +628,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            validatePacket(agent, dscpValue = 1, dstPort = 3333)
+            validatePackets(agent, dscpValue = 1, dstPort = 3333)
         }
 
         /* Remove Policies and check CE is no longer set */
@@ -643,7 +654,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            validatePacket(agent, dscpValue = 1, dstPort = 1111)
+            validatePackets(agent, dscpValue = 1, dstPort = 1111)
         }
 
         val policy2 = DscpPolicy.Builder(2, 1)
@@ -652,7 +663,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            validatePacket(agent, dscpValue = 1, dstPort = 2222)
+            validatePackets(agent, dscpValue = 1, dstPort = 2222)
         }
 
         val policy3 = DscpPolicy.Builder(3, 1)
@@ -661,24 +672,24 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            validatePacket(agent, dscpValue = 1, dstPort = 3333)
+            validatePackets(agent, dscpValue = 1, dstPort = 3333)
         }
 
         agent.sendRemoveAllDscpPolicies()
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
-            validatePacket(agent, false, dstPort = 1111)
+            validatePackets(agent, false, dstPort = 1111)
         }
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(2, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
-            validatePacket(agent, false, dstPort = 2222)
+            validatePackets(agent, false, dstPort = 2222)
         }
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(3, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
-            validatePacket(agent, false, dstPort = 3333)
+            validatePackets(agent, false, dstPort = 3333)
         }
     }
 
@@ -690,7 +701,7 @@
         agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
             assertEquals(1, it.policyId)
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
-            validatePacket(agent, dscpValue = 1, dstPort = 4444)
+            validatePackets(agent, dscpValue = 1, dstPort = 4444)
         }
 
         val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(5555, 5555)).build()
@@ -700,8 +711,8 @@
             assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
 
             // Sending packet with old policy should fail
-            validatePacket(agent, dscpValue = 0, dstPort = 4444)
-            validatePacket(agent, dscpValue = 1, dstPort = 5555)
+            validatePackets(agent, dscpValue = 0, dstPort = 4444)
+            validatePackets(agent, dscpValue = 1, dstPort = 5555)
         }
 
         agent.sendRemoveDscpPolicy(1)
diff --git a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
index 2a6c638..c480135 100644
--- a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
@@ -23,10 +23,13 @@
 import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA384;
 import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA512;
 import static android.net.IpSecAlgorithm.CRYPT_AES_CBC;
+import static android.net.cts.PacketUtils.ICMP_HDRLEN;
+import static android.net.cts.PacketUtils.IP6_HDRLEN;
 import static android.system.OsConstants.FIONREAD;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
 import android.net.ConnectivityManager;
@@ -152,10 +155,17 @@
         final IpSecTransformState transformState =
                 futureIpSecTransform.get(SOCK_TIMEOUT, TimeUnit.MILLISECONDS);
 
-        assertEquals(txHighestSeqNum, transformState.getTxHighestSequenceNumber());
+        // There might be ICMPv6(Router Solicitation) packets. Thus we can only check the lower
+        // bound of the outgoing traffic.
+        final long icmpV6RsCnt = transformState.getTxHighestSequenceNumber() - txHighestSeqNum;
+        assertTrue(icmpV6RsCnt >= 0);
+
+        final long adjustedPacketCnt = packetCnt + icmpV6RsCnt;
+        final long adjustedByteCnt = byteCnt + icmpV6RsCnt * (IP6_HDRLEN + ICMP_HDRLEN);
+
+        assertEquals(adjustedPacketCnt, transformState.getPacketCount());
+        assertEquals(adjustedByteCnt, transformState.getByteCount());
         assertEquals(rxHighestSeqNum, transformState.getRxHighestSequenceNumber());
-        assertEquals(packetCnt, transformState.getPacketCount());
-        assertEquals(byteCnt, transformState.getByteCount());
         assertArrayEquals(replayBitmap, transformState.getReplayBitmap());
     }
 
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
index 22a51d6..890c071 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
@@ -65,6 +65,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.CollectionUtils;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
@@ -119,7 +120,7 @@
 
     private static final int TIMEOUT_MS = 500;
 
-    private static final int PACKET_COUNT = 5000;
+    private static final int PACKET_COUNT = 100;
 
     // Static state to reduce setup/teardown
     private static ConnectivityManager sCM;
@@ -1088,6 +1089,27 @@
             UdpEncapsulationSocket encapSocket,
             IpSecTunnelTestRunnable test)
             throws Exception {
+        return buildTunnelNetworkAndRunTests(
+                localInner,
+                remoteInner,
+                localOuter,
+                remoteOuter,
+                spi,
+                encapSocket,
+                test,
+                true /* enableEncrypt */);
+    }
+
+    private int buildTunnelNetworkAndRunTests(
+            InetAddress localInner,
+            InetAddress remoteInner,
+            InetAddress localOuter,
+            InetAddress remoteOuter,
+            int spi,
+            UdpEncapsulationSocket encapSocket,
+            IpSecTunnelTestRunnable test,
+            boolean enableEncrypt)
+            throws Exception {
         int innerPrefixLen = localInner instanceof Inet6Address ? IP6_PREFIX_LEN : IP4_PREFIX_LEN;
         TestNetworkCallback testNetworkCb = null;
         int innerSocketPort;
@@ -1115,8 +1137,12 @@
 
             // Configure Transform parameters
             IpSecTransform.Builder transformBuilder = new IpSecTransform.Builder(sContext);
-            transformBuilder.setEncryption(
-                    new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY));
+
+            if (enableEncrypt) {
+                transformBuilder.setEncryption(
+                        new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY));
+            }
+
             transformBuilder.setAuthentication(
                     new IpSecAlgorithm(
                             IpSecAlgorithm.AUTH_HMAC_SHA256, AUTH_KEY, AUTH_KEY.length * 4));
@@ -1167,8 +1193,8 @@
         return innerSocketPort;
     }
 
-    private int buildTunnelNetworkAndRunTestsSimple(int spi, IpSecTunnelTestRunnable test)
-            throws Exception {
+    private int buildTunnelNetworkAndRunTestsSimple(
+            int spi, IpSecTunnelTestRunnable test, boolean enableEncrypt) throws Exception {
         return buildTunnelNetworkAndRunTests(
                 LOCAL_INNER_6,
                 REMOTE_INNER_6,
@@ -1176,7 +1202,8 @@
                 REMOTE_OUTER_6,
                 spi,
                 null /* encapSocket */,
-                test);
+                test,
+                enableEncrypt);
     }
 
     private static void receiveAndValidatePacket(JavaUdpSocket socket) throws Exception {
@@ -1787,10 +1814,11 @@
                             PACKET_COUNT,
                             PACKET_COUNT,
                             PACKET_COUNT * (long) innerPacketSize,
-                            newReplayBitmap(REPLAY_BITMAP_LEN_BYTE * 8));
+                            newReplayBitmap(PACKET_COUNT));
 
                     return innerSocketPort;
-                });
+                },
+                true /* enableEncrypt */);
     }
 
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@@ -1814,17 +1842,22 @@
                     ipsecNetwork.bindSocket(outSocket.mSocket);
                     int innerSocketPort = outSocket.getPort();
 
-                    int expectedPacketSize =
-                            getPacketSize(
-                                    AF_INET6,
-                                    AF_INET6,
-                                    false /* useEncap */,
-                                    false /* transportInTunnelMode */);
+                    int outSeqNum = 1;
+                    int receivedTestDataEspCnt = 0;
 
-                    for (int i = 0; i < PACKET_COUNT; i++) {
+                    while (receivedTestDataEspCnt < PACKET_COUNT) {
                         outSocket.sendTo(TEST_DATA, REMOTE_INNER_6, innerSocketPort);
-                        tunUtils.awaitEspPacketNoPlaintext(
-                                spi, TEST_DATA, false /* useEncap */, expectedPacketSize);
+
+                        byte[] pkt = null;
+
+                        // If it is an ESP that contains the TEST_DATA, move to the next
+                        // loop. Otherwise, the ESP may contain an ICMPv6(Router Solicitation).
+                        // In this case, just increase the expected sequence number and continue
+                        // waiting for the ESP with TEST_DATA
+                        do {
+                            pkt = tunUtils.awaitEspPacket(spi, false /* useEncap */, outSeqNum++);
+                        } while (CollectionUtils.indexOfSubArray(pkt, TEST_DATA) == -1);
+                        receivedTestDataEspCnt++;
                     }
 
                     final int innerPacketSize =
@@ -1838,6 +1871,7 @@
                             newReplayBitmap(0));
 
                     return innerSocketPort;
-                });
+                },
+                false /* enableEncrypt */);
     }
 }
diff --git a/tests/cts/net/src/android/net/cts/PacketUtils.java b/tests/cts/net/src/android/net/cts/PacketUtils.java
index 4d924d1..1588835 100644
--- a/tests/cts/net/src/android/net/cts/PacketUtils.java
+++ b/tests/cts/net/src/android/net/cts/PacketUtils.java
@@ -49,6 +49,7 @@
     static final int TCP_HDRLEN = 20;
     static final int TCP_HDRLEN_WITH_TIMESTAMP_OPT = TCP_HDRLEN + 12;
     static final int ESP_HDRLEN = 8;
+    static final int ICMP_HDRLEN = 8;
     static final int ESP_BLK_SIZE = 4; // ESP has to be 4-byte aligned
     static final int ESP_TRAILER_LEN = 2;
 
diff --git a/tests/cts/net/src/android/net/cts/TunUtils.java b/tests/cts/net/src/android/net/cts/TunUtils.java
index 268d8d2..8bf4998 100644
--- a/tests/cts/net/src/android/net/cts/TunUtils.java
+++ b/tests/cts/net/src/android/net/cts/TunUtils.java
@@ -47,6 +47,8 @@
     protected static final int IP4_PROTO_OFFSET = 9;
     protected static final int IP6_PROTO_OFFSET = 6;
 
+    private static final int SEQ_NUM_MATCH_NOT_REQUIRED = -1;
+
     private static final int DATA_BUFFER_LEN = 4096;
     private static final int TIMEOUT = 2000;
 
@@ -146,16 +148,30 @@
         return espPkt; // We've found the packet we're looking for.
     }
 
+    /** Await the expected ESP packet */
     public byte[] awaitEspPacket(int spi, boolean useEncap) throws Exception {
-        return awaitPacket((pkt) -> isEsp(pkt, spi, useEncap));
+        return awaitEspPacket(spi, useEncap, SEQ_NUM_MATCH_NOT_REQUIRED);
     }
 
-    private static boolean isSpiEqual(byte[] pkt, int espOffset, int spi) {
+    /** Await the expected ESP packet with a matching sequence number */
+    public byte[] awaitEspPacket(int spi, boolean useEncap, int seqNum) throws Exception {
+        return awaitPacket((pkt) -> isEsp(pkt, spi, seqNum, useEncap));
+    }
+
+    private static boolean isMatchingEspPacket(byte[] pkt, int espOffset, int spi, int seqNum) {
         ByteBuffer buffer = ByteBuffer.wrap(pkt);
         buffer.get(new byte[espOffset]); // Skip IP, UDP header
         int actualSpi = buffer.getInt();
+        int actualSeqNum = buffer.getInt();
 
-        return actualSpi == spi;
+        if (actualSeqNum < 0) {
+            throw new UnsupportedOperationException(
+                    "actualSeqNum overflowed and needs to be converted to an unsigned integer");
+        }
+
+        boolean isSeqNumMatched = (seqNum == SEQ_NUM_MATCH_NOT_REQUIRED || seqNum == actualSeqNum);
+
+        return actualSpi == spi && isSeqNumMatched;
     }
 
     /**
@@ -173,29 +189,32 @@
             fail("Banned plaintext packet found");
         }
 
-        return isEsp(pkt, spi, encap);
+        return isEsp(pkt, spi, SEQ_NUM_MATCH_NOT_REQUIRED, encap);
     }
 
-    private static boolean isEsp(byte[] pkt, int spi, boolean encap) {
+    private static boolean isEsp(byte[] pkt, int spi, int seqNum, boolean encap) {
         if (isIpv6(pkt)) {
             if (encap) {
                 return pkt[IP6_PROTO_OFFSET] == IPPROTO_UDP
-                        && isSpiEqual(pkt, IP6_HDRLEN + UDP_HDRLEN, spi);
+                        && isMatchingEspPacket(pkt, IP6_HDRLEN + UDP_HDRLEN, spi, seqNum);
             } else {
-                return pkt[IP6_PROTO_OFFSET] == IPPROTO_ESP && isSpiEqual(pkt, IP6_HDRLEN, spi);
+                return pkt[IP6_PROTO_OFFSET] == IPPROTO_ESP
+                        && isMatchingEspPacket(pkt, IP6_HDRLEN, spi, seqNum);
             }
 
         } else {
             // Use default IPv4 header length (assuming no options)
             if (encap) {
                 return pkt[IP4_PROTO_OFFSET] == IPPROTO_UDP
-                        && isSpiEqual(pkt, IP4_HDRLEN + UDP_HDRLEN, spi);
+                        && isMatchingEspPacket(pkt, IP4_HDRLEN + UDP_HDRLEN, spi, seqNum);
             } else {
-                return pkt[IP4_PROTO_OFFSET] == IPPROTO_ESP && isSpiEqual(pkt, IP4_HDRLEN, spi);
+                return pkt[IP4_PROTO_OFFSET] == IPPROTO_ESP
+                        && isMatchingEspPacket(pkt, IP4_HDRLEN, spi, seqNum);
             }
         }
     }
 
+
     public static boolean isIpv6(byte[] pkt) {
         // First nibble shows IP version. 0x60 for IPv6
         return (pkt[0] & (byte) 0xF0) == (byte) 0x60;
diff --git a/tests/unit/java/android/net/IpMemoryStoreTest.java b/tests/unit/java/android/net/IpMemoryStoreTest.java
index 0b82759..e8f91e6 100644
--- a/tests/unit/java/android/net/IpMemoryStoreTest.java
+++ b/tests/unit/java/android/net/IpMemoryStoreTest.java
@@ -16,6 +16,11 @@
 
 package android.net;
 
+import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM;
+import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_CONFIRM;
+import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC;
+import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
@@ -68,6 +73,14 @@
             -128, 0, 89, 112, 91, -34 };
     private static final NetworkAttributes TEST_NETWORK_ATTRIBUTES = buildTestNetworkAttributes(
             "hint", 219);
+    private static final long ONE_WEEK_IN_MS = 7 * 24 * 3600 * 1000;
+    private static final long ONE_DAY_IN_MS = 24 * 3600 * 1000;
+    private static final int[] NETWORK_EVENT_NUD_FAILURES = new int[] {
+        NETWORK_EVENT_NUD_FAILURE_ROAM,
+        NETWORK_EVENT_NUD_FAILURE_CONFIRM,
+        NETWORK_EVENT_NUD_FAILURE_ORGANIC,
+        NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED
+    };
 
     @Mock
     Context mMockContext;
@@ -333,4 +346,31 @@
         mStore.factoryReset();
         verify(mMockService, times(1)).factoryReset();
     }
+
+    @Test
+    public void testNetworkEvents() throws Exception {
+        startIpMemoryStore(true /* supplyService */);
+        final String cluster = "cluster";
+
+        final long now = System.currentTimeMillis();
+        final long expiry = now + ONE_WEEK_IN_MS;
+        mStore.storeNetworkEvent(cluster, now, expiry, NETWORK_EVENT_NUD_FAILURE_ROAM,
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
+        verify(mMockService, times(1)).storeNetworkEvent(eq(cluster),
+                eq(now), eq(expiry), eq(NETWORK_EVENT_NUD_FAILURE_ROAM), any());
+
+        final long[] sinceTimes = new long[2];
+        sinceTimes[0] = now - ONE_WEEK_IN_MS;
+        sinceTimes[1] = now - ONE_DAY_IN_MS;
+        mStore.retrieveNetworkEventCount(cluster, sinceTimes, NETWORK_EVENT_NUD_FAILURES,
+                (status, counts) -> {
+                    assertTrue("Retrieve network event counts not successful : "
+                            + status.resultCode, status.isSuccess());
+                    assertEquals(new int[0], counts);
+                });
+
+        verify(mMockService, times(1)).retrieveNetworkEventCount(eq(cluster), eq(sinceTimes),
+                eq(NETWORK_EVENT_NUD_FAILURES), any());
+    }
 }
diff --git a/thread/apex/Android.bp b/thread/apex/Android.bp
index edf000a..838c0d9 100644
--- a/thread/apex/Android.bp
+++ b/thread/apex/Android.bp
@@ -23,8 +23,8 @@
 // See https://android.googlesource.com/platform/system/core/+/HEAD/init/README.md#versioned-rc-files-within-apexs
 // for details of versioned rc files.
 prebuilt_etc {
-    name: "ot-daemon.init.34rc",
+    name: "ot-daemon.34rc",
     src: "ot-daemon.34rc",
-    filename: "init.34rc",
+    filename: "ot-daemon.34rc",
     installable: false,
 }
diff --git a/thread/service/java/com/android/server/thread/NsdPublisher.java b/thread/service/java/com/android/server/thread/NsdPublisher.java
index 1447ff8..8d89e13 100644
--- a/thread/service/java/com/android/server/thread/NsdPublisher.java
+++ b/thread/service/java/com/android/server/thread/NsdPublisher.java
@@ -31,10 +31,10 @@
 import android.os.Handler;
 import android.os.RemoteException;
 import android.text.TextUtils;
-import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
 import com.android.server.thread.openthread.DnsTxtAttribute;
 import com.android.server.thread.openthread.INsdDiscoverServiceCallback;
 import com.android.server.thread.openthread.INsdPublisher;
@@ -62,6 +62,7 @@
  */
 public final class NsdPublisher extends INsdPublisher.Stub {
     private static final String TAG = NsdPublisher.class.getSimpleName();
+    private static final SharedLog LOG = ThreadNetworkLogger.forSubComponent(TAG);
 
     // TODO: b/321883491 - specify network for mDNS operations
     @Nullable private Network mNetwork;
@@ -158,8 +159,7 @@
             int listenerId,
             String registrationType) {
         checkOnHandlerThread();
-        Log.i(
-                TAG,
+        LOG.i(
                 "Registering "
                         + registrationType
                         + ". Listener ID: "
@@ -171,7 +171,7 @@
         try {
             mNsdManager.registerService(serviceInfo, PROTOCOL_DNS_SD, mExecutor, listener);
         } catch (IllegalArgumentException e) {
-            Log.i(TAG, "Failed to register service. serviceInfo: " + serviceInfo, e);
+            LOG.e("Failed to register service. serviceInfo: " + serviceInfo, e);
             listener.onRegistrationFailed(serviceInfo, NsdManager.FAILURE_INTERNAL_ERROR);
         }
     }
@@ -184,8 +184,7 @@
         checkOnHandlerThread();
         RegistrationListener registrationListener = mRegistrationListeners.get(listenerId);
         if (registrationListener == null) {
-            Log.w(
-                    TAG,
+            LOG.w(
                     "Failed to unregister service."
                             + " Listener ID: "
                             + listenerId
@@ -193,8 +192,7 @@
 
             return;
         }
-        Log.i(
-                TAG,
+        LOG.i(
                 "Unregistering service."
                         + " Listener ID: "
                         + listenerId
@@ -212,13 +210,7 @@
     private void discoverServiceInternal(
             String type, INsdDiscoverServiceCallback callback, int listenerId) {
         checkOnHandlerThread();
-        Log.i(
-                TAG,
-                "Discovering services."
-                        + " Listener ID: "
-                        + listenerId
-                        + ", service type: "
-                        + type);
+        LOG.i("Discovering services." + " Listener ID: " + listenerId + ", service type: " + type);
 
         DiscoveryListener listener = new DiscoveryListener(listenerId, type, callback);
         mDiscoveryListeners.append(listenerId, listener);
@@ -237,15 +229,14 @@
 
         DiscoveryListener listener = mDiscoveryListeners.get(listenerId);
         if (listener == null) {
-            Log.w(
-                    TAG,
+            LOG.w(
                     "Failed to stop service discovery. Listener ID "
                             + listenerId
                             + ". The listener is null.");
             return;
         }
 
-        Log.i(TAG, "Stopping service discovery. Listener: " + listener);
+        LOG.i("Stopping service discovery. Listener: " + listener);
         mNsdManager.stopServiceDiscovery(listener);
     }
 
@@ -263,8 +254,7 @@
         serviceInfo.setServiceName(name);
         serviceInfo.setServiceType(type);
         serviceInfo.setNetwork(null);
-        Log.i(
-                TAG,
+        LOG.i(
                 "Resolving service."
                         + " Listener ID: "
                         + listenerId
@@ -288,21 +278,19 @@
 
         ServiceInfoListener listener = mServiceInfoListeners.get(listenerId);
         if (listener == null) {
-            Log.w(
-                    TAG,
+            LOG.w(
                     "Failed to stop service resolution. Listener ID: "
                             + listenerId
                             + ". The listener is null.");
             return;
         }
 
-        Log.i(TAG, "Stopping service resolution. Listener: " + listener);
+        LOG.i("Stopping service resolution. Listener: " + listener);
 
         try {
             mNsdManager.unregisterServiceInfoCallback(listener);
         } catch (IllegalArgumentException e) {
-            Log.w(
-                    TAG,
+            LOG.w(
                     "Failed to stop the service resolution because it's already stopped. Listener: "
                             + listener);
         }
@@ -330,7 +318,7 @@
                 listener);
         mHostInfoListeners.append(listenerId, listener);
 
-        Log.i(TAG, "Resolving host." + " Listener ID: " + listenerId + ", hostname: " + name);
+        LOG.i("Resolving host." + " Listener ID: " + listenerId + ", hostname: " + name);
     }
 
     @Override
@@ -343,14 +331,13 @@
 
         HostInfoListener listener = mHostInfoListeners.get(listenerId);
         if (listener == null) {
-            Log.w(
-                    TAG,
+            LOG.w(
                     "Failed to stop host resolution. Listener ID: "
                             + listenerId
                             + ". The listener is null.");
             return;
         }
-        Log.i(TAG, "Stopping host resolution. Listener: " + listener);
+        LOG.i("Stopping host resolution. Listener: " + listener);
         listener.cancel();
         mHostInfoListeners.remove(listenerId);
     }
@@ -373,14 +360,14 @@
             try {
                 mNsdManager.unregisterService(mRegistrationListeners.valueAt(i));
             } catch (IllegalArgumentException e) {
-                Log.i(
-                        TAG,
+                LOG.i(
                         "Failed to unregister."
                                 + " Listener ID: "
                                 + mRegistrationListeners.keyAt(i)
                                 + " serviceInfo: "
-                                + mRegistrationListeners.valueAt(i).mServiceInfo,
-                        e);
+                                + mRegistrationListeners.valueAt(i).mServiceInfo
+                                + ", error: "
+                                + e.getMessage());
             }
         }
         mRegistrationListeners.clear();
@@ -415,8 +402,7 @@
         public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
             checkOnHandlerThread();
             mRegistrationListeners.remove(mListenerId);
-            Log.i(
-                    TAG,
+            LOG.i(
                     "Failed to register listener ID: "
                             + mListenerId
                             + " error code: "
@@ -434,8 +420,7 @@
         public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
             checkOnHandlerThread();
             for (INsdStatusReceiver receiver : mUnregistrationReceivers) {
-                Log.i(
-                        TAG,
+                LOG.i(
                         "Failed to unregister."
                                 + "Listener ID: "
                                 + mListenerId
@@ -454,8 +439,7 @@
         @Override
         public void onServiceRegistered(NsdServiceInfo serviceInfo) {
             checkOnHandlerThread();
-            Log.i(
-                    TAG,
+            LOG.i(
                     "Registered successfully. "
                             + "Listener ID: "
                             + mListenerId
@@ -472,8 +456,7 @@
         public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
             checkOnHandlerThread();
             for (INsdStatusReceiver receiver : mUnregistrationReceivers) {
-                Log.i(
-                        TAG,
+                LOG.i(
                         "Unregistered successfully. "
                                 + "Listener ID: "
                                 + mListenerId
@@ -505,8 +488,7 @@
 
         @Override
         public void onStartDiscoveryFailed(String serviceType, int errorCode) {
-            Log.e(
-                    TAG,
+            LOG.e(
                     "Failed to start service discovery."
                             + " Error code: "
                             + errorCode
@@ -517,8 +499,7 @@
 
         @Override
         public void onStopDiscoveryFailed(String serviceType, int errorCode) {
-            Log.e(
-                    TAG,
+            LOG.e(
                     "Failed to stop service discovery."
                             + " Error code: "
                             + errorCode
@@ -529,18 +510,18 @@
 
         @Override
         public void onDiscoveryStarted(String serviceType) {
-            Log.i(TAG, "Started service discovery. Listener: " + this);
+            LOG.i("Started service discovery. Listener: " + this);
         }
 
         @Override
         public void onDiscoveryStopped(String serviceType) {
-            Log.i(TAG, "Stopped service discovery. Listener: " + this);
+            LOG.i("Stopped service discovery. Listener: " + this);
             mDiscoveryListeners.remove(mListenerId);
         }
 
         @Override
         public void onServiceFound(NsdServiceInfo serviceInfo) {
-            Log.i(TAG, "Found service: " + serviceInfo);
+            LOG.i("Found service: " + serviceInfo);
             try {
                 mDiscoverServiceCallback.onServiceDiscovered(
                         serviceInfo.getServiceName(), mType, true);
@@ -551,7 +532,7 @@
 
         @Override
         public void onServiceLost(NsdServiceInfo serviceInfo) {
-            Log.i(TAG, "Lost service: " + serviceInfo);
+            LOG.i("Lost service: " + serviceInfo);
             try {
                 mDiscoverServiceCallback.onServiceDiscovered(
                         serviceInfo.getServiceName(), mType, false);
@@ -584,8 +565,7 @@
 
         @Override
         public void onServiceInfoCallbackRegistrationFailed(int errorCode) {
-            Log.e(
-                    TAG,
+            LOG.e(
                     "Failed to register service info callback."
                             + " Listener ID: "
                             + mListenerId
@@ -599,8 +579,7 @@
 
         @Override
         public void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo) {
-            Log.i(
-                    TAG,
+            LOG.i(
                     "Service is resolved. "
                             + " Listener ID: "
                             + mListenerId
@@ -640,7 +619,7 @@
 
         @Override
         public void onServiceInfoCallbackUnregistered() {
-            Log.i(TAG, "The service info callback is unregistered. Listener: " + this);
+            LOG.i("The service info callback is unregistered. Listener: " + this);
             mServiceInfoListeners.remove(mListenerId);
         }
 
@@ -671,8 +650,7 @@
         public void onAnswer(@NonNull List<InetAddress> answerList, int rcode) {
             checkOnHandlerThread();
 
-            Log.i(
-                    TAG,
+            LOG.i(
                     "Host is resolved."
                             + " Listener ID: "
                             + mListenerId
@@ -698,14 +676,14 @@
         public void onError(@NonNull DnsResolver.DnsException error) {
             checkOnHandlerThread();
 
-            Log.i(
-                    TAG,
+            LOG.i(
                     "Failed to resolve host."
                             + " Listener ID: "
                             + mListenerId
                             + ", hostname: "
-                            + mHostname,
-                    error);
+                            + mHostname
+                            + ", error: "
+                            + error.getMessage());
             try {
                 mResolveHostCallback.onHostResolved(mHostname, Collections.emptyList());
             } catch (RemoteException e) {
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 93b2f70..6edaae9 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -112,11 +112,11 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserManager;
-import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.connectivity.resources.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
 import com.android.server.ServiceManagerWrapper;
 import com.android.server.connectivity.ConnectivityResources;
 import com.android.server.thread.openthread.BackboneRouterState;
@@ -160,7 +160,8 @@
  */
 @TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
 final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
-    private static final String TAG = "ThreadNetworkService";
+    private static final String TAG = "ControllerService";
+    private static final SharedLog LOG = ThreadNetworkLogger.forSubComponent(TAG);
 
     // The model name length in utf-8 bytes
     private static final int MAX_MODEL_NAME_UTF8_BYTES = 24;
@@ -303,12 +304,12 @@
             return;
         }
 
-        Log.i(TAG, "Starting OT daemon...");
+        LOG.i("Starting OT daemon...");
 
         try {
             getOtDaemon();
         } catch (RemoteException e) {
-            Log.e(TAG, "Failed to initialize ot-daemon", e);
+            LOG.e("Failed to initialize ot-daemon", e);
         } catch (ThreadNetworkException e) {
             // no ThreadNetworkException.ERROR_THREAD_DISABLED error should be thrown
             throw new AssertionError(e);
@@ -422,7 +423,7 @@
 
     private void onOtDaemonDied() {
         checkOnHandlerThread();
-        Log.w(TAG, "OT daemon is dead, clean up...");
+        LOG.w("OT daemon is dead, clean up...");
 
         OperationReceiverWrapper.onOtDaemonDied();
         mOtDaemonCallbackProxy.onOtDaemonDied();
@@ -435,8 +436,7 @@
     public void initialize() {
         mHandler.post(
                 () -> {
-                    Log.d(
-                            TAG,
+                    LOG.v(
                             "Initializing Thread system service: Thread is "
                                     + (shouldEnableThread() ? "enabled" : "disabled"));
                     try {
@@ -493,7 +493,7 @@
             // become dead, so that it's guaranteed that ot-daemon is stopped when {@code
             // receiver} is completed
         } catch (RemoteException e) {
-            Log.e(TAG, "otDaemon.terminate failed", e);
+            LOG.e("otDaemon.terminate failed", e);
             receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
         } catch (ThreadNetworkException e) {
             // No ThreadNetworkException.ERROR_THREAD_DISABLED error will be thrown
@@ -524,7 +524,7 @@
             return;
         }
 
-        Log.i(TAG, "Set Thread enabled: " + isEnabled + ", persist: " + persist);
+        LOG.i("Set Thread enabled: " + isEnabled + ", persist: " + persist);
 
         if (persist) {
             // The persistent setting keeps the desired enabled state, thus it's set regardless
@@ -536,7 +536,7 @@
         try {
             getOtDaemon().setThreadEnabled(isEnabled, newOtStatusReceiver(receiver));
         } catch (RemoteException | ThreadNetworkException e) {
-            Log.e(TAG, "otDaemon.setThreadEnabled failed", e);
+            LOG.e("otDaemon.setThreadEnabled failed", e);
             receiver.onError(e);
         }
     }
@@ -553,7 +553,7 @@
             @NonNull IOperationReceiver operationReceiver) {
         checkOnHandlerThread();
 
-        Log.i(TAG, "Set Thread configuration: " + configuration);
+        LOG.i("Set Thread configuration: " + configuration);
 
         final boolean changed = mPersistentSettings.putConfiguration(configuration);
         try {
@@ -631,8 +631,7 @@
         if (mUserRestricted == newUserRestrictedState) {
             return;
         }
-        Log.i(
-                TAG,
+        LOG.i(
                 "Thread user restriction changed: "
                         + mUserRestricted
                         + " -> "
@@ -644,16 +643,14 @@
                 new IOperationReceiver.Stub() {
                     @Override
                     public void onSuccess() {
-                        Log.d(
-                                TAG,
+                        LOG.v(
                                 (shouldEnableThread ? "Enabled" : "Disabled")
                                         + " Thread due to user restriction change");
                     }
 
                     @Override
                     public void onError(int errorCode, String errorMessage) {
-                        Log.e(
-                                TAG,
+                        LOG.e(
                                 "Failed to "
                                         + (shouldEnableThread ? "enable" : "disable")
                                         + " Thread for user restriction change");
@@ -702,13 +699,13 @@
         @Override
         public void onAvailable(@NonNull Network network) {
             checkOnHandlerThread();
-            Log.i(TAG, "Upstream network available: " + network);
+            LOG.i("Upstream network available: " + network);
         }
 
         @Override
         public void onLost(@NonNull Network network) {
             checkOnHandlerThread();
-            Log.i(TAG, "Upstream network lost: " + network);
+            LOG.i("Upstream network lost: " + network);
 
             // TODO: disable border routing when upsteam network disconnected
         }
@@ -723,7 +720,7 @@
             if (Objects.equals(existingIfName, newIfName)) {
                 return;
             }
-            Log.i(TAG, "Upstream network changed: " + existingIfName + " -> " + newIfName);
+            LOG.i("Upstream network changed: " + existingIfName + " -> " + newIfName);
             mNetworkToInterface.put(network, newIfName);
 
             // TODO: disable border routing if netIfName is null
@@ -737,13 +734,13 @@
         @Override
         public void onAvailable(@NonNull Network network) {
             checkOnHandlerThread();
-            Log.i(TAG, "Thread network is available: " + network);
+            LOG.i("Thread network is available: " + network);
         }
 
         @Override
         public void onLost(@NonNull Network network) {
             checkOnHandlerThread();
-            Log.i(TAG, "Thread network is lost: " + network);
+            LOG.i("Thread network is lost: " + network);
             disableBorderRouting();
         }
 
@@ -751,8 +748,7 @@
         public void onLocalNetworkInfoChanged(
                 @NonNull Network network, @NonNull LocalNetworkInfo localNetworkInfo) {
             checkOnHandlerThread();
-            Log.i(
-                    TAG,
+            LOG.i(
                     "LocalNetworkInfo of Thread network changed: {threadNetwork: "
                             + network
                             + ", localNetworkInfo: "
@@ -810,7 +806,7 @@
         return new NetworkAgent(
                 mContext,
                 mHandler.getLooper(),
-                TAG,
+                LOG.getTag(),
                 netCaps,
                 mTunIfController.getLinkProperties(),
                 newLocalNetworkConfig(),
@@ -827,7 +823,7 @@
         mNetworkAgent = newNetworkAgent();
         mNetworkAgent.register();
         mNetworkAgent.markConnected();
-        Log.i(TAG, "Registered Thread network");
+        LOG.i("Registered Thread network");
     }
 
     private void unregisterThreadNetwork() {
@@ -837,7 +833,7 @@
             return;
         }
 
-        Log.d(TAG, "Unregistering Thread network agent");
+        LOG.v("Unregistering Thread network agent");
 
         mNetworkAgent.unregister();
         mNetworkAgent = null;
@@ -863,7 +859,7 @@
         try {
             getOtDaemon().getChannelMasks(newChannelMasksReceiver(networkName, receiver));
         } catch (RemoteException | ThreadNetworkException e) {
-            Log.e(TAG, "otDaemon.getChannelMasks failed", e);
+            LOG.e("otDaemon.getChannelMasks failed", e);
             receiver.onError(e);
         }
     }
@@ -904,7 +900,7 @@
             now = clock.instant();
             authoritative = true;
         } catch (DateTimeException e) {
-            Log.w(TAG, "Failed to get authoritative time", e);
+            LOG.w("Failed to get authoritative time: " + e.getMessage());
         }
 
         int panId = random.nextInt(/* bound= */ 0xffff);
@@ -1095,7 +1091,7 @@
             // The otDaemon.join() will leave first if this device is currently attached
             getOtDaemon().join(activeDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
         } catch (RemoteException | ThreadNetworkException e) {
-            Log.e(TAG, "otDaemon.join failed", e);
+            LOG.e("otDaemon.join failed", e);
             receiver.onError(e);
         }
     }
@@ -1120,7 +1116,7 @@
                     .scheduleMigration(
                             pendingDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
         } catch (RemoteException | ThreadNetworkException e) {
-            Log.e(TAG, "otDaemon.scheduleMigration failed", e);
+            LOG.e("otDaemon.scheduleMigration failed", e);
             receiver.onError(e);
         }
     }
@@ -1138,7 +1134,7 @@
         try {
             getOtDaemon().leave(newOtStatusReceiver(receiver));
         } catch (RemoteException | ThreadNetworkException e) {
-            Log.e(TAG, "otDaemon.leave failed", e);
+            LOG.e("otDaemon.leave failed", e);
             receiver.onError(e);
         }
     }
@@ -1171,7 +1167,7 @@
         try {
             getOtDaemon().setCountryCode(countryCode, newOtStatusReceiver(receiver));
         } catch (RemoteException | ThreadNetworkException e) {
-            Log.e(TAG, "otDaemon.setCountryCode failed", e);
+            LOG.e("otDaemon.setCountryCode failed", e);
             receiver.onError(e);
         }
     }
@@ -1181,7 +1177,7 @@
             @Nullable String testNetworkInterfaceName, @NonNull IOperationReceiver receiver) {
         enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED, NETWORK_SETTINGS);
 
-        Log.i(TAG, "setTestNetworkAsUpstream: " + testNetworkInterfaceName);
+        LOG.i("setTestNetworkAsUpstream: " + testNetworkInterfaceName);
         mHandler.post(() -> setTestNetworkAsUpstreamInternal(testNetworkInterfaceName, receiver));
     }
 
@@ -1227,7 +1223,7 @@
         try {
             getOtDaemon().setChannelMaxPowers(channelMaxPowers, newOtStatusReceiver(receiver));
         } catch (RemoteException | ThreadNetworkException e) {
-            Log.e(TAG, "otDaemon.setChannelMaxPowers failed", e);
+            LOG.e("otDaemon.setChannelMaxPowers failed", e);
             receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
         }
     }
@@ -1236,7 +1232,7 @@
         if (mInfraLinkState.equals(infraLinkState)) {
             return;
         }
-        Log.i(TAG, "Infra link state changed: " + mInfraLinkState + " -> " + infraLinkState);
+        LOG.i("Infra link state changed: " + mInfraLinkState + " -> " + infraLinkState);
         mInfraLinkState = infraLinkState;
         ParcelFileDescriptor infraIcmp6Socket = null;
         if (mInfraLinkState.interfaceName != null) {
@@ -1244,7 +1240,7 @@
                 infraIcmp6Socket =
                         mInfraIfController.createIcmp6Socket(mInfraLinkState.interfaceName);
             } catch (IOException e) {
-                Log.i(TAG, "Failed to create ICMPv6 socket on infra network interface", e);
+                LOG.e("Failed to create ICMPv6 socket on infra network interface", e);
             }
         }
         try {
@@ -1252,16 +1248,16 @@
                     .setInfraLinkState(
                             mInfraLinkState,
                             infraIcmp6Socket,
-                            new setInfraLinkStateStatusReceiver());
+                            new LoggingOtStatusReceiver("setInfraLinkState"));
         } catch (RemoteException | ThreadNetworkException e) {
-            Log.w(TAG, "Failed to configure border router " + mOtDaemonConfig, e);
+            LOG.e("Failed to configure border router " + mOtDaemonConfig, e);
         }
     }
 
     private void enableBorderRouting(String infraIfName) {
         InfraLinkState infraLinkState =
                 newInfraLinkStateBuilder(mInfraLinkState).setInterfaceName(infraIfName).build();
-        Log.i(TAG, "Enable border routing on AIL: " + infraIfName);
+        LOG.i("Enable border routing on AIL: " + infraIfName);
         setInfraLinkState(infraLinkState);
     }
 
@@ -1269,28 +1265,28 @@
         mUpstreamNetwork = null;
         InfraLinkState infraLinkState =
                 newInfraLinkStateBuilder(mInfraLinkState).setInterfaceName(null).build();
-        Log.i(TAG, "Disabling border routing");
+        LOG.i("Disabling border routing");
         setInfraLinkState(infraLinkState);
     }
 
     private void handleThreadInterfaceStateChanged(boolean isUp) {
         try {
             mTunIfController.setInterfaceUp(isUp);
-            Log.i(TAG, "Thread TUN interface becomes " + (isUp ? "up" : "down"));
+            LOG.i("Thread TUN interface becomes " + (isUp ? "up" : "down"));
         } catch (IOException e) {
-            Log.e(TAG, "Failed to handle Thread interface state changes", e);
+            LOG.e("Failed to handle Thread interface state changes", e);
         }
     }
 
     private void handleDeviceRoleChanged(@DeviceRole int deviceRole) {
         if (ThreadNetworkController.isAttached(deviceRole)) {
-            Log.i(TAG, "Attached to the Thread network");
+            LOG.i("Attached to the Thread network");
 
             // This is an idempotent method which can be called for multiple times when the device
             // is already attached (e.g. going from Child to Router)
             registerThreadNetwork();
         } else {
-            Log.i(TAG, "Detached from the Thread network");
+            LOG.i("Detached from the Thread network");
 
             // This is an idempotent method which can be called for multiple times when the device
             // is already detached or stopped
@@ -1328,7 +1324,7 @@
         }
         final LocalNetworkConfig localNetworkConfig = newLocalNetworkConfig();
         mNetworkAgent.sendLocalNetworkConfig(localNetworkConfig);
-        Log.d(TAG, "Sent localNetworkConfig: " + localNetworkConfig);
+        LOG.v("Sent localNetworkConfig: " + localNetworkConfig);
     }
 
     private void handleMulticastForwardingChanged(BackboneRouterState state) {
@@ -1401,32 +1397,21 @@
         }
     }
 
-    private static final class setOtDaemonConfigurationStatusReceiver
-            extends IOtStatusReceiver.Stub {
-        public setOtDaemonConfigurationStatusReceiver() {}
+    private static class LoggingOtStatusReceiver extends IOtStatusReceiver.Stub {
+        private final String mAction;
+
+        LoggingOtStatusReceiver(String action) {
+            mAction = action;
+        }
 
         @Override
         public void onSuccess() {
-            Log.i(TAG, "Configured border router successfully");
+            LOG.i("The action " + mAction + " succeeded");
         }
 
         @Override
         public void onError(int i, String 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));
+            LOG.w("The action " + mAction + " failed: " + i + " " + s);
         }
     }
 
@@ -1463,7 +1448,7 @@
             try {
                 getOtDaemon().registerStateCallback(this, callbackMetadata.id);
             } catch (RemoteException | ThreadNetworkException e) {
-                Log.e(TAG, "otDaemon.registerStateCallback failed", e);
+                LOG.e("otDaemon.registerStateCallback failed", e);
             }
         }
 
@@ -1495,7 +1480,7 @@
             try {
                 getOtDaemon().registerStateCallback(this, callbackMetadata.id);
             } catch (RemoteException | ThreadNetworkException e) {
-                Log.e(TAG, "otDaemon.registerStateCallback failed", e);
+                LOG.e("otDaemon.registerStateCallback failed", e);
             }
         }
 
@@ -1587,7 +1572,7 @@
                 mActiveDataset = newActiveDataset;
             } catch (IllegalArgumentException e) {
                 // Is unlikely that OT will generate invalid Operational Dataset
-                Log.wtf(TAG, "Invalid Active Operational Dataset from OpenThread", e);
+                LOG.wtf("Invalid Active Operational Dataset from OpenThread", e);
             }
 
             PendingOperationalDataset newPendingDataset;
@@ -1602,7 +1587,7 @@
                 mPendingDataset = newPendingDataset;
             } catch (IllegalArgumentException e) {
                 // Is unlikely that OT will generate invalid Operational Dataset
-                Log.wtf(TAG, "Invalid Pending Operational Dataset from OpenThread", e);
+                LOG.wtf("Invalid Pending Operational Dataset from OpenThread", e);
             }
         }
 
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
index a194114..2cd34e8 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
@@ -38,10 +38,10 @@
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.util.ArrayMap;
-import android.util.Log;
 
 import com.android.connectivity.resources.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.ConnectivityResources;
 
 import java.io.FileDescriptor;
@@ -63,7 +63,9 @@
  */
 @TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
 public class ThreadNetworkCountryCode {
-    private static final String TAG = "ThreadNetworkCountryCode";
+    private static final String TAG = "CountryCode";
+    private static final SharedLog LOG = ThreadNetworkLogger.forSubComponent(TAG);
+
     // To be used when there is no country code available.
     @VisibleForTesting public static final String DEFAULT_COUNTRY_CODE = "WW";
 
@@ -280,11 +282,11 @@
             String countryCode = addresses.get(0).getCountryCode();
 
             if (isValidCountryCode(countryCode)) {
-                Log.d(TAG, "Set location country code to: " + countryCode);
+                LOG.v("Set location country code to: " + countryCode);
                 mLocationCountryCodeInfo =
                         new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_LOCATION);
             } else {
-                Log.d(TAG, "Received invalid location country code");
+                LOG.v("Received invalid location country code");
                 mLocationCountryCodeInfo = null;
             }
 
@@ -296,8 +298,7 @@
         if ((location == null) || (mGeocoder == null)) return;
 
         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
-            Log.wtf(
-                    TAG,
+            LOG.wtf(
                     "Unexpected call to set country code from the Geocoding location, "
                             + "Thread code never runs under T or lower.");
             return;
@@ -320,13 +321,13 @@
     private class WifiCountryCodeCallback implements ActiveCountryCodeChangedCallback {
         @Override
         public void onActiveCountryCodeChanged(String countryCode) {
-            Log.d(TAG, "Wifi country code is changed to " + countryCode);
+            LOG.v("Wifi country code is changed to " + countryCode);
             synchronized ("ThreadNetworkCountryCode.this") {
                 if (isValidCountryCode(countryCode)) {
                     mWifiCountryCodeInfo =
                             new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_WIFI);
                 } else {
-                    Log.w(TAG, "WiFi country code " + countryCode + " is invalid");
+                    LOG.w("WiFi country code " + countryCode + " is invalid");
                     mWifiCountryCodeInfo = null;
                 }
 
@@ -336,7 +337,7 @@
 
         @Override
         public void onCountryCodeInactive() {
-            Log.d(TAG, "Wifi country code is inactived");
+            LOG.v("Wifi country code is inactived");
             synchronized ("ThreadNetworkCountryCode.this") {
                 mWifiCountryCodeInfo = null;
                 updateCountryCode(false /* forceUpdate */);
@@ -346,8 +347,7 @@
 
     private synchronized void registerTelephonyCountryCodeCallback() {
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
-            Log.wtf(
-                    TAG,
+            LOG.wtf(
                     "Unexpected call to register the telephony country code changed callback, "
                             + "Thread code never runs under T or lower.");
             return;
@@ -387,7 +387,7 @@
                 mSubscriptionManager.getActiveSubscriptionInfoList();
 
         if (subscriptionInfoList == null) {
-            Log.d(TAG, "No SIM card is found");
+            LOG.v("No SIM card is found");
             return;
         }
 
@@ -399,11 +399,11 @@
             try {
                 countryCode = mTelephonyManager.getNetworkCountryIso(slotIndex);
             } catch (IllegalArgumentException e) {
-                Log.e(TAG, "Failed to get country code for slot index:" + slotIndex, e);
+                LOG.e("Failed to get country code for slot index:" + slotIndex, e);
                 continue;
             }
 
-            Log.d(TAG, "Telephony slot " + slotIndex + " country code is " + countryCode);
+            LOG.v("Telephony slot " + slotIndex + " country code is " + countryCode);
             setTelephonyCountryCodeAndLastKnownCountryCode(
                     slotIndex, countryCode, null /* lastKnownCountryCode */);
         }
@@ -411,8 +411,7 @@
 
     private synchronized void setTelephonyCountryCodeAndLastKnownCountryCode(
             int slotIndex, String countryCode, String lastKnownCountryCode) {
-        Log.d(
-                TAG,
+        LOG.v(
                 "Set telephony country code to: "
                         + countryCode
                         + ", last country code to: "
@@ -522,8 +521,7 @@
 
             @Override
             public void onError(int otError, String message) {
-                Log.e(
-                        TAG,
+                LOG.e(
                         "Error "
                                 + otError
                                 + ": "
@@ -545,11 +543,11 @@
         CountryCodeInfo countryCodeInfo = pickCountryCode();
 
         if (!forceUpdate && countryCodeInfo.isCountryCodeMatch(mCurrentCountryCodeInfo)) {
-            Log.i(TAG, "Ignoring already set country code " + countryCodeInfo.getCountryCode());
+            LOG.i("Ignoring already set country code " + countryCodeInfo.getCountryCode());
             return;
         }
 
-        Log.i(TAG, "Set country code: " + countryCodeInfo);
+        LOG.i("Set country code: " + countryCodeInfo);
         mThreadNetworkControllerService.setCountryCode(
                 countryCodeInfo.getCountryCode().toUpperCase(Locale.ROOT),
                 newOperationReceiver(countryCodeInfo));
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkLogger.java b/thread/service/java/com/android/server/thread/ThreadNetworkLogger.java
new file mode 100644
index 0000000..a765304
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkLogger.java
@@ -0,0 +1,37 @@
+/*
+ * 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.thread;
+
+import com.android.net.module.util.SharedLog;
+
+/**
+ * The Logger for Thread network.
+ *
+ * <p>Each class should log with its own tag using the logger of
+ * ThreadNetworkLogger.forSubComponent(TAG).
+ */
+public final class ThreadNetworkLogger {
+    private static final String TAG = "ThreadNetwork";
+    private static final SharedLog mLog = new SharedLog(TAG);
+
+    public static SharedLog forSubComponent(String subComponent) {
+        return mLog.forSubComponent(subComponent);
+    }
+
+    // Disable instantiation
+    private ThreadNetworkLogger() {}
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index 7c4c72d..fc18ef9 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -25,11 +25,11 @@
 import android.net.thread.ThreadConfiguration;
 import android.os.PersistableBundle;
 import android.util.AtomicFile;
-import android.util.Log;
 
 import com.android.connectivity.resources.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.ConnectivityResources;
 
 import java.io.ByteArrayInputStream;
@@ -48,6 +48,7 @@
  */
 public class ThreadPersistentSettings {
     private static final String TAG = "ThreadPersistentSettings";
+    private static final SharedLog LOG = ThreadNetworkLogger.forSubComponent(TAG);
 
     /** File name used for storing settings. */
     private static final String FILE_NAME = "ThreadPersistentSettings.xml";
@@ -115,7 +116,7 @@
         readFromStoreFile();
         synchronized (mLock) {
             if (!mSettings.containsKey(THREAD_ENABLED.key)) {
-                Log.i(TAG, "\"thread_enabled\" is missing in settings file, using default value");
+                LOG.i("\"thread_enabled\" is missing in settings file, using default value");
                 put(
                         THREAD_ENABLED.key,
                         mResources.get().getBoolean(R.bool.config_thread_default_enabled));
@@ -243,7 +244,7 @@
                 writeToAtomicFile(mAtomicFile, outputStream.toByteArray());
             }
         } catch (IOException e) {
-            Log.wtf(TAG, "Write to store file failed", e);
+            LOG.wtf("Write to store file failed", e);
         }
     }
 
@@ -251,7 +252,7 @@
         try {
             final byte[] readData;
             synchronized (mLock) {
-                Log.i(TAG, "Reading from store file: " + mAtomicFile.getBaseFile());
+                LOG.i("Reading from store file: " + mAtomicFile.getBaseFile());
                 readData = readFromAtomicFile(mAtomicFile);
             }
             final ByteArrayInputStream inputStream = new ByteArrayInputStream(readData);
@@ -262,9 +263,9 @@
                 mSettings.putAll(bundleRead);
             }
         } catch (FileNotFoundException e) {
-            Log.w(TAG, "No store file to read", e);
+            LOG.w("No store file to read " + e.getMessage());
         } catch (IOException e) {
-            Log.e(TAG, "Read from store file failed", e);
+            LOG.e("Read from store file failed", e);
         }
     }
 
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
index 976f93d..85a0371 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -38,10 +38,10 @@
 import android.os.SystemClock;
 import android.system.ErrnoException;
 import android.system.Os;
-import android.util.Log;
 
 import com.android.net.module.util.HexDump;
 import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
+import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.netlink.NetlinkUtils;
 import com.android.net.module.util.netlink.StructIfinfoMsg;
 import com.android.net.module.util.netlink.StructNlAttr;
@@ -66,6 +66,7 @@
 public class TunInterfaceController {
     private static final String TAG = "TunIfController";
     private static final boolean DBG = false;
+    private static final SharedLog LOG = ThreadNetworkLogger.forSubComponent(TAG);
     private static final long INFINITE_LIFETIME = 0xffffffffL;
     static final int MTU = 1280;
 
@@ -147,7 +148,7 @@
 
     /** Adds a new address to the interface. */
     public void addAddress(LinkAddress address) {
-        Log.d(TAG, "Adding address " + address + " with flags: " + address.getFlags());
+        LOG.v("Adding address " + address + " with flags: " + address.getFlags());
 
         long preferredLifetimeSeconds;
         long validLifetimeSeconds;
@@ -180,7 +181,7 @@
                 (byte) address.getScope(),
                 preferredLifetimeSeconds,
                 validLifetimeSeconds)) {
-            Log.w(TAG, "Failed to add address " + address.getAddress().getHostAddress());
+            LOG.w("Failed to add address " + address.getAddress().getHostAddress());
             return;
         }
         mLinkProperties.addLinkAddress(address);
@@ -189,7 +190,7 @@
 
     /** Removes an address from the interface. */
     public void removeAddress(LinkAddress address) {
-        Log.d(TAG, "Removing address " + address);
+        LOG.v("Removing address " + address);
 
         // Intentionally update the mLinkProperties before send netlink message because the
         // address is already removed from ot-daemon and apps can't reach to the address even
@@ -200,7 +201,7 @@
                 Os.if_nametoindex(mIfName),
                 (Inet6Address) address.getAddress(),
                 (short) address.getPrefixLength())) {
-            Log.w(TAG, "Failed to remove address " + address.getAddress().getHostAddress());
+            LOG.w("Failed to remove address " + address.getAddress().getHostAddress());
         }
     }
 
@@ -287,7 +288,7 @@
         try {
             setInterfaceUp(false);
         } catch (IOException e) {
-            Log.e(TAG, "Failed to set Thread TUN interface down");
+            LOG.e("Failed to set Thread TUN interface down");
         }
     }
 
@@ -347,11 +348,15 @@
             if (e.getCause() instanceof ErrnoException) {
                 ErrnoException ee = (ErrnoException) e.getCause();
                 if (ee.errno == EADDRINUSE) {
-                    Log.w(TAG, "Already joined group" + address.getHostAddress(), e);
+                    LOG.w(
+                            "Already joined group "
+                                    + address.getHostAddress()
+                                    + ": "
+                                    + e.getMessage());
                     return;
                 }
             }
-            Log.e(TAG, "failed to join group " + address.getHostAddress(), e);
+            LOG.e("failed to join group " + address.getHostAddress(), e);
         }
     }
 
@@ -360,7 +365,7 @@
         try {
             mMulticastSocket.leaveGroup(socketAddress, mNetworkInterface);
         } catch (IOException e) {
-            Log.e(TAG, "failed to leave group " + address.getHostAddress(), e);
+            LOG.e("failed to leave group " + address.getHostAddress(), e);
         }
     }
 
@@ -415,14 +420,14 @@
         }
 
         if (DBG) {
-            Log.d(TAG, "ADDR_GEN_MODE message is:");
-            Log.d(TAG, HexDump.dumpHexString(msg));
+            LOG.v("ADDR_GEN_MODE message is:");
+            LOG.v(HexDump.dumpHexString(msg));
         }
 
         try {
             NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
         } catch (ErrnoException e) {
-            Log.e(TAG, "Failed to set ADDR_GEN_MODE to NONE", e);
+            LOG.e("Failed to set ADDR_GEN_MODE to NONE", e);
         }
     }
 }
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
index 6eda1e9..34aabe2 100644
--- a/thread/tests/cts/AndroidTest.xml
+++ b/thread/tests/cts/AndroidTest.xml
@@ -22,6 +22,7 @@
     <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user_on_secondary_display" />
 
     <!--
         Only run tests if the device under test is SDK version 33 (Android 13) or above.