Merge "Add packet capture and match network_stack adb command to to `apf_utils`" into main
diff --git a/Cronet/OWNERS b/Cronet/OWNERS
deleted file mode 100644
index c24680e..0000000
--- a/Cronet/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-set noparent
-file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 94adc5b..b773ed8 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -316,6 +316,14 @@
         }
       ]
     },
+    {
+      "name": "CtsHostsideNetworkTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        }
+      ]
+    },
     // Test with APK modules only, in cases where APEX is not supported, or the other modules
     // were simply not updated
     {
@@ -414,15 +422,6 @@
           "exclude-annotation": "androidx.test.filters.RequiresDevice"
         }
       ]
-    },
-    // TODO: upgrade to presubmit. Postsubmit on virtual devices to monitor flakiness only.
-    {
-      "name": "CtsHostsideNetworkTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
-      "options": [
-        {
-          "exclude-annotation": "androidx.test.filters.RequiresDevice"
-        }
-      ]
     }
   ],
   "imports": [
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 70b38a4..5cf5528 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -79,6 +79,7 @@
     ],
     defaults: ["TetheringExternalLibs"],
     libs: [
+        "framework-annotations-lib",
         "framework-tethering.impl",
     ],
     manifest: "AndroidManifestBase.xml",
@@ -203,6 +204,7 @@
     use_embedded_native_libs: true,
     privapp_allowlist: ":privapp_allowlist_com.android.tethering",
     apex_available: ["com.android.tethering"],
+    updatable: true,
 }
 
 android_app {
@@ -220,6 +222,7 @@
     lint: {
         error_checks: ["NewApi"],
     },
+    updatable: true,
 }
 
 sdk {
diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
index 0df9047..af061e4 100644
--- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -198,4 +198,13 @@
     public String toString() {
         return "Netd used";
     }
+
+    @Override
+    public int getLastMaxConnectionAndResetToCurrent() {
+        return 0;
+    }
+
+    @Override
+    public void clearConnectionCounters() {
+    }
 }
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 e6e99f4..b460f0d 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
@@ -19,6 +19,7 @@
 import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
 
 import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_ACTIVE_SESSIONS_METRICS;
 
 import android.system.ErrnoException;
 import android.system.Os;
@@ -108,6 +109,22 @@
     // TODO: Add IPv6 rule count.
     private final SparseArray<Integer> mRule4CountOnUpstream = new SparseArray<>();
 
+    private final boolean mSupportActiveSessionsMetrics;
+    /**
+     * Tracks the current number of tethering connections and the maximum
+     * observed since the last metrics collection. Used to provide insights
+     * into the distribution of active tethering sessions for metrics reporting.
+
+     * These variables are accessed on the handler thread, which includes:
+     *  1. ConntrackEvents signaling the addition or removal of an IPv4 rule.
+     *  2. ConntrackEvents indicating the removal of a tethering client,
+     *     triggering the removal of associated rules.
+     *  3. Removal of the last IpServer, which resets counters to handle
+     *     potential synchronization issues.
+     */
+    private int mLastMaxConnectionCount = 0;
+    private int mCurrentConnectionCount = 0;
+
     public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) {
         mLog = deps.getSharedLog().forSubComponent(TAG);
 
@@ -156,6 +173,9 @@
         } catch (ErrnoException e) {
             mLog.e("Could not clear mBpfDevMap: " + e);
         }
+
+        mSupportActiveSessionsMetrics = deps.isFeatureEnabled(deps.getContext(),
+                TETHER_ACTIVE_SESSIONS_METRICS);
     }
 
     @Override
@@ -350,6 +370,12 @@
                 final int upstreamIfindex = (int) key.iif;
                 int count = mRule4CountOnUpstream.get(upstreamIfindex, 0 /* default */);
                 mRule4CountOnUpstream.put(upstreamIfindex, ++count);
+
+                if (mSupportActiveSessionsMetrics) {
+                    mCurrentConnectionCount++;
+                    mLastMaxConnectionCount = Math.max(mCurrentConnectionCount,
+                            mLastMaxConnectionCount);
+                }
             } else {
                 mBpfUpstream4Map.insertEntry(key, value);
             }
@@ -385,6 +411,10 @@
                 } else {
                     mRule4CountOnUpstream.put(upstreamIfindex, count);
                 }
+
+                if (mSupportActiveSessionsMetrics) {
+                    mCurrentConnectionCount--;
+                }
             } else {
                 if (!mBpfUpstream4Map.deleteEntry(key)) return false;  // Rule did not exist
             }
@@ -465,14 +495,16 @@
 
     @Override
     public String toString() {
-        return String.join(", ", new String[] {
-                mapStatus(mBpfDownstream6Map, "mBpfDownstream6Map"),
-                mapStatus(mBpfUpstream6Map, "mBpfUpstream6Map"),
-                mapStatus(mBpfDownstream4Map, "mBpfDownstream4Map"),
-                mapStatus(mBpfUpstream4Map, "mBpfUpstream4Map"),
-                mapStatus(mBpfStatsMap, "mBpfStatsMap"),
-                mapStatus(mBpfLimitMap, "mBpfLimitMap"),
-                mapStatus(mBpfDevMap, "mBpfDevMap")
+        return String.join(", ", new String[]{
+            mapStatus(mBpfDownstream6Map, "mBpfDownstream6Map"),
+            mapStatus(mBpfUpstream6Map, "mBpfUpstream6Map"),
+            mapStatus(mBpfDownstream4Map, "mBpfDownstream4Map"),
+            mapStatus(mBpfUpstream4Map, "mBpfUpstream4Map"),
+            mapStatus(mBpfStatsMap, "mBpfStatsMap"),
+            mapStatus(mBpfLimitMap, "mBpfLimitMap"),
+            mapStatus(mBpfDevMap, "mBpfDevMap"),
+            "mCurrentConnectionCount=" + mCurrentConnectionCount,
+            "mLastMaxConnectionCount=" + mLastMaxConnectionCount
         });
     }
 
@@ -507,4 +539,17 @@
 
         return 0;
     }
+
+    /** Get last max connection count and reset to current count. */
+    public int getLastMaxConnectionAndResetToCurrent() {
+        final int ret = mLastMaxConnectionCount;
+        mLastMaxConnectionCount = mCurrentConnectionCount;
+        return ret;
+    }
+
+    /** Clear current connection count. */
+    public void clearConnectionCounters() {
+        mCurrentConnectionCount = 0;
+        mLastMaxConnectionCount = 0;
+    }
 }
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 026b1c3..cb8bcc9 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
@@ -202,5 +202,11 @@
      * Remove interface index mapping.
      */
     public abstract boolean removeDevMap(int ifIndex);
+
+    /** Get last max connection count and reset to current count. */
+    public abstract int getLastMaxConnectionAndResetToCurrent();
+
+    /** Clear current connection count. */
+    public abstract void clearConnectionCounters();
 }
 
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 5aca642..411971d 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -430,7 +430,7 @@
     // Used to dispatch legacy ConnectivityManager methods that expect tethering to be able to
     // return results and perform operations synchronously.
     // TODO: remove once there are no callers of these legacy methods.
-    private class RequestDispatcher {
+    private static class RequestDispatcher {
         private final ConditionVariable mWaiting;
         public volatile int mRemoteResult;
 
@@ -446,8 +446,8 @@
             mWaiting = new ConditionVariable();
         }
 
-        int waitForResult(final RequestHelper request) {
-            getConnector(c -> request.runRequest(c, mListener));
+        int waitForResult(final RequestHelper request, final TetheringManager mgr) {
+            mgr.getConnector(c -> request.runRequest(c, mListener));
             if (!mWaiting.block(DEFAULT_TIMEOUT_MS)) {
                 throw new IllegalStateException("Callback timeout");
             }
@@ -603,7 +603,7 @@
             } catch (RemoteException e) {
                 throw new IllegalStateException(e);
             }
-        });
+        }, this);
     }
 
     /**
@@ -635,7 +635,7 @@
             } catch (RemoteException e) {
                 throw new IllegalStateException(e);
             }
-        });
+        }, this);
     }
 
     /**
@@ -663,7 +663,7 @@
             } catch (RemoteException e) {
                 throw new IllegalStateException(e);
             }
-        });
+        }, this);
     }
 
     /**
@@ -1751,7 +1751,7 @@
             } catch (RemoteException e) {
                 throw new IllegalStateException(e);
             }
-        });
+        }, this);
 
         return ret == TETHER_ERROR_NO_ERROR;
     }
@@ -1800,6 +1800,6 @@
             } catch (RemoteException e) {
                 throw new IllegalStateException(e);
             }
-        });
+        }, this);
     }
 }
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index b807544..506fa56 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -1148,6 +1148,7 @@
                 case CMD_SERVICE_FAILED_TO_START:
                     mLog.e("start serving fail, error: " + message.arg1);
                     transitionTo(mInitialState);
+                    break;
                 default:
                     return false;
             }
@@ -1393,8 +1394,28 @@
         @Override
         public void enter() {
             mLastError = TETHER_ERROR_NO_ERROR;
+            // TODO: clean this up after the synchronous state machine is fully rolled out. Clean up
+            // can be directly triggered after calling IpServer.stop() inside Tethering.java.
             sendInterfaceState(STATE_UNAVAILABLE);
         }
+
+        @Override
+        public boolean processMessage(Message message) {
+            switch (message.what) {
+                case CMD_IPV6_TETHER_UPDATE:
+                    // sendInterfaceState(STATE_UNAVAILABLE) triggers
+                    // handleInterfaceServingStateInactive which in turn cleans up IPv6 tethering
+                    // (and calls into IpServer one more time). At this point, this is the only
+                    // message we potentially see in this state because this IpServer has already
+                    // been removed from mTetherStates before transitioning to this State; however,
+                    // handleInterfaceServiceStateInactive passes a reference.
+                    // TODO: This can be removed once SyncStateMachine is rolled out and the
+                    // teardown path is cleaned up.
+                    return true;
+                default:
+                    return false;
+            }
+        }
     }
 
     class WaitingForRestartState extends State {
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 89e06da..75ab9ec 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -27,6 +27,7 @@
 import static android.system.OsConstants.ETH_P_IP;
 import static android.system.OsConstants.ETH_P_IPV6;
 
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_MIN_MTU;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
 import static com.android.net.module.util.ip.ConntrackMonitor.ConntrackEvent;
@@ -334,7 +335,6 @@
     };
 
     // TODO: add BpfMap<TetherDownstream64Key, TetherDownstream64Value> retrieving function.
-    @VisibleForTesting
     public abstract static class Dependencies {
         /** Get handler. */
         @NonNull public abstract Handler getHandler();
@@ -585,14 +585,10 @@
             if (mHandler.hasCallbacks(mScheduledConntrackMetricsSampling)) {
                 mHandler.removeCallbacks(mScheduledConntrackMetricsSampling);
             }
-            final int currentCount = mBpfConntrackEventConsumer.getCurrentConnectionCount();
-            if (currentCount != 0) {
-                Log.wtf(TAG, "Unexpected CurrentConnectionCount: " + currentCount);
-            }
             // Avoid sending metrics when tethering is about to close.
             // This leads to a missing final sample before disconnect
             // but avoids possibly duplicating the last metric in the upload.
-            mBpfConntrackEventConsumer.clearConnectionCounters();
+            mBpfCoordinatorShim.clearConnectionCounters();
         }
         // Stop scheduled polling stats and poll the latest stats from BPF maps.
         if (mHandler.hasCallbacks(mScheduledPollingStats)) {
@@ -1091,10 +1087,6 @@
         for (final Tether4Key k : deleteDownstreamRuleKeys) {
             mBpfCoordinatorShim.tetherOffloadRuleRemove(DOWNSTREAM, k);
         }
-        if (mSupportActiveSessionsMetrics) {
-            mBpfConntrackEventConsumer.decreaseCurrentConnectionCount(
-                    deleteUpstreamRuleKeys.size());
-        }
 
         // Cleanup each upstream interface by a set which avoids duplicated work on the same
         // upstream interface. Cleaning up the same interface twice (or more) here may raise
@@ -1367,10 +1359,6 @@
 
         pw.println();
         pw.println("mSupportActiveSessionsMetrics: " + mSupportActiveSessionsMetrics);
-        pw.println("getLastMaxConnectionCount: "
-                + mBpfConntrackEventConsumer.getLastMaxConnectionCount());
-        pw.println("getCurrentConnectionCount: "
-                + mBpfConntrackEventConsumer.getCurrentConnectionCount());
     }
 
     private void dumpStats(@NonNull IndentingPrintWriter pw) {
@@ -2062,21 +2050,6 @@
     // while TCP status is established.
     @VisibleForTesting
     class BpfConntrackEventConsumer implements ConntrackEventConsumer {
-        /**
-         * Tracks the current number of tethering connections and the maximum
-         * observed since the last metrics collection. Used to provide insights
-         * into the distribution of active tethering sessions for metrics reporting.
-
-         * These variables are accessed on the handler thread, which includes:
-         *  1. ConntrackEvents signaling the addition or removal of an IPv4 rule.
-         *  2. ConntrackEvents indicating the removal of a tethering client,
-         *     triggering the removal of associated rules.
-         *  3. Removal of the last IpServer, which resets counters to handle
-         *     potential synchronization issues.
-         */
-        private int mLastMaxConnectionCount = 0;
-        private int mCurrentConnectionCount = 0;
-
         // The upstream4 and downstream4 rules are built as the following tables. Only raw ip
         // upstream interface is supported. Note that the field "lastUsed" is only updated by
         // BPF program which records the last used time for a given rule.
@@ -2210,10 +2183,6 @@
                     return;
                 }
 
-                if (mSupportActiveSessionsMetrics) {
-                    decreaseCurrentConnectionCount(1);
-                }
-
                 maybeClearLimit(upstreamIndex);
                 return;
             }
@@ -2237,40 +2206,12 @@
                         + ", downstream: " + addedDownstream + ")");
                 return;
             }
-            if (mSupportActiveSessionsMetrics && addedUpstream && addedDownstream) {
-                mCurrentConnectionCount++;
-                mLastMaxConnectionCount = Math.max(mCurrentConnectionCount,
-                        mLastMaxConnectionCount);
-            }
         }
+    }
 
-        public int getLastMaxConnectionAndResetToCurrent() {
-            final int ret = mLastMaxConnectionCount;
-            mLastMaxConnectionCount = mCurrentConnectionCount;
-            return ret;
-        }
-
-        /** For dumping current state only. */
-        public int getLastMaxConnectionCount() {
-            return mLastMaxConnectionCount;
-        }
-
-        public int getCurrentConnectionCount() {
-            return mCurrentConnectionCount;
-        }
-
-        public void decreaseCurrentConnectionCount(int count) {
-            mCurrentConnectionCount -= count;
-            if (mCurrentConnectionCount < 0) {
-                Log.wtf(TAG, "Unexpected mCurrentConnectionCount: "
-                        + mCurrentConnectionCount);
-            }
-        }
-
-        public void clearConnectionCounters() {
-            mCurrentConnectionCount = 0;
-            mLastMaxConnectionCount = 0;
-        }
+    @VisibleForTesting(visibility = PRIVATE)
+    public int getLastMaxConnectionAndResetToCurrent() {
+        return mBpfCoordinatorShim.getLastMaxConnectionAndResetToCurrent();
     }
 
     @VisibleForTesting
@@ -2611,7 +2552,7 @@
 
     private void uploadConntrackMetricsSample() {
         mDeps.sendTetheringActiveSessionsReported(
-                mBpfConntrackEventConsumer.getLastMaxConnectionAndResetToCurrent());
+                mBpfCoordinatorShim.getLastMaxConnectionAndResetToCurrent());
     }
 
     private void schedulePollingStats() {
diff --git a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
index 21e55b4..1d5df61 100644
--- a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
@@ -28,8 +28,6 @@
 
 import static java.util.Arrays.asList;
 
-import android.content.Context;
-import android.net.ConnectivityManager;
 import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
@@ -53,6 +51,7 @@
 import java.util.List;
 import java.util.Random;
 import java.util.Set;
+import java.util.function.Supplier;
 
 /**
  * This class coordinate IP addresses conflict problem.
@@ -65,6 +64,7 @@
  * @hide
  */
 public class PrivateAddressCoordinator {
+    // WARNING: Keep in sync with chooseDownstreamAddress
     public static final int PREFIX_LENGTH = 24;
 
     // Upstream monitor would be stopped when tethering is down. When tethering restart, downstream
@@ -77,19 +77,20 @@
     private static final String LEGACY_WIFI_P2P_IFACE_ADDRESS = "192.168.49.1/24";
     private static final String LEGACY_BLUETOOTH_IFACE_ADDRESS = "192.168.44.1/24";
     private final List<IpPrefix> mTetheringPrefixes;
-    private final ConnectivityManager mConnectivityMgr;
+    // A supplier that returns ConnectivityManager#getAllNetworks.
+    private final Supplier<Network[]> mGetAllNetworksSupplier;
     private final boolean mIsRandomPrefixBaseEnabled;
     private final boolean mShouldEnableWifiP2pDedicatedIp;
     // keyed by downstream type(TetheringManager.TETHERING_*).
     private final ArrayMap<AddressKey, LinkAddress> mCachedAddresses;
     private final Random mRandom;
 
-    public PrivateAddressCoordinator(Context context, boolean isRandomPrefixBase,
-                                     boolean shouldEnableWifiP2pDedicatedIp) {
+    public PrivateAddressCoordinator(Supplier<Network[]> getAllNetworksSupplier,
+            boolean isRandomPrefixBase,
+            boolean shouldEnableWifiP2pDedicatedIp) {
         mDownstreams = new ArraySet<>();
         mUpstreamPrefixMap = new ArrayMap<>();
-        mConnectivityMgr = (ConnectivityManager) context.getSystemService(
-                Context.CONNECTIVITY_SERVICE);
+        mGetAllNetworksSupplier = getAllNetworksSupplier;
         mIsRandomPrefixBaseEnabled = isRandomPrefixBase;
         mShouldEnableWifiP2pDedicatedIp = shouldEnableWifiP2pDedicatedIp;
         mCachedAddresses = new ArrayMap<AddressKey, LinkAddress>();
@@ -166,7 +167,7 @@
 
         // Remove all upstreams that are no longer valid networks
         final Set<Network> toBeRemoved = new HashSet<>(mUpstreamPrefixMap.keySet());
-        toBeRemoved.removeAll(asList(mConnectivityMgr.getAllNetworks()));
+        toBeRemoved.removeAll(asList(mGetAllNetworksSupplier.get()));
 
         mUpstreamPrefixMap.removeAll(toBeRemoved);
     }
@@ -194,7 +195,7 @@
             return cachedAddress;
         }
 
-        final int prefixIndex = getStartedPrefixIndex();
+        final int prefixIndex = getRandomPrefixIndex();
         for (int i = 0; i < mTetheringPrefixes.size(); i++) {
             final IpPrefix prefixRange = mTetheringPrefixes.get(
                     (prefixIndex + i) % mTetheringPrefixes.size());
@@ -210,7 +211,7 @@
         return null;
     }
 
-    private int getStartedPrefixIndex() {
+    private int getRandomPrefixIndex() {
         if (!mIsRandomPrefixBaseEnabled) return 0;
 
         final int random = getRandomInt() & 0xffffff;
@@ -247,123 +248,62 @@
         return getInUseDownstreamPrefix(prefix);
     }
 
-    // Get the next non-conflict sub prefix. E.g: To get next sub prefix from 10.0.0.0/8, if the
-    // previously selected prefix is 10.20.42.0/24(subPrefix: 0.20.42.0) and the conflicting prefix
-    // is 10.16.0.0/20 (10.16.0.0 ~ 10.16.15.255), then the max address under subPrefix is
-    // 0.16.15.255 and the next subPrefix is 0.16.16.255/24 (0.16.15.255 + 0.0.1.0).
-    // Note: the sub address 0.0.0.255 here is fine to be any value that it will be replaced as
-    // selected random sub address later.
-    private int getNextSubPrefix(final IpPrefix conflictPrefix, final int prefixRangeMask) {
-        final int suffixMask = ~prefixLengthToV4NetmaskIntHTH(conflictPrefix.getPrefixLength());
-        // The largest offset within the prefix assignment block that still conflicts with
-        // conflictPrefix.
-        final int maxConflict =
-                (getPrefixBaseAddress(conflictPrefix) | suffixMask) & ~prefixRangeMask;
-
-        final int prefixMask = prefixLengthToV4NetmaskIntHTH(PREFIX_LENGTH);
-        // Pick a sub prefix a full prefix (1 << (32 - PREFIX_LENGTH) addresses) greater than
-        // maxConflict. This ensures that the selected prefix never overlaps with conflictPrefix.
-        // There is no need to mask the result with PREFIX_LENGTH bits because this is done by
-        // findAvailablePrefixFromRange when it constructs the prefix.
-        return maxConflict + (1 << (32 - PREFIX_LENGTH));
-    }
-
-    private LinkAddress chooseDownstreamAddress(final IpPrefix prefixRange) {
+    @VisibleForTesting
+    public LinkAddress chooseDownstreamAddress(final IpPrefix prefixRange) {
         // The netmask of the prefix assignment block (e.g., 0xfff00000 for 172.16.0.0/12).
         final int prefixRangeMask = prefixLengthToV4NetmaskIntHTH(prefixRange.getPrefixLength());
 
         // The zero address in the block (e.g., 0xac100000 for 172.16.0.0/12).
         final int baseAddress = getPrefixBaseAddress(prefixRange);
 
-        // The subnet mask corresponding to PREFIX_LENGTH.
-        final int prefixMask = prefixLengthToV4NetmaskIntHTH(PREFIX_LENGTH);
+        // Try to get an address within the given prefix that does not conflict with any other
+        // prefix in the system.
+        for (int i = 0; i < 20; ++i) {
+            final int randomSuffix = mRandom.nextInt() & ~prefixRangeMask;
+            final int randomAddress = baseAddress | randomSuffix;
 
-        // The offset within prefixRange of a randomly-selected prefix of length PREFIX_LENGTH.
-        // This may not be the prefix of the address returned by this method:
-        // - If it is already in use, the method will return an address in another prefix.
-        // - If all prefixes within prefixRange are in use, the method will return null. For
-        // example, for a /24 prefix within 172.26.0.0/12, this will be a multiple of 256 in
-        // [0, 1048576). In other words, a random 32-bit number with mask 0x000fff00.
-        //
-        // prefixRangeMask is required to ensure no wrapping. For example, consider:
-        // - prefixRange 127.0.0.0/8
-        // - randomPrefixStart 127.255.255.0
-        // - A conflicting prefix of 127.255.254.0/23
-        // In this case without prefixRangeMask, getNextSubPrefix would return 128.0.0.0, which
-        // means the "start < end" check in findAvailablePrefixFromRange would not reject the prefix
-        // because Java doesn't have unsigned integers, so 128.0.0.0 = 0x80000000 = -2147483648
-        // is less than 127.0.0.0 = 0x7f000000 = 2130706432.
-        //
-        // Additionally, it makes debug output easier to read by making the numbers smaller.
-        final int randomInt = getRandomInt();
-        final int randomPrefixStart = randomInt & ~prefixRangeMask & prefixMask;
+            // Avoid selecting x.x.x.[0, 1, 255] addresses.
+            switch (randomAddress & 0xFF) {
+                case 0:
+                case 1:
+                case 255:
+                    // Try selecting a different address
+                    continue;
+            }
 
-        // A random offset within the prefix. Used to determine the local address once the prefix
-        // is selected. It does not result in an IPv4 address ending in .0, .1, or .255
-        // For a PREFIX_LENGTH of 24, this is a number between 2 and 254.
-        final int subAddress = getSanitizedSubAddr(randomInt, ~prefixMask);
+            // Avoid selecting commonly used subnets.
+            switch (randomAddress & 0xFFFFFF00) {
+                case 0xC0A80000: // 192.168.0.0/24
+                case 0xC0A80100: // 192.168.1.0/24
+                case 0xC0A85800: // 192.168.88.0/24
+                case 0xC0A86400: // 192.168.100.0/24
+                    continue;
+            }
 
-        // Find a prefix length PREFIX_LENGTH between randomPrefixStart and the end of the block,
-        // such that the prefix does not conflict with any upstream.
-        IpPrefix downstreamPrefix = findAvailablePrefixFromRange(
-                 randomPrefixStart, (~prefixRangeMask) + 1, baseAddress, prefixRangeMask);
-        if (downstreamPrefix != null) return getLinkAddress(downstreamPrefix, subAddress);
+            // Avoid 10.0.0.0 - 10.10.255.255
+            if (randomAddress >= 0x0A000000 && randomAddress <= 0x0A0AFFFF) {
+                continue;
+            }
 
-        // If that failed, do the same, but between 0 and randomPrefixStart.
-        downstreamPrefix = findAvailablePrefixFromRange(
-                0, randomPrefixStart, baseAddress, prefixRangeMask);
-
-        return getLinkAddress(downstreamPrefix, subAddress);
-    }
-
-    private LinkAddress getLinkAddress(final IpPrefix prefix, final int subAddress) {
-        if (prefix == null) return null;
-
-        final InetAddress address = intToInet4AddressHTH(getPrefixBaseAddress(prefix) | subAddress);
-        return new LinkAddress(address, PREFIX_LENGTH);
-    }
-
-    private IpPrefix findAvailablePrefixFromRange(final int start, final int end,
-            final int baseAddress, final int prefixRangeMask) {
-        int newSubPrefix = start;
-        while (newSubPrefix < end) {
-            final InetAddress address = intToInet4AddressHTH(baseAddress | newSubPrefix);
+            final InetAddress address = intToInet4AddressHTH(randomAddress);
             final IpPrefix prefix = new IpPrefix(address, PREFIX_LENGTH);
-
-            final IpPrefix conflictPrefix = getConflictPrefix(prefix);
-
-            if (conflictPrefix == null) return prefix;
-
-            newSubPrefix = getNextSubPrefix(conflictPrefix, prefixRangeMask);
+            if (getConflictPrefix(prefix) != null) {
+                // Prefix is conflicting with another prefix used in the system, find another one.
+                continue;
+            }
+            return new LinkAddress(address, PREFIX_LENGTH);
         }
-
+        // Could not find a prefix, return null and let caller try another range.
         return null;
     }
 
     /** Get random int which could be used to generate random address. */
+    // TODO: get rid of this function and mock getRandomPrefixIndex in tests.
     @VisibleForTesting
     public int getRandomInt() {
         return mRandom.nextInt();
     }
 
-    /** Get random subAddress and avoid selecting x.x.x.0, x.x.x.1 and x.x.x.255 address. */
-    private int getSanitizedSubAddr(final int randomInt, final int subAddrMask) {
-        final int randomSubAddr = randomInt & subAddrMask;
-        // If prefix length > 30, the selecting speace would be less than 4 which may be hard to
-        // avoid 3 consecutive address.
-        if (PREFIX_LENGTH > 30) return randomSubAddr;
-
-        // TODO: maybe it is not necessary to avoid .0, .1 and .255 address because tethering
-        // address would not be conflicted. This code only works because PREFIX_LENGTH is not longer
-        // than 24
-        final int candidate = randomSubAddr & 0xff;
-        if (candidate == 0 || candidate == 1 || candidate == 255) {
-            return (randomSubAddr & 0xfffffffc) + 2;
-        }
-
-        return randomSubAddr;
-    }
-
     /** Release downstream record for IpServer. */
     public void releaseDownstream(final IpServer ipServer) {
         mDownstreams.remove(ipServer);
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 5bb1694..81f057c 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -21,6 +21,7 @@
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothPan;
 import android.content.Context;
+import android.net.ConnectivityManager;
 import android.net.INetd;
 import android.net.connectivity.ConnectivityInternalApiUtil;
 import android.net.ip.IpServer;
@@ -176,9 +177,12 @@
     /**
      * Make PrivateAddressCoordinator to be used by Tethering.
      */
-    public PrivateAddressCoordinator makePrivateAddressCoordinator(Context ctx,
-            TetheringConfiguration cfg) {
-        return new PrivateAddressCoordinator(ctx, cfg.isRandomPrefixBaseEnabled(),
+    public PrivateAddressCoordinator makePrivateAddressCoordinator(
+            Context ctx, TetheringConfiguration cfg) {
+        final ConnectivityManager cm = ctx.getSystemService(ConnectivityManager.class);
+        return new PrivateAddressCoordinator(
+                cm::getAllNetworks,
+                cfg.isRandomPrefixBaseEnabled(),
                 cfg.shouldEnableWifiP2pDedicatedIp());
     }
 
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index fc50faf..6de4062 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -62,7 +62,6 @@
 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;
@@ -111,7 +110,11 @@
     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<>();
+    // Store the last reported data usage for each upstream type to be used for calculating the
+    // usage delta. The keys are the upstream types, and the values are the tethering UID data
+    // usage for the corresponding types. Retrieve the baseline data usage when tethering is
+    // enabled, update it when the upstream changes, and clear it when tethering is disabled.
+    private final ArrayMap<UpstreamType, DataUsage> mLastReportedUpstreamUsage = new ArrayMap<>();
     private final Context mContext;
     private final Dependencies mDependencies;
     private final NetworkStatsManager mNetworkStatsManager;
@@ -157,10 +160,15 @@
 
         /**
          * @see Handler
+         *
+         * Note: This should only be called once, within the constructor, as it creates a new
+         * thread. Calling it multiple times could lead to a thread leak.
          */
         @NonNull
-        public Handler createHandler(Looper looper) {
-            return new Handler(looper);
+        public Handler createHandler() {
+            final HandlerThread thread = new HandlerThread(TAG);
+            thread.start();
+            return new Handler(thread.getLooper());
         }
     }
 
@@ -177,9 +185,7 @@
         mContext = context;
         mDependencies = dependencies;
         mNetworkStatsManager = mContext.getSystemService(NetworkStatsManager.class);
-        final HandlerThread thread = new HandlerThread(TAG);
-        thread.start();
-        mHandler = dependencies.createHandler(thread.getLooper());
+        mHandler = dependencies.createHandler();
     }
 
     @VisibleForTesting
@@ -282,22 +288,33 @@
      * Calculates the data usage difference between the current and previous usage for the
      * specified upstream type.
      *
+     * Note: This must be called before updating mCurrentUpstream when changing the upstream.
+     *
      * @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)) {
-            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);
+        if (!mDependencies.isUpstreamDataUsageMetricsEnabled(mContext)) {
+            return EMPTY;
         }
-        return EMPTY;
+
+        if (upstream == null || !isUsageSupportedForUpstreamType(upstream)) {
+            return EMPTY;
+        }
+
+        final DataUsage oldUsage = mLastReportedUpstreamUsage.getOrDefault(upstream, EMPTY);
+        if (oldUsage.equals(EMPTY)) {
+            Log.d(TAG, "No usage baseline for the upstream=" + upstream);
+            return EMPTY;
+        }
+        // TODO(b/370724247): Fix data usage which might be incorrect if the device uses
+        //  tethering with the same upstream for over 15 days.
+        // Need to refresh the baseline usage data. If the network switches back to Wi-Fi after
+        // using cellular data (Wi-Fi -> Cellular -> Wi-Fi), the old baseline might be
+        // inaccurate, leading to incorrect delta calculations.
+        final DataUsage newUsage = getCurrentDataUsageForUpstreamType(upstream);
+        mLastReportedUpstreamUsage.put(upstream, newUsage);
+        return DataUsage.subtract(newUsage, oldUsage);
     }
 
     /**
@@ -444,25 +461,29 @@
     }
 
     private void handleInitUpstreamUsageBaseline() {
-        if (!(mDependencies.isUpstreamDataUsageMetricsEnabled(mContext)
-                && mUpstreamUsageBaseline.isEmpty())) {
+        if (!mDependencies.isUpstreamDataUsageMetricsEnabled(mContext)) {
+            return;
+        }
+
+        if (!mLastReportedUpstreamUsage.isEmpty()) {
+            Log.wtf(TAG, "The upstream usage baseline has been initialed.");
             return;
         }
 
         for (UpstreamType type : UpstreamType.values()) {
             if (!isUsageSupportedForUpstreamType(type)) continue;
-            mUpstreamUsageBaseline.put(type, getCurrentDataUsageForUpstreamType(type));
+            mLastReportedUpstreamUsage.put(type, getCurrentDataUsageForUpstreamType(type));
         }
     }
 
     @VisibleForTesting
     @NonNull
-    DataUsage getDataUsageFromUpstreamType(@NonNull UpstreamType type) {
+    DataUsage getLastReportedUsageFromUpstreamType(@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);
+        return mLastReportedUpstreamUsage.getOrDefault(type, EMPTY);
     }
 
 
@@ -497,7 +518,7 @@
         mUpstreamEventList.clear();
         mCurrentUpstream = null;
         mCurrentUpStreamStartTime = 0L;
-        mUpstreamUsageBaseline.clear();
+        mLastReportedUpstreamUsage.clear();
     }
 
     private DownstreamType downstreamTypeToEnum(final int ifaceType) {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index 5d22977..dd10cc3 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -2032,7 +2032,7 @@
         final BpfCoordinator coordinator = makeBpfCoordinator();
         initBpfCoordinatorForRule4(coordinator);
         resetNetdAndBpfMaps();
-        assertEquals(0, mConsumer.getLastMaxConnectionAndResetToCurrent());
+        assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
 
         // Prepare add/delete rule events.
         final ArrayList<ConntrackEvent> addRuleEvents = new ArrayList<>();
@@ -2049,49 +2049,44 @@
         // Add rules, verify counter increases.
         for (int i = 0; i < 5; i++) {
             mConsumer.accept(addRuleEvents.get(i));
-            assertConsumerCountersEquals(supportActiveSessionsMetrics ? i + 1 : 0);
+            assertEquals(supportActiveSessionsMetrics ? i + 1 : 0,
+                    coordinator.getLastMaxConnectionAndResetToCurrent());
         }
 
         // Add the same events again should not increase the counter because
         // all events are already exist.
         for (final ConntrackEvent event : addRuleEvents) {
             mConsumer.accept(event);
-            assertConsumerCountersEquals(supportActiveSessionsMetrics ? 5 : 0);
+            assertEquals(supportActiveSessionsMetrics ? 5 : 0,
+                    coordinator.getLastMaxConnectionAndResetToCurrent());
         }
 
         // Verify removing non-existent items won't change the counters.
         for (int i = 5; i < 8; i++) {
             mConsumer.accept(new TestConntrackEvent.Builder().setMsgType(
                     IPCTNL_MSG_CT_DELETE).setProto(IPPROTO_TCP).setRemotePort(i).build());
-            assertConsumerCountersEquals(supportActiveSessionsMetrics ? 5 : 0);
+            assertEquals(supportActiveSessionsMetrics ? 5 : 0,
+                    coordinator.getLastMaxConnectionAndResetToCurrent());
         }
 
         // Verify remove the rules decrease the counter.
         // Note the max counter returns the max, so it returns the count before deleting.
         for (int i = 0; i < 5; i++) {
             mConsumer.accept(delRuleEvents.get(i));
-            assertEquals(supportActiveSessionsMetrics ? 4 - i : 0,
-                    mConsumer.getCurrentConnectionCount());
-            assertEquals(supportActiveSessionsMetrics ? 5 - i : 0,
-                    mConsumer.getLastMaxConnectionCount());
-            assertEquals(supportActiveSessionsMetrics ? 5 - i : 0,
-                    mConsumer.getLastMaxConnectionAndResetToCurrent());
         }
+        // The maximum number of rules observed is still 5.
+        assertEquals(supportActiveSessionsMetrics ? 5 : 0,
+                coordinator.getLastMaxConnectionAndResetToCurrent());
+        // After the reset, the maximum number of rules observed is 0.
+        assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
 
         // Verify remove these rules again doesn't decrease the counter.
         for (int i = 0; i < 5; i++) {
             mConsumer.accept(delRuleEvents.get(i));
-            assertConsumerCountersEquals(0);
+            assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
         }
     }
 
-    // Helper method to assert all counter values inside consumer.
-    private void assertConsumerCountersEquals(int expectedCount) {
-        assertEquals(expectedCount, mConsumer.getCurrentConnectionCount());
-        assertEquals(expectedCount, mConsumer.getLastMaxConnectionCount());
-        assertEquals(expectedCount, mConsumer.getLastMaxConnectionAndResetToCurrent());
-    }
-
     @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS)
     // BPF IPv4 forwarding only supports on S+.
     @IgnoreUpTo(Build.VERSION_CODES.R)
@@ -2121,7 +2116,7 @@
         coordinator.tetherOffloadClientAdd(mIpServer, clientB);
         assertClientInfoExists(mIpServer, clientA);
         assertClientInfoExists(mIpServer, clientB);
-        assertEquals(0, mConsumer.getLastMaxConnectionAndResetToCurrent());
+        assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
 
         // Add some rules for both clients.
         final int addr1RuleCount = 5;
@@ -2145,31 +2140,24 @@
                     .build());
         }
 
-        assertConsumerCountersEquals(
-                supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0);
+        assertEquals(supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0,
+                coordinator.getLastMaxConnectionAndResetToCurrent());
 
         // Remove 1 client. Since the 1st poll will return the LastMaxCounter and
-        // update it to the current, the max counter will be kept at 1st poll, while
-        // the current counter reflect the rule decreasing.
+        // update it to the current, the max counter will be kept at 1st poll.
         coordinator.tetherOffloadClientRemove(mIpServer, clientA);
+        assertEquals(supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0,
+                coordinator.getLastMaxConnectionAndResetToCurrent());
+        // And the counter be updated at 2nd poll.
         assertEquals(supportActiveSessionsMetrics ? addr2RuleCount : 0,
-                mConsumer.getCurrentConnectionCount());
-        assertEquals(supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0,
-                mConsumer.getLastMaxConnectionCount());
-        assertEquals(supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0,
-                mConsumer.getLastMaxConnectionAndResetToCurrent());
-        // And all counters be updated at 2nd poll.
-        assertConsumerCountersEquals(supportActiveSessionsMetrics ? addr2RuleCount : 0);
+                coordinator.getLastMaxConnectionAndResetToCurrent());
 
         // Remove other client.
         coordinator.tetherOffloadClientRemove(mIpServer, clientB);
-        assertEquals(0, mConsumer.getCurrentConnectionCount());
         assertEquals(supportActiveSessionsMetrics ? addr2RuleCount : 0,
-                mConsumer.getLastMaxConnectionCount());
-        assertEquals(supportActiveSessionsMetrics ? addr2RuleCount : 0,
-                mConsumer.getLastMaxConnectionAndResetToCurrent());
-        // All counters reach zero at 2nd poll.
-        assertConsumerCountersEquals(0);
+                coordinator.getLastMaxConnectionAndResetToCurrent());
+        // Verify the counter reach zero at 2nd poll.
+        assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
     }
 
     @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS)
@@ -2191,7 +2179,7 @@
         final BpfCoordinator coordinator = makeBpfCoordinator();
         initBpfCoordinatorForRule4(coordinator);
         resetNetdAndBpfMaps();
-        assertConsumerCountersEquals(0);
+        assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
 
         // Prepare the counter value.
         for (int i = 0; i < 5; i++) {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index 148787f..a5c06f3 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -16,7 +16,6 @@
 package com.android.networkstack.tethering;
 
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
-import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
@@ -29,9 +28,11 @@
 import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
@@ -103,6 +104,7 @@
         MockitoAnnotations.initMocks(this);
 
         when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(mConnectivityMgr);
+        when(mContext.getSystemService(ConnectivityManager.class)).thenReturn(mConnectivityMgr);
         when(mConnectivityMgr.getAllNetworks()).thenReturn(mAllNetworks);
         when(mConfig.shouldEnableWifiP2pDedicatedIp()).thenReturn(false);
         when(mConfig.isRandomPrefixBaseEnabled()).thenReturn(false);
@@ -110,7 +112,7 @@
         mPrivateAddressCoordinator =
                 spy(
                         new PrivateAddressCoordinator(
-                                mContext,
+                                mConnectivityMgr::getAllNetworks,
                                 mConfig.isRandomPrefixBaseEnabled(),
                                 mConfig.shouldEnableWifiP2pDedicatedIp()));
     }
@@ -153,37 +155,6 @@
     }
 
     @Test
-    public void testSanitizedAddress() throws Exception {
-        int fakeSubAddr = 0x2b00; // 43.0.
-        when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
-        LinkAddress actualAddress = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
-        assertEquals(new LinkAddress("192.168.43.2/24"), actualAddress);
-        mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
-
-        fakeSubAddr = 0x2d01; // 45.1.
-        when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
-        actualAddress = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
-        assertEquals(new LinkAddress("192.168.45.2/24"), actualAddress);
-        mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
-
-        fakeSubAddr = 0x2eff; // 46.255.
-        when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
-        actualAddress = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
-        assertEquals(new LinkAddress("192.168.46.254/24"), actualAddress);
-        mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
-
-        fakeSubAddr = 0x2f05; // 47.5.
-        when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
-        actualAddress = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
-        assertEquals(new LinkAddress("192.168.47.5/24"), actualAddress);
-        mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
-    }
-
-    @Test
     public void testReservedPrefix() throws Exception {
         // - Test bluetooth prefix is reserved.
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
@@ -218,22 +189,15 @@
 
     @Test
     public void testRequestLastDownstreamAddress() throws Exception {
-        final int fakeHotspotSubAddr = 0x2b05; // 43.5
-        when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
         final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
                 CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong wifi prefix: ", new LinkAddress("192.168.43.5/24"), hotspotAddress);
 
         final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
                 CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong wifi prefix: ", new LinkAddress("192.168.45.5/24"), usbAddress);
 
         mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
         mPrivateAddressCoordinator.releaseDownstream(mUsbIpServer);
 
-        final int newFakeSubAddr = 0x3c05;
-        when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
-
         final LinkAddress newHotspotAddress = requestDownstreamAddress(mHotspotIpServer,
                 CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
         assertEquals(hotspotAddress, newHotspotAddress);
@@ -271,262 +235,27 @@
     }
 
     @Test
-    public void testNoConflictUpstreamPrefix() throws Exception {
-        final int fakeHotspotSubAddr = 0x2b05; // 43.5
-        final IpPrefix predefinedPrefix = new IpPrefix("192.168.43.0/24");
-        // Force always get subAddress "43.5" for conflict testing.
-        when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
-        // - Enable hotspot with prefix 192.168.43.0/24
-        final LinkAddress hotspotAddr = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddr);
-        assertEquals("Wrong wifi prefix: ", predefinedPrefix, hotspotPrefix);
-        // - test mobile network with null NetworkCapabilities. Ideally this should not happen
-        // because NetworkCapabilities update should always happen before LinkProperties update
-        // and the UpstreamNetworkState update, just make sure no crash in this case.
-        final UpstreamNetworkState noCapUpstream = buildUpstreamNetworkState(mMobileNetwork,
-                new LinkAddress("10.0.0.8/24"), null, null);
-        updateUpstreamPrefix(noCapUpstream);
-        verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
-        // - test mobile upstream with no address.
-        final UpstreamNetworkState noAddress = buildUpstreamNetworkState(mMobileNetwork,
-                null, null, makeNetworkCapabilities(TRANSPORT_CELLULAR));
-        updateUpstreamPrefix(noCapUpstream);
-        verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
-        // - Update v6 only mobile network, hotspot prefix should not be removed.
-        final UpstreamNetworkState v6OnlyMobile = buildUpstreamNetworkState(mMobileNetwork,
-                null, new LinkAddress("2001:db8::/64"),
-                makeNetworkCapabilities(TRANSPORT_CELLULAR));
-        updateUpstreamPrefix(v6OnlyMobile);
-        verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
-        mPrivateAddressCoordinator.removeUpstreamPrefix(mMobileNetwork);
-        // - Update v4 only mobile network, hotspot prefix should not be removed.
-        final UpstreamNetworkState v4OnlyMobile = buildUpstreamNetworkState(mMobileNetwork,
-                new LinkAddress("10.0.0.8/24"), null,
-                makeNetworkCapabilities(TRANSPORT_CELLULAR));
-        updateUpstreamPrefix(v4OnlyMobile);
-        verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
-        // - Update v4v6 mobile network, hotspot prefix should not be removed.
-        final UpstreamNetworkState v4v6Mobile = buildUpstreamNetworkState(mMobileNetwork,
-                new LinkAddress("10.0.0.8/24"), new LinkAddress("2001:db8::/64"),
-                makeNetworkCapabilities(TRANSPORT_CELLULAR));
-        updateUpstreamPrefix(v4v6Mobile);
-        verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
-        // - Update v6 only wifi network, hotspot prefix should not be removed.
-        final UpstreamNetworkState v6OnlyWifi = buildUpstreamNetworkState(mWifiNetwork,
-                null, new LinkAddress("2001:db8::/64"), makeNetworkCapabilities(TRANSPORT_WIFI));
-        updateUpstreamPrefix(v6OnlyWifi);
-        verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
-        mPrivateAddressCoordinator.removeUpstreamPrefix(mWifiNetwork);
-        // - Update vpn network, it conflict with hotspot prefix but VPN networks are ignored.
-        final UpstreamNetworkState v4OnlyVpn = buildUpstreamNetworkState(mVpnNetwork,
-                new LinkAddress("192.168.43.5/24"), null, makeNetworkCapabilities(TRANSPORT_VPN));
-        updateUpstreamPrefix(v4OnlyVpn);
-        verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
-        // - Update v4 only wifi network, it conflict with hotspot prefix.
-        final UpstreamNetworkState v4OnlyWifi = buildUpstreamNetworkState(mWifiNetwork,
-                new LinkAddress("192.168.43.5/24"), null, makeNetworkCapabilities(TRANSPORT_WIFI));
-        updateUpstreamPrefix(v4OnlyWifi);
-        verify(mHotspotIpServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
-        reset(mHotspotIpServer);
-        // - Restart hotspot again and its prefix is different previous.
-        mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
-        final LinkAddress hotspotAddr2 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        final IpPrefix hotspotPrefix2 = asIpPrefix(hotspotAddr2);
-        assertNotEquals(hotspotPrefix, hotspotPrefix2);
-        updateUpstreamPrefix(v4OnlyWifi);
-        verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
-        // - Usb tethering can be enabled and its prefix is different with conflict one.
-        final LinkAddress usbAddr = requestDownstreamAddress(mUsbIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        final IpPrefix usbPrefix = asIpPrefix(usbAddr);
-        assertNotEquals(predefinedPrefix, usbPrefix);
-        assertNotEquals(hotspotPrefix2, usbPrefix);
-        // - Disable wifi upstream, then wifi's prefix can be selected again.
-        mPrivateAddressCoordinator.removeUpstreamPrefix(mWifiNetwork);
-        final LinkAddress ethAddr = requestDownstreamAddress(mEthernetIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        final IpPrefix ethPrefix = asIpPrefix(ethAddr);
-        assertEquals(predefinedPrefix, ethPrefix);
+    public void testChooseDownstreamAddress_noUpstreamConflicts() throws Exception {
+        LinkAddress address = new LinkAddress("192.168.42.42/24");
+        UpstreamNetworkState ns = buildUpstreamNetworkState(mMobileNetwork, address, null, null);
+        updateUpstreamPrefix(ns);
+        // try to look for a /24 in upstream that does not conflict with upstream -> impossible.
+        assertNull(mPrivateAddressCoordinator.chooseDownstreamAddress(asIpPrefix(address)));
+
+        IpPrefix prefix = new IpPrefix("192.168.0.0/16");
+        LinkAddress chosenAddress = mPrivateAddressCoordinator.chooseDownstreamAddress(prefix);
+        assertNotNull(chosenAddress);
+        assertTrue(prefix.containsPrefix(asIpPrefix(chosenAddress)));
     }
 
     @Test
-    public void testChooseAvailablePrefix() throws Exception {
-        final int randomAddress = 0x8605; // 134.5
-        when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomAddress);
-        final LinkAddress addr0 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        // Check whether return address is prefix 192.168.0.0/16 + subAddress 0.0.134.5.
-        assertEquals("Wrong prefix: ", new LinkAddress("192.168.134.5/24"), addr0);
-        final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
-                new LinkAddress("192.168.134.13/26"), null,
-                makeNetworkCapabilities(TRANSPORT_WIFI));
-        updateUpstreamPrefix(wifiUpstream);
-
-        // Check whether return address is next prefix of 192.168.134.0/24.
-        final LinkAddress addr1 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("192.168.135.5/24"), addr1);
-        final UpstreamNetworkState wifiUpstream2 = buildUpstreamNetworkState(mWifiNetwork,
-                new LinkAddress("192.168.149.16/19"), null,
-                makeNetworkCapabilities(TRANSPORT_WIFI));
-        updateUpstreamPrefix(wifiUpstream2);
-
-
-        // The conflict range is 128 ~ 159, so the address is 192.168.160.5/24.
-        final LinkAddress addr2 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("192.168.160.5/24"), addr2);
-        final UpstreamNetworkState mobileUpstream = buildUpstreamNetworkState(mMobileNetwork,
-                new LinkAddress("192.168.129.53/18"), null,
-                makeNetworkCapabilities(TRANSPORT_CELLULAR));
-        // Update another conflict upstream which is covered by the previous one (but not the first
-        // one) and verify whether this would affect the result.
-        final UpstreamNetworkState mobileUpstream2 = buildUpstreamNetworkState(mMobileNetwork2,
-                new LinkAddress("192.168.170.7/19"), null,
-                makeNetworkCapabilities(TRANSPORT_CELLULAR));
-        updateUpstreamPrefix(mobileUpstream);
-        updateUpstreamPrefix(mobileUpstream2);
-
-        // The conflict range are 128 ~ 159 and 159 ~ 191, so the address is 192.168.192.5/24.
-        final LinkAddress addr3 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("192.168.192.5/24"), addr3);
-        final UpstreamNetworkState mobileUpstream3 = buildUpstreamNetworkState(mMobileNetwork3,
-                new LinkAddress("192.168.188.133/17"), null,
-                makeNetworkCapabilities(TRANSPORT_CELLULAR));
-        updateUpstreamPrefix(mobileUpstream3);
-
-        // Conflict range: 128 ~ 255. The next available address is 192.168.0.5 because
-        // 192.168.134/24 ~ 192.168.255.255/24 is not available.
-        final LinkAddress addr4 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("192.168.0.5/24"), addr4);
-        final UpstreamNetworkState mobileUpstream4 = buildUpstreamNetworkState(mMobileNetwork4,
-                new LinkAddress("192.168.3.59/21"), null,
-                makeNetworkCapabilities(TRANSPORT_CELLULAR));
-        updateUpstreamPrefix(mobileUpstream4);
-
-        // Conflict ranges: 128 ~ 255 and 0 ~ 7, so the address is 192.168.8.5/24.
-        final LinkAddress addr5 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("192.168.8.5/24"), addr5);
-        final UpstreamNetworkState mobileUpstream5 = buildUpstreamNetworkState(mMobileNetwork5,
-                new LinkAddress("192.168.68.43/21"), null,
-                makeNetworkCapabilities(TRANSPORT_CELLULAR));
-        updateUpstreamPrefix(mobileUpstream5);
-
-        // Update an upstream that does *not* conflict, check whether return the same address
-        // 192.168.5/24.
-        final LinkAddress addr6 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("192.168.8.5/24"), addr6);
-        final UpstreamNetworkState mobileUpstream6 = buildUpstreamNetworkState(mMobileNetwork6,
-                new LinkAddress("192.168.10.97/21"), null,
-                makeNetworkCapabilities(TRANSPORT_CELLULAR));
-        updateUpstreamPrefix(mobileUpstream6);
-
-        // Conflict ranges: 0 ~ 15 and 128 ~ 255, so the address is 192.168.16.5/24.
-        final LinkAddress addr7 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("192.168.16.5/24"), addr7);
-        final UpstreamNetworkState mobileUpstream7 = buildUpstreamNetworkState(mMobileNetwork6,
-                new LinkAddress("192.168.0.0/17"), null,
-                makeNetworkCapabilities(TRANSPORT_CELLULAR));
-        updateUpstreamPrefix(mobileUpstream7);
-
-        // Choose prefix from next range(172.16.0.0/12) when no available prefix in 192.168.0.0/16.
-        final LinkAddress addr8 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("172.16.134.5/24"), addr8);
-    }
-
-    @Test
-    public void testChoosePrefixFromDifferentRanges() throws Exception {
-        final int randomAddress = 0x1f2b2a; // 31.43.42
-        when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomAddress);
-        final LinkAddress classC1 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        // Check whether return address is prefix 192.168.0.0/16 + subAddress 0.0.43.42.
-        assertEquals("Wrong prefix: ", new LinkAddress("192.168.43.42/24"), classC1);
-        final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
-                new LinkAddress("192.168.88.23/17"), null,
-                makeNetworkCapabilities(TRANSPORT_WIFI));
-        updateUpstreamPrefix(wifiUpstream);
-        verifyNotifyConflictAndRelease(mHotspotIpServer);
-
-        // Check whether return address is next address of prefix 192.168.128.0/17.
-        final LinkAddress classC2 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("192.168.128.42/24"), classC2);
-        final UpstreamNetworkState mobileUpstream = buildUpstreamNetworkState(mMobileNetwork,
-                new LinkAddress("192.1.2.3/8"), null,
-                makeNetworkCapabilities(TRANSPORT_CELLULAR));
-        updateUpstreamPrefix(mobileUpstream);
-        verifyNotifyConflictAndRelease(mHotspotIpServer);
-
-        // Check whether return address is under prefix 172.16.0.0/12.
-        final LinkAddress classB1 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("172.31.43.42/24"), classB1);
-        final UpstreamNetworkState mobileUpstream2 = buildUpstreamNetworkState(mMobileNetwork2,
-                new LinkAddress("172.28.123.100/14"), null,
-                makeNetworkCapabilities(TRANSPORT_CELLULAR));
-        updateUpstreamPrefix(mobileUpstream2);
-        verifyNotifyConflictAndRelease(mHotspotIpServer);
-
-        // 172.28.0.0 ~ 172.31.255.255 is not available.
-        // Check whether return address is next address of prefix 172.16.0.0/14.
-        final LinkAddress classB2 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("172.16.0.42/24"), classB2);
-
-        // Check whether new downstream is next address of address 172.16.0.42/24.
-        final LinkAddress classB3 = requestDownstreamAddress(mUsbIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("172.16.1.42/24"), classB3);
-        final UpstreamNetworkState mobileUpstream3 = buildUpstreamNetworkState(mMobileNetwork3,
-                new LinkAddress("172.16.0.1/24"), null,
-                makeNetworkCapabilities(TRANSPORT_CELLULAR));
-        updateUpstreamPrefix(mobileUpstream3);
-        verifyNotifyConflictAndRelease(mHotspotIpServer);
-        verify(mUsbIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
-
-        // Check whether return address is next address of prefix 172.16.1.42/24.
-        final LinkAddress classB4 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("172.16.2.42/24"), classB4);
-        final UpstreamNetworkState mobileUpstream4 = buildUpstreamNetworkState(mMobileNetwork4,
-                new LinkAddress("172.16.0.1/13"), null,
-                makeNetworkCapabilities(TRANSPORT_CELLULAR));
-        updateUpstreamPrefix(mobileUpstream4);
-        verifyNotifyConflictAndRelease(mHotspotIpServer);
-        verifyNotifyConflictAndRelease(mUsbIpServer);
-
-        // Check whether return address is next address of prefix 172.16.0.1/13.
-        final LinkAddress classB5 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("172.24.0.42/24"), classB5);
-        // Check whether return address is next address of prefix 172.24.0.42/24.
-        final LinkAddress classB6 = requestDownstreamAddress(mUsbIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("172.24.1.42/24"), classB6);
-        final UpstreamNetworkState mobileUpstream5 = buildUpstreamNetworkState(mMobileNetwork5,
-                new LinkAddress("172.24.0.1/12"), null,
-                makeNetworkCapabilities(TRANSPORT_CELLULAR));
-        updateUpstreamPrefix(mobileUpstream5);
-        verifyNotifyConflictAndRelease(mHotspotIpServer);
-        verifyNotifyConflictAndRelease(mUsbIpServer);
-
-        // Check whether return address is prefix 10.0.0.0/8 + subAddress 0.31.43.42.
-        final LinkAddress classA1 = requestDownstreamAddress(mHotspotIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("10.31.43.42/24"), classA1);
-        // Check whether new downstream is next address of address 10.31.43.42/24.
-        final LinkAddress classA2 = requestDownstreamAddress(mUsbIpServer,
-                CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong prefix: ", new LinkAddress("10.31.44.42/24"), classA2);
+    public void testChooseDownstreamAddress_excludesWellKnownPrefixes() throws Exception {
+        IpPrefix prefix = new IpPrefix("192.168.0.0/24");
+        assertNull(mPrivateAddressCoordinator.chooseDownstreamAddress(prefix));
+        prefix = new IpPrefix("192.168.100.0/24");
+        assertNull(mPrivateAddressCoordinator.chooseDownstreamAddress(prefix));
+        prefix = new IpPrefix("10.3.0.0/16");
+        assertNull(mPrivateAddressCoordinator.chooseDownstreamAddress(prefix));
     }
 
     private void verifyNotifyConflictAndRelease(final IpServer ipServer) throws Exception {
@@ -572,17 +301,18 @@
 
     @Test
     public void testEnableSapAndLohsConcurrently() throws Exception {
-        // 0x2b05 -> 43.5, 0x8605 -> 134.5
-        when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(0x2b05, 0x8605);
-
         final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
                 CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
-        assertEquals("Wrong hotspot prefix: ", new LinkAddress("192.168.43.5/24"), hotspotAddress);
+        assertNotNull(hotspotAddress);
 
         final LinkAddress localHotspotAddress = requestDownstreamAddress(mLocalHotspotIpServer,
                 CONNECTIVITY_SCOPE_LOCAL, true /* useLastAddress */);
-        assertEquals("Wrong local hotspot prefix: ", new LinkAddress("192.168.134.5/24"),
-                localHotspotAddress);
+        assertNotNull(localHotspotAddress);
+
+        final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress);
+        final IpPrefix localHotspotPrefix = asIpPrefix(localHotspotAddress);
+        assertFalse(hotspotPrefix.containsPrefix(localHotspotPrefix));
+        assertFalse(localHotspotPrefix.containsPrefix(hotspotPrefix));
     }
 
     @Test
@@ -615,7 +345,7 @@
         mPrivateAddressCoordinator =
                 spy(
                         new PrivateAddressCoordinator(
-                                mContext,
+                                mConnectivityMgr::getAllNetworks,
                                 mConfig.isRandomPrefixBaseEnabled(),
                                 mConfig.shouldEnableWifiP2pDedicatedIp()));
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomIntForPrefixBase);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 6ba5d48..9a4945e 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -124,6 +124,7 @@
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.hardware.usb.UsbManager;
+import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
 import android.net.EthernetManager;
 import android.net.EthernetManager.TetheredInterfaceCallback;
@@ -374,6 +375,7 @@
         @Override
         public String getSystemServiceName(Class<?> serviceClass) {
             if (TelephonyManager.class.equals(serviceClass)) return Context.TELEPHONY_SERVICE;
+            if (ConnectivityManager.class.equals(serviceClass)) return Context.CONNECTIVITY_SERVICE;
             return super.getSystemServiceName(serviceClass);
         }
     }
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 34689bc..6b646ec 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
@@ -87,13 +87,13 @@
 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.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
 
 import org.junit.After;
@@ -104,7 +104,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-@RunWith(AndroidJUnit4.class)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 public final class TetheringMetricsTest {
     @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
@@ -158,7 +159,7 @@
         mThread = new HandlerThread("TetheringMetricsTest");
         mThread.start();
         mHandler = new Handler(mThread.getLooper());
-        doReturn(mHandler).when(mDeps).createHandler(any());
+        doReturn(mHandler).when(mDeps).createHandler();
         // Set up the usage for upstream types.
         mMockUpstreamUsageBaseline.put(UT_CELLULAR, new DataUsage(100L, 200L));
         mMockUpstreamUsageBaseline.put(UT_WIFI, new DataUsage(400L, 800L));
@@ -498,7 +499,7 @@
     private void verifyEmptyUsageForAllUpstreamTypes() {
         mHandler.post(() -> {
             for (UpstreamType type : UpstreamType.values()) {
-                assertEquals(EMPTY, mTetheringMetrics.getDataUsageFromUpstreamType(type));
+                assertEquals(EMPTY, mTetheringMetrics.getLastReportedUsageFromUpstreamType(type));
             }
         });
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
@@ -555,7 +556,8 @@
 
         mHandler.post(() -> {
             for (UpstreamType type : UpstreamType.values()) {
-                final DataUsage dataUsage = mTetheringMetrics.getDataUsageFromUpstreamType(type);
+                final DataUsage dataUsage =
+                        mTetheringMetrics.getLastReportedUsageFromUpstreamType(type);
                 if (TetheringMetrics.isUsageSupportedForUpstreamType(type)) {
                     assertEquals(mMockUpstreamUsageBaseline.get(type), dataUsage);
                 } else {
@@ -610,12 +612,21 @@
         incrementCurrentTime(cellDuration);
         updateUpstreamDataUsage(UT_CELLULAR, cellUsageDiff);
 
+        // Change the upstream back to Wi-FI and update the data usage
+        runAndWaitForIdle(() ->
+                mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
+        final long wifiDuration2 = 50 * SECOND_IN_MILLIS;
+        final long wifiUsageDiff2 = 1000L;
+        incrementCurrentTime(wifiDuration2);
+        updateUpstreamDataUsage(UT_WIFI, wifiUsageDiff2);
+
         // 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);
+        addUpstreamEvent(upstreamEvents, UT_WIFI, wifiDuration2, wifiUsageDiff2, wifiUsageDiff2);
         verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
                 UserType.USER_SETTINGS, upstreamEvents,
                 currentTimeMillis() - wifiTetheringStartTime);
diff --git a/DnsResolver/Android.bp b/bpf/dns_helper/Android.bp
similarity index 100%
rename from DnsResolver/Android.bp
rename to bpf/dns_helper/Android.bp
diff --git a/DnsResolver/DnsBpfHelper.cpp b/bpf/dns_helper/DnsBpfHelper.cpp
similarity index 100%
rename from DnsResolver/DnsBpfHelper.cpp
rename to bpf/dns_helper/DnsBpfHelper.cpp
diff --git a/DnsResolver/DnsBpfHelper.h b/bpf/dns_helper/DnsBpfHelper.h
similarity index 100%
rename from DnsResolver/DnsBpfHelper.h
rename to bpf/dns_helper/DnsBpfHelper.h
diff --git a/DnsResolver/DnsBpfHelperTest.cpp b/bpf/dns_helper/DnsBpfHelperTest.cpp
similarity index 100%
rename from DnsResolver/DnsBpfHelperTest.cpp
rename to bpf/dns_helper/DnsBpfHelperTest.cpp
diff --git a/DnsResolver/DnsHelper.cpp b/bpf/dns_helper/DnsHelper.cpp
similarity index 100%
rename from DnsResolver/DnsHelper.cpp
rename to bpf/dns_helper/DnsHelper.cpp
diff --git a/DnsResolver/include/DnsHelperPublic.h b/bpf/dns_helper/include/DnsHelperPublic.h
similarity index 100%
rename from DnsResolver/include/DnsHelperPublic.h
rename to bpf/dns_helper/include/DnsHelperPublic.h
diff --git a/DnsResolver/libcom.android.tethering.dns_helper.map.txt b/bpf/dns_helper/libcom.android.tethering.dns_helper.map.txt
similarity index 100%
rename from DnsResolver/libcom.android.tethering.dns_helper.map.txt
rename to bpf/dns_helper/libcom.android.tethering.dns_helper.map.txt
diff --git a/bpf/headers/include/bpf/KernelUtils.h b/bpf/headers/include/bpf/KernelUtils.h
index 417a5c4..68bc607 100644
--- a/bpf/headers/include/bpf/KernelUtils.h
+++ b/bpf/headers/include/bpf/KernelUtils.h
@@ -55,11 +55,12 @@
            isKernelVersion(4,  9) ||  // minimum for Android S & T
            isKernelVersion(4, 14) ||  // minimum for Android U
            isKernelVersion(4, 19) ||  // minimum for Android V
-           isKernelVersion(5,  4) ||  // first supported in Android R
+           isKernelVersion(5,  4) ||  // first supported in Android R, min for W
            isKernelVersion(5, 10) ||  // first supported in Android S
            isKernelVersion(5, 15) ||  // first supported in Android T
            isKernelVersion(6,  1) ||  // first supported in Android U
-           isKernelVersion(6,  6);    // first supported in Android V
+           isKernelVersion(6,  6) ||  // first supported in Android V
+           isKernelVersion(6, 12);    // first supported in Android W
 }
 
 // Figure out the bitness of userspace.
diff --git a/bpf/loader/initrc-doc/README.txt b/bpf/loader/initrc-doc/README.txt
index 42e1fc2..2b22326 100644
--- a/bpf/loader/initrc-doc/README.txt
+++ b/bpf/loader/initrc-doc/README.txt
@@ -1,20 +1,42 @@
 This directory contains comment stripped versions of
   //system/bpf/bpfloader/bpfloader.rc
-from previous versions of Android.
+or
+  //packages/modules/Connectivity/bpf/loader/netbpfload.rc
+(as appropriate) from previous versions of Android.
 
 Generated via:
-  (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android11-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
-  (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android12-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
-  (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android13-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
-  (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android14-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
-  (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/main:bpfloader/bpfloader.rc;              ) | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2.rc
+  (cd ../../../../../../system/bpf && git cat-file -p remotes/aosp/android11-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+  (cd ../../../../../../system/bpf && git cat-file -p remotes/aosp/android12-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+  (cd ../../../../../../system/bpf && git cat-file -p remotes/aosp/android13-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+  (cd ../../../../../../system/bpf && git cat-file -p remotes/aosp/android14-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+  git cat-file -p remotes/aosp/android14-qpr2-release:netbpfload/netbpfload.rc | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2-24Q1.rc
+  git cat-file -p remotes/aosp/android14-qpr3-release:netbpfload/netbpfload.rc | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR3-24Q2.rc
+  git cat-file -p remotes/aosp/android15-release:netbpfload/netbpfload.rc      | egrep -v '^ *#' > bpfloader-sdk35-15-V-24Q3.rc
+  git cat-file -p remotes/aosp/main:bpf/loader/netbpfload.rc                   | egrep -v '^ *#' > bpfloader-sdk35-15-V-QPR1-24Q4.rc
+
+see also:
+  https://android.googlesource.com/platform/system/bpf/+/refs/heads/android11-release/bpfloader/bpfloader.rc
+  https://android.googlesource.com/platform/system/bpf/+/refs/heads/android12-release/bpfloader/bpfloader.rc
+  https://android.googlesource.com/platform/system/bpf/+/refs/heads/android13-release/bpfloader/bpfloader.rc
+  https://android.googlesource.com/platform/system/bpf/+/refs/heads/android14-release/bpfloader/bpfloader.rc
+  https://android.googlesource.com/platform/system/bpf/+/refs/heads/android14-qpr1-release/bpfloader/bpfloader.rc
+  https://android.googlesource.com/platform/system/bpf/+/refs/heads/android14-qpr2-release/bpfloader/ (rc file is gone in QPR2)
+  https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/android14-qpr2-release/netbpfload/netbpfload.rc
+  https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/android14-qpr3-release/netbpfload/netbpfload.rc
+  https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/android15-release/netbpfload/netbpfload.rc
+  https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/android15-qpr1-release/netbpfload/netbpfload.rc
+  https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/main/netbpfload/netbpfload.rc
+or:
+  https://googleplex-android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/24Q1-release/netbpfload/netbpfload.rc
+  https://googleplex-android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/24Q2-release/netbpfload/netbpfload.rc
+  https://googleplex-android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/24Q3-release/netbpfload/netbpfload.rc
+  https://googleplex-android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/24Q4-release/bpf/loader/netbpfload.rc
 
 this is entirely equivalent to:
   (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
   (cd /android1/system/bpf && git cat-file -p remotes/goog/sc-dev:bpfloader/bpfloader.rc;  ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
   (cd /android1/system/bpf && git cat-file -p remotes/goog/tm-dev:bpfloader/bpfloader.rc;  ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
   (cd /android1/system/bpf && git cat-file -p remotes/goog/udc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
-  (cd /android1/system/bpf && git cat-file -p remotes/goog/main:bpfloader/bpfloader.rc;    ) | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2.rc
 
 it is also equivalent to:
   (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
@@ -29,34 +51,66 @@
 
 Key takeaways:
 
-= R bpfloader:
+= R bpfloader (platform)
   - CHOWN + SYS_ADMIN
   - asynchronous startup
   - platform only
   - proc file setup handled by initrc
 
-= S bpfloader
+= S bpfloader (platform)
   - adds NET_ADMIN
   - synchronous startup
   - platform + mainline tethering offload
 
-= T bpfloader
+= T bpfloader (platform)
   - platform + mainline networking (including tethering offload)
   - supported btf for maps via exec of btfloader
 
-= U bpfloader
+= U bpfloader (platform)
   - proc file setup moved into bpfloader binary
   - explicitly specified user and groups:
     group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
     user root
 
-= U QPR2 bpfloader
+= U QPR2 [24Q1] bpfloader (platform netbpfload -> platform bpfloader)
   - drops support of btf for maps
   - invocation of /system/bin/netbpfload binary, which after handling *all*
     networking bpf related things executes the platform /system/bin/bpfloader
     which handles non-networking bpf.
+  - Note: this does not (by itself) call into apex NetBpfLoad
+
+= U QPR3 [24Q2] bpfloader (platform netbpfload -> apex netbpfload -> platform bpfloader)
+  - platform NetBpfload *always* execs into apex NetBpfLoad,
+  - shipped with mainline tethering apex that includes NetBpfLoad binary.
+
+= V [24Q3] bpfloader (apex netbpfload -> platform bpfloader)
+  - no significant changes, though it does hard require the apex NetBpfLoad
+    by virtue of the platform NetBpfLoad no longer being present.
+    ie. the apex must override the platform 'bpfloader' service for 35+:
+    the V FRC M-2024-08+ tethering apex does this.
+
+= V QPR1 [24Q4] bpfloader (apex netbpfload -> platform bpfloader)
+  - made netd start earlier (previously happened in parallel to zygote)
+  - renamed and moved the trigger out of netbpload.rc into
+    //system/core/rootdir/init.rc
+  - the new sequence is:
+      trigger post-fs-data        (logd available, starts apexd)
+      trigger load-bpf-programs   (does: exec_start bpfloader)
+      trigger bpf-progs-loaded    (does: start netd)
+      trigger zygote-start
+  - this is more or less irrelevant from the point of view of the bpfloader,
+    but it does mean netd init could fail and abort the boot earlier,
+    before 'A/B update_verifier marks a successful boot'.
+    Though note that due to netd being started asynchronously, it is racy.
 
 Note that there is now a copy of 'netbpfload' provided by the tethering apex
 mainline module at /apex/com.android.tethering/bin/netbpfload, which due
 to the use of execve("/system/bin/bpfloader") relies on T+ selinux which was
 added for btf map support (specifically the ability to exec the "btfloader").
+
+= mainline tethering apex M-2024-08+ overrides the platform service for V+
+  thus loading mainline (ie. networking) bpf programs from mainline 'NetBpfLoad'
+  and platform ones from platform 'bpfloader'.
+
+= mainline tethering apex M-2024-09+ changes T+ behaviour (U QPR3+ unaffected)
+  netd -> netd_updatable.so -> ctl.start=mdnsd_netbpfload -> load net bpf programs
diff --git a/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc b/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2-24Q1.rc
similarity index 100%
copy from bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
copy to bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2-24Q1.rc
diff --git a/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc b/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR3-24Q2.rc
similarity index 100%
rename from bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR3-24Q2.rc
diff --git a/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR3.rc b/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR3.rc
deleted file mode 100644
index 8f3f462..0000000
--- a/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR3.rc
+++ /dev/null
@@ -1,11 +0,0 @@
-on load_bpf_programs
-    exec_start bpfloader
-
-service bpfloader /system/bin/netbpfload
-    capabilities CHOWN SYS_ADMIN NET_ADMIN
-    group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
-    user root
-    rlimit memlock 1073741824 1073741824
-    oneshot
-    reboot_on_failure reboot,bpfloader-failed
-    updatable
diff --git a/bpf/loader/initrc-doc/bpfloader-sdk35-15-V.rc b/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-24Q3.rc
similarity index 100%
rename from bpf/loader/initrc-doc/bpfloader-sdk35-15-V.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk35-15-V-24Q3.rc
diff --git a/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-QPR1-24Q4.rc b/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-QPR1-24Q4.rc
new file mode 100644
index 0000000..e2639ac
--- /dev/null
+++ b/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-QPR1-24Q4.rc
@@ -0,0 +1,5 @@
+service bpfloader /system/bin/false
+    user root
+    oneshot
+    reboot_on_failure reboot,netbpfload-missing
+    updatable
diff --git a/bpf/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
index 5dea851..8e4c2c6 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -201,7 +201,7 @@
     }
 }
 
-Status BpfHandler::init(const char* cg2_path) {
+static inline void waitForBpf() {
     // Note: netd *can* be restarted, so this might get called a second time after boot is complete
     // at which point we don't need to (and shouldn't) wait for (more importantly start) loading bpf
 
@@ -229,6 +229,21 @@
     }
 
     ALOGI("BPF programs are loaded");
+}
+
+Status BpfHandler::init(const char* cg2_path) {
+    // This wait is effectively a no-op on U QPR3+ devices (as netd starts
+    // *after* the synchronous 'exec_start bpfloader' which calls NetBpfLoad)
+    // but checking for U QPR3 is hard.
+    //
+    // Waiting should not be required on U QPR3+ devices,
+    // ...
+    //
+    // ...unless someone changed 'exec_start bpfloader' to 'start bpfloader'
+    // in the rc file.
+    //
+    // TODO: should be: if (!modules::sdklevel::IsAtLeastW())
+    if (android_get_device_api_level() <= __ANDROID_API_V__) waitForBpf();
 
     RETURN_IF_NOT_OK(initPrograms(cg2_path));
     RETURN_IF_NOT_OK(initMaps());
diff --git a/bpf/progs/Android.bp b/bpf/progs/Android.bp
index 52eb1b3..20d194c 100644
--- a/bpf/progs/Android.bp
+++ b/bpf/progs/Android.bp
@@ -47,8 +47,8 @@
         "com.android.tethering",
     ],
     visibility: [
+        "//packages/modules/Connectivity/bpf/dns_helper",
         "//packages/modules/Connectivity/bpf/netd",
-        "//packages/modules/Connectivity/DnsResolver",
         "//packages/modules/Connectivity/service",
         "//packages/modules/Connectivity/service/native/libs/libclat",
         "//packages/modules/Connectivity/Tethering",
diff --git a/bpf/progs/offload.c b/bpf/progs/offload.c
index 7e1184d..631908a 100644
--- a/bpf/progs/offload.c
+++ b/bpf/progs/offload.c
@@ -85,9 +85,8 @@
 
     // Since the program never writes via DPA (direct packet access) auto-pull/unclone logic does
     // not trigger and thus we need to manually make sure we can read packet headers via DPA.
-    // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter.
     // It has to be done early cause it will invalidate any skb->data/data_end derived pointers.
-    try_make_writable(skb, l2_header_size + IP6_HLEN + TCP_HLEN);
+    if (bpf_skb_pull_data(skb, l2_header_size + IP6_HLEN)) return TC_ACT_PIPE;
 
     void* data = (void*)(long)skb->data;
     const void* data_end = (void*)(long)skb->data_end;
@@ -110,6 +109,14 @@
     // If hardware offload is running and programming flows based on conntrack entries,
     // try not to interfere with it.
     if (ip6->nexthdr == IPPROTO_TCP) {
+        // don't need to check return code, as it's effectively checked in the next 'if' below
+        bpf_skb_pull_data(skb, l2_header_size + IP6_HLEN + TCP_HLEN);
+
+        data = (void*)(long)skb->data;
+        data_end = (void*)(long)skb->data_end;
+        eth = is_ethernet ? data : NULL;  // used iff is_ethernet
+        ip6 = is_ethernet ? (void*)(eth + 1) : data;
+
         struct tcphdr* tcph = (void*)(ip6 + 1);
 
         // Make sure we can get at the tcp header
diff --git a/common/thread_flags.aconfig b/common/thread_flags.aconfig
index c11c6c0..14b70d0 100644
--- a/common/thread_flags.aconfig
+++ b/common/thread_flags.aconfig
@@ -26,3 +26,12 @@
     description: "Controls whether the Android Thread setting max power of channel feature is enabled"
     bug: "346686506"
 }
+
+flag {
+    name: "epskc_enabled"
+    is_exported: true
+    is_fixed_read_only: true
+    namespace: "thread_network"
+    description: "Controls whether the Android Thread Ephemeral Key feature is enabled"
+    bug: "348323500"
+}
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 9f26bcf..09a3681 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -507,7 +507,10 @@
   }
 
   @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkController {
+    method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void activateEphemeralKeyMode(@NonNull java.time.Duration, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
     method public void createRandomizedDataset(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.thread.ActiveOperationalDataset,android.net.thread.ThreadNetworkException>);
+    method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void deactivateEphemeralKeyMode(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+    method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @NonNull public java.time.Duration getMaxEphemeralKeyLifetime();
     method public int getThreadVersion();
     method public static boolean isAttached(int);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void join(@NonNull android.net.thread.ActiveOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
@@ -526,6 +529,9 @@
     field public static final int DEVICE_ROLE_LEADER = 4; // 0x4
     field public static final int DEVICE_ROLE_ROUTER = 3; // 0x3
     field public static final int DEVICE_ROLE_STOPPED = 0; // 0x0
+    field @FlaggedApi("com.android.net.thread.flags.epskc_enabled") public static final int EPHEMERAL_KEY_DISABLED = 0; // 0x0
+    field @FlaggedApi("com.android.net.thread.flags.epskc_enabled") public static final int EPHEMERAL_KEY_ENABLED = 1; // 0x1
+    field @FlaggedApi("com.android.net.thread.flags.epskc_enabled") public static final int EPHEMERAL_KEY_IN_USE = 2; // 0x2
     field public static final int MAX_POWER_CHANNEL_DISABLED = -2147483648; // 0x80000000
     field public static final int STATE_DISABLED = 0; // 0x0
     field public static final int STATE_DISABLING = 2; // 0x2
@@ -540,6 +546,7 @@
 
   public static interface ThreadNetworkController.StateCallback {
     method public void onDeviceRoleChanged(int);
+    method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public default void onEphemeralKeyStateChanged(int, @Nullable String, @Nullable java.time.Instant);
     method public default void onPartitionIdChanged(long);
     method public default void onThreadEnableStateChanged(int);
   }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
index cfeca5d..e52dd2f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
@@ -107,7 +107,7 @@
         final QueryTaskConfig nextRunConfig = currentConfig.getConfigForNextRun(queryMode);
         long timeToRun;
         if (mLastScheduledQueryTaskArgs == null && !forceEnableBackoff) {
-            timeToRun = now + nextRunConfig.delayUntilNextTaskWithoutBackoffMs;
+            timeToRun = now + nextRunConfig.delayBeforeTaskWithoutBackoffMs;
         } else {
             timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
                     nextRunConfig, now, minRemainingTtl, lastSentTime, numOfQueriesBeforeBackoff,
@@ -133,7 +133,7 @@
     private static long calculateTimeToRun(@Nullable ScheduledQueryTaskArgs taskArgs,
             QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime,
             int numOfQueriesBeforeBackoff, boolean forceEnableBackoff) {
-        final long baseDelayInMs = queryTaskConfig.delayUntilNextTaskWithoutBackoffMs;
+        final long baseDelayInMs = queryTaskConfig.delayBeforeTaskWithoutBackoffMs;
         if (!(forceEnableBackoff
                 || queryTaskConfig.shouldUseQueryBackoff(numOfQueriesBeforeBackoff))) {
             return lastSentTime + baseDelayInMs;
diff --git a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
index d2cd463..4e74159 100644
--- a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
+++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
@@ -55,22 +55,22 @@
     private final int queriesPerBurst;
     private final int timeBetweenBurstsInMs;
     private final int burstCounter;
-    final long delayUntilNextTaskWithoutBackoffMs;
+    final long delayBeforeTaskWithoutBackoffMs;
     private final boolean isFirstBurst;
-    private final long queryCount;
+    private final long queryIndex;
 
-    QueryTaskConfig(long queryCount, int transactionId,
+    QueryTaskConfig(long queryIndex, int transactionId,
             boolean expectUnicastResponse, boolean isFirstBurst, int burstCounter,
             int queriesPerBurst, int timeBetweenBurstsInMs,
-            long delayUntilNextTaskWithoutBackoffMs) {
+            long delayBeforeTaskWithoutBackoffMs) {
         this.transactionId = transactionId;
         this.expectUnicastResponse = expectUnicastResponse;
         this.queriesPerBurst = queriesPerBurst;
         this.timeBetweenBurstsInMs = timeBetweenBurstsInMs;
         this.burstCounter = burstCounter;
-        this.delayUntilNextTaskWithoutBackoffMs = delayUntilNextTaskWithoutBackoffMs;
+        this.delayBeforeTaskWithoutBackoffMs = delayBeforeTaskWithoutBackoffMs;
         this.isFirstBurst = isFirstBurst;
-        this.queryCount = queryCount;
+        this.queryIndex = queryIndex;
     }
 
     QueryTaskConfig(int queryMode) {
@@ -82,26 +82,26 @@
         // Config the scan frequency based on the scan mode.
         if (queryMode == AGGRESSIVE_QUERY_MODE) {
             this.timeBetweenBurstsInMs = INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
-            this.delayUntilNextTaskWithoutBackoffMs =
+            this.delayBeforeTaskWithoutBackoffMs =
                     TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
         } else if (queryMode == PASSIVE_QUERY_MODE) {
             // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then
             // in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
             // queries.
             this.timeBetweenBurstsInMs = MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
-            this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
+            this.delayBeforeTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
         } else {
             // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
             // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
             // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
             // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
             this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS;
-            this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
+            this.delayBeforeTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
         }
-        this.queryCount = 0;
+        this.queryIndex = 0;
     }
 
-    long getDelayUntilNextTaskWithoutBackoff(boolean isFirstQueryInBurst,
+    long getDelayBeforeNextTaskWithoutBackoff(boolean isFirstQueryInBurst,
             boolean isLastQueryInBurst, int queryMode) {
         if (isFirstQueryInBurst && queryMode == AGGRESSIVE_QUERY_MODE) {
             return 0;
@@ -137,7 +137,7 @@
      * Get new QueryTaskConfig for next run.
      */
     public QueryTaskConfig getConfigForNextRun(int queryMode) {
-        long newQueryCount = queryCount + 1;
+        long newQueryCount = queryIndex + 1;
         int newTransactionId = transactionId + 1;
         if (newTransactionId > UNSIGNED_SHORT_MAX_VALUE) {
             newTransactionId = 1;
@@ -162,7 +162,7 @@
                 getNextExpectUnicastResponse(isLastQueryInBurst, queryMode), newIsFirstBurst,
                 newBurstCounter, newQueriesPerBurst,
                 getNextTimeBetweenBurstsMs(isLastQueryInBurst, queryMode),
-                getDelayUntilNextTaskWithoutBackoff(
+                getDelayBeforeNextTaskWithoutBackoff(
                         isFirstQueryInBurst, isLastQueryInBurst, queryMode));
     }
 
@@ -174,6 +174,6 @@
         if (burstCounter != 0 || isFirstBurst) {
             return false;
         }
-        return queryCount > numOfQueriesBeforeBackoff;
+        return queryIndex > numOfQueriesBeforeBackoff;
     }
 }
diff --git a/service/ServiceConnectivityResources/Android.bp b/service/ServiceConnectivityResources/Android.bp
index 2621256..be9b2b5 100644
--- a/service/ServiceConnectivityResources/Android.bp
+++ b/service/ServiceConnectivityResources/Android.bp
@@ -33,6 +33,7 @@
         "com.android.tethering",
     ],
     certificate: ":com.android.connectivity.resources.certificate",
+    updatable: true,
 }
 
 android_app_certificate {
diff --git a/thread/framework/java/android/net/thread/IStateCallback.aidl b/thread/framework/java/android/net/thread/IStateCallback.aidl
index 9d0a571..57c365b 100644
--- a/thread/framework/java/android/net/thread/IStateCallback.aidl
+++ b/thread/framework/java/android/net/thread/IStateCallback.aidl
@@ -23,4 +23,6 @@
     void onDeviceRoleChanged(int deviceRole);
     void onPartitionIdChanged(long partitionId);
     void onThreadEnableStateChanged(int enabledState);
+    void onEphemeralKeyStateChanged(
+            int ephemeralKeyState, @nullable String ephemeralKey, long expiryMillis);
 }
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index b7f68c9..e9cbb83 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -53,4 +53,7 @@
     void setConfiguration(in ThreadConfiguration config, in IOperationReceiver receiver);
     void registerConfigurationCallback(in IConfigurationReceiver receiver);
     void unregisterConfigurationCallback(in IConfigurationReceiver receiver);
+
+    void activateEphemeralKeyMode(long lifetimeMillis, in IOperationReceiver receiver);
+    void deactivateEphemeralKeyMode(in IOperationReceiver receiver);
 }
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index cb4e8de..1222398 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -40,6 +40,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.time.Duration;
+import java.time.Instant;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.Executor;
@@ -82,6 +83,25 @@
     /** The Thread radio is being disabled. */
     public static final int STATE_DISABLING = 2;
 
+    /** The ephemeral key mode is disabled. */
+    @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+    public static final int EPHEMERAL_KEY_DISABLED = 0;
+
+    /**
+     * The ephemeral key mode is enabled, an external commissioner candidate can use the ephemeral
+     * key to connect to this device and get Thread credential shared.
+     */
+    @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+    public static final int EPHEMERAL_KEY_ENABLED = 1;
+
+    /**
+     * The ephemeral key is in use. This state means there is already an active secure session
+     * connected to this device with the ephemeral key, it's not possible to use the ephemeral key
+     * for new connections in this state.
+     */
+    @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+    public static final int EPHEMERAL_KEY_IN_USE = 2;
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
@@ -100,6 +120,13 @@
             value = {STATE_DISABLED, STATE_ENABLED, STATE_DISABLING})
     public @interface EnabledState {}
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            prefix = {"EPHEMERAL_KEY_"},
+            value = {EPHEMERAL_KEY_DISABLED, EPHEMERAL_KEY_ENABLED, EPHEMERAL_KEY_IN_USE})
+    public @interface EphemeralKeyState {}
+
     /** Thread standard version 1.3. */
     public static final int THREAD_VERSION_1_3 = 4;
 
@@ -110,6 +137,9 @@
     @SuppressLint("MinMaxConstant")
     public static final int MAX_POWER_CHANNEL_DISABLED = Integer.MIN_VALUE;
 
+    /** The maximum lifetime of an ephemeral key. @hide */
+    @NonNull private static final Duration EPHEMERAL_KEY_LIFETIME_MAX = Duration.ofMinutes(10);
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({THREAD_VERSION_1_3})
@@ -174,6 +204,87 @@
         }
     }
 
+    /** Returns the maximum lifetime allowed when activating ephemeral key mode. */
+    @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+    @NonNull
+    public Duration getMaxEphemeralKeyLifetime() {
+        return EPHEMERAL_KEY_LIFETIME_MAX;
+    }
+
+    /**
+     * Activates ephemeral key mode with a given {@code lifetime}. The ephemeral key is a temporary,
+     * single-use numeric code that is used for Thread Administration Sharing. After activation, the
+     * mode may expire or get deactivated, caller to this method should subscribe to the ephemeral
+     * key state updates with {@link #registerStateCallback} to get notified when the ephemeral key
+     * state changes.
+     *
+     * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. The ephemeral
+     * key string contains a sequence of numeric digits 0-9 of user-input friendly length (typically
+     * 9). Subscribers to ephemeral key state updates with {@link #registerStateCallback} will be
+     * notified with a call to {@link #onEphemeralKeyStateChanged}.
+     *
+     * <p>On failure, {@link OutcomeReceiver#onError} of {@code receiver} will be invoked with a
+     * specific error:
+     *
+     * <ul>
+     *   <li>{@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} when this device is not
+     *       attached to Thread network
+     *   <li>{@link ThreadNetworkException#ERROR_BUSY} when ephemeral key mode is already activated
+     *       on the device, caller can recover from this error when the ephemeral key mode gets
+     *       deactivated
+     * </ul>
+     *
+     * @param lifetime valid lifetime of the generated ephemeral key, should be larger than {@link
+     *     Duration#ZERO} and at most the duration returned by {@link #getMaxEphemeralKeyLifetime}.
+     * @param executor the executor on which to execute {@code receiver}
+     * @param receiver the receiver to receive the result of this operation
+     * @throws IllegalArgumentException if the {@code lifetime} exceeds the allowed range
+     */
+    @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+    @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+    public void activateEphemeralKeyMode(
+            @NonNull Duration lifetime,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+        if (lifetime.compareTo(Duration.ZERO) <= 0
+                || lifetime.compareTo(EPHEMERAL_KEY_LIFETIME_MAX) > 0) {
+            throw new IllegalArgumentException(
+                    "Invalid ephemeral key lifetime: the value must be in range of (0, "
+                            + EPHEMERAL_KEY_LIFETIME_MAX
+                            + "]");
+        }
+        long lifetimeMillis = lifetime.toMillis();
+        try {
+            mControllerService.activateEphemeralKeyMode(
+                    lifetimeMillis, new OperationReceiverProxy(executor, receiver));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Deactivates ephemeral key mode. If there is an active connection with the ephemeral key, the
+     * connection will be terminated.
+     *
+     * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. The call will
+     * always succeed if the device is not in ephemeral key mode.
+     *
+     * @param executor the executor to execute {@code receiver}
+     * @param receiver the receiver to receive the result of this operation
+     */
+    @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+    @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+    public void deactivateEphemeralKeyMode(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+        try {
+            mControllerService.deactivateEphemeralKeyMode(
+                    new OperationReceiverProxy(executor, receiver));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     /** Returns the Thread version this device is operating on. */
     @ThreadVersion
     public int getThreadVersion() {
@@ -248,6 +359,24 @@
          * @param enabledState the new Thread enabled state
          */
         default void onThreadEnableStateChanged(@EnabledState int enabledState) {}
+
+        /**
+         * The ephemeral key state has changed.
+         *
+         * @param ephemeralKeyState the ephemeral key state
+         * @param ephemeralKey the ephemeral key string which contains a sequence of numeric digits
+         *     0-9 of user-input friendly length (typically 9), or {@code null} if {@code
+         *     ephemeralKeyState} is {@link #EPHEMERAL_KEY_DISABLED} or the caller doesn't have the
+         *     permission {@link android.permission.THREAD_NETWORK_PRIVILEGED}
+         * @param expiry a timestamp of when the ephemeral key will expireor {@code null} if {@code
+         *     ephemeralKeyState} is {@link #EPHEMERAL_KEY_DISABLED}
+         */
+        @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
+        @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+        default void onEphemeralKeyStateChanged(
+                @EphemeralKeyState int ephemeralKeyState,
+                @Nullable String ephemeralKey,
+                @Nullable Instant expiry) {}
     }
 
     private static final class StateCallbackProxy extends IStateCallback.Stub {
@@ -288,13 +417,37 @@
                 Binder.restoreCallingIdentity(identity);
             }
         }
+
+        @Override
+        public void onEphemeralKeyStateChanged(
+                @EphemeralKeyState int ephemeralKeyState, String ephemeralKey, long expiryMillis) {
+            if (!Flags.epskcEnabled()) {
+                throw new IllegalStateException(
+                        "This should not be called when Ephemeral key API is disabled");
+            }
+
+            final long identity = Binder.clearCallingIdentity();
+            final Instant expiry =
+                    ephemeralKeyState == EPHEMERAL_KEY_DISABLED
+                            ? null
+                            : Instant.ofEpochMilli(expiryMillis);
+
+            try {
+                mExecutor.execute(
+                        () ->
+                                mCallback.onEphemeralKeyStateChanged(
+                                        ephemeralKeyState, ephemeralKey, expiry));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
     }
 
     /**
      * Registers a callback to be called when Thread network states are changed.
      *
-     * <p>Upon return of this method, methods of {@code callback} will be invoked immediately with
-     * existing states.
+     * <p>Upon return of this method, all methods of {@code callback} will be invoked immediately
+     * with existing states. The order of the invoked callbacks is not guaranteed.
      *
      * @param executor the executor to execute the {@code callback}
      * @param callback the callback to receive Thread network state changes
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 57fea34..3d854d7 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -15,6 +15,7 @@
 package com.android.server.thread;
 
 import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.net.MulticastRoutingConfig.CONFIG_FORWARD_NONE;
 import static android.net.MulticastRoutingConfig.FORWARD_SELECTED;
 import static android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE;
@@ -26,6 +27,7 @@
 import static android.net.thread.ActiveOperationalDataset.MESH_LOCAL_PREFIX_FIRST_BYTE;
 import static android.net.thread.ActiveOperationalDataset.SecurityPolicy.DEFAULT_ROTATION_TIME_HOURS;
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
+import static android.net.thread.ThreadNetworkController.EPHEMERAL_KEY_DISABLED;
 import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
 import static android.net.thread.ThreadNetworkController.STATE_DISABLING;
 import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
@@ -855,6 +857,47 @@
     }
 
     @Override
+    public void activateEphemeralKeyMode(long lifetimeMillis, IOperationReceiver receiver) {
+        enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+        mHandler.post(
+                () ->
+                        activateEphemeralKeyModeInternal(
+                                lifetimeMillis, new OperationReceiverWrapper(receiver)));
+    }
+
+    private void activateEphemeralKeyModeInternal(
+            long lifetimeMillis, OperationReceiverWrapper receiver) {
+        checkOnHandlerThread();
+
+        try {
+            getOtDaemon().activateEphemeralKeyMode(lifetimeMillis, newOtStatusReceiver(receiver));
+        } catch (RemoteException | ThreadNetworkException e) {
+            LOG.e("otDaemon.activateEphemeralKeyMode failed", e);
+            receiver.onError(e);
+        }
+    }
+
+    @Override
+    public void deactivateEphemeralKeyMode(IOperationReceiver receiver) {
+        enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+        mHandler.post(
+                () -> deactivateEphemeralKeyModeInternal(new OperationReceiverWrapper(receiver)));
+    }
+
+    private void deactivateEphemeralKeyModeInternal(OperationReceiverWrapper receiver) {
+        checkOnHandlerThread();
+
+        try {
+            getOtDaemon().deactivateEphemeralKeyMode(newOtStatusReceiver(receiver));
+        } catch (RemoteException | ThreadNetworkException e) {
+            LOG.e("otDaemon.deactivateEphemeralKeyMode failed", e);
+            receiver.onError(e);
+        }
+    }
+
+    @Override
     public void createRandomizedDataset(
             String networkName, IActiveOperationalDatasetReceiver receiver) {
         ActiveOperationalDatasetReceiverWrapper receiverWrapper =
@@ -1003,7 +1046,14 @@
     @Override
     public void registerStateCallback(IStateCallback stateCallback) throws RemoteException {
         enforceAllPermissionsGranted(permission.ACCESS_NETWORK_STATE);
-        mHandler.post(() -> mOtDaemonCallbackProxy.registerStateCallback(stateCallback));
+        boolean hasThreadPrivilegedPermission =
+                (mContext.checkCallingOrSelfPermission(PERMISSION_THREAD_NETWORK_PRIVILEGED)
+                        == PERMISSION_GRANTED);
+
+        mHandler.post(
+                () ->
+                        mOtDaemonCallbackProxy.registerStateCallback(
+                                stateCallback, hasThreadPrivilegedPermission));
     }
 
     @Override
@@ -1458,9 +1508,13 @@
 
         final IBinder.DeathRecipient deathRecipient;
 
-        CallbackMetadata(IBinder.DeathRecipient deathRecipient) {
+        final boolean hasThreadPrivilegedPermission;
+
+        CallbackMetadata(
+                IBinder.DeathRecipient deathRecipient, boolean hasThreadPrivilegedPermission) {
             this.id = allocId();
             this.deathRecipient = deathRecipient;
+            this.hasThreadPrivilegedPermission = hasThreadPrivilegedPermission;
         }
 
         private static long allocId() {
@@ -1502,7 +1556,8 @@
         private ActiveOperationalDataset mActiveDataset;
         private PendingOperationalDataset mPendingDataset;
 
-        public void registerStateCallback(IStateCallback callback) {
+        public void registerStateCallback(
+                IStateCallback callback, boolean hasThreadPrivilegedPermission) {
             checkOnHandlerThread();
             if (mStateCallbacks.containsKey(callback)) {
                 throw new IllegalStateException("Registering the same IStateCallback twice");
@@ -1510,7 +1565,8 @@
 
             IBinder.DeathRecipient deathRecipient =
                     () -> mHandler.post(() -> unregisterStateCallback(callback));
-            CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
+            CallbackMetadata callbackMetadata =
+                    new CallbackMetadata(deathRecipient, hasThreadPrivilegedPermission);
             mStateCallbacks.put(callback, callbackMetadata);
             try {
                 callback.asBinder().linkToDeath(deathRecipient, 0);
@@ -1543,7 +1599,8 @@
 
             IBinder.DeathRecipient deathRecipient =
                     () -> mHandler.post(() -> unregisterDatasetCallback(callback));
-            CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
+            CallbackMetadata callbackMetadata =
+                    new CallbackMetadata(deathRecipient, true /* hasThreadPrivilegedPermission */);
             mOpDatasetCallbacks.put(callback, callbackMetadata);
             try {
                 callback.asBinder().linkToDeath(deathRecipient, 0);
@@ -1622,16 +1679,18 @@
         }
 
         @Override
-        public void onStateChanged(OtDaemonState newState, long listenerId) {
+        public void onStateChanged(@NonNull OtDaemonState newState, long listenerId) {
             mHandler.post(() -> onStateChangedInternal(newState, listenerId));
         }
 
         private void onStateChangedInternal(OtDaemonState newState, long listenerId) {
             checkOnHandlerThread();
+
             onInterfaceStateChanged(newState.isInterfaceUp);
             onDeviceRoleChanged(newState.deviceRole, listenerId);
             onPartitionIdChanged(newState.partitionId, listenerId);
             onThreadEnabledChanged(newState.threadEnabled, listenerId);
+            onEphemeralKeyStateChanged(newState, listenerId);
             mState = newState;
 
             ActiveOperationalDataset newActiveDataset;
@@ -1707,6 +1766,43 @@
             }
         }
 
+        private void onEphemeralKeyStateChanged(OtDaemonState newState, long listenerId) {
+            checkOnHandlerThread();
+            boolean hasChange = isEphemeralKeyStateChanged(mState, newState);
+
+            for (var callbackEntry : mStateCallbacks.entrySet()) {
+                if (!hasChange && callbackEntry.getValue().id != listenerId) {
+                    continue;
+                }
+                String passcode =
+                        callbackEntry.getValue().hasThreadPrivilegedPermission
+                                ? newState.ephemeralKeyPasscode
+                                : null;
+                if (newState.ephemeralKeyState == EPHEMERAL_KEY_DISABLED) {
+                    passcode = null;
+                }
+                try {
+                    callbackEntry
+                            .getKey()
+                            .onEphemeralKeyStateChanged(
+                                    newState.ephemeralKeyState,
+                                    passcode,
+                                    newState.ephemeralKeyExpiryMillis);
+                } catch (RemoteException ignored) {
+                    // do nothing if the client is dead
+                }
+            }
+        }
+
+        private static boolean isEphemeralKeyStateChanged(
+                OtDaemonState oldState, @NonNull OtDaemonState newState) {
+            if (oldState == null) return true;
+            if (oldState.ephemeralKeyState != newState.ephemeralKeyState) return true;
+            if (oldState.ephemeralKeyState == EPHEMERAL_KEY_DISABLED) return false;
+            return (!Objects.equals(oldState.ephemeralKeyPasscode, newState.ephemeralKeyPasscode)
+                    || oldState.ephemeralKeyExpiryMillis != newState.ephemeralKeyExpiryMillis);
+        }
+
         private void onActiveOperationalDatasetChanged(
                 ActiveOperationalDataset activeDataset, long listenerId) {
             checkOnHandlerThread();
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
index 34aabe2..e954d3b 100644
--- a/thread/tests/cts/AndroidTest.xml
+++ b/thread/tests/cts/AndroidTest.xml
@@ -56,4 +56,14 @@
         <!-- Ignores tests introduced by guava-android-testlib -->
         <option name="exclude-annotation" value="org.junit.Ignore"/>
     </test>
+
+    <!--
+        This doesn't override a read-only flag, to run the tests locally with `epskc_enabled` flag
+        enabled, set the flag to `is_fixed_read_only: false`. This should be removed after the
+        `epskc_enabled` flag is rolled out.
+    -->
+    <target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer">
+        <option name="flag-value"
+                value="thread_network/com.android.net.thread.flags.epskc_enabled=true"/>
+    </target_preparer>
 </configuration>
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index c048394..1792bfb 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -27,11 +27,14 @@
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_ROUTER;
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
+import static android.net.thread.ThreadNetworkController.EPHEMERAL_KEY_DISABLED;
+import static android.net.thread.ThreadNetworkController.EPHEMERAL_KEY_ENABLED;
 import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
 import static android.net.thread.ThreadNetworkController.STATE_DISABLING;
 import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
 import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
 import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
+import static android.net.thread.ThreadNetworkException.ERROR_BUSY;
 import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
 import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
 import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
@@ -72,6 +75,8 @@
 import android.os.HandlerThread;
 import android.os.OutcomeReceiver;
 import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.util.SparseIntArray;
 
 import androidx.annotation.NonNull;
@@ -82,6 +87,8 @@
 import com.android.net.thread.flags.Flags;
 import com.android.testutils.FunctionalUtils.ThrowingRunnable;
 
+import kotlin.Triple;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
@@ -96,6 +103,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Random;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
@@ -134,9 +142,13 @@
                     put(VALID_CHANNEL, VALID_POWER);
                 }
             };
+    private static final Duration EPHEMERAL_KEY_LIFETIME = Duration.ofSeconds(1);
 
     @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private final Context mContext = ApplicationProvider.getApplicationContext();
     private ExecutorService mExecutor;
     private ThreadNetworkController mController;
@@ -164,6 +176,7 @@
 
         setEnabledAndWait(mController, true);
         setConfigurationAndWait(mController, DEFAULT_CONFIG);
+        deactivateEphemeralKeyModeAndWait(mController);
     }
 
     @After
@@ -183,6 +196,7 @@
             }
         }
         mConfigurationCallbacksToCleanUp.clear();
+        deactivateEphemeralKeyModeAndWait(mController);
     }
 
     @Test
@@ -819,6 +833,221 @@
         listener.unregisterStateCallback();
     }
 
+    @Test
+    @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+    public void getMaxEphemeralKeyLifetime_isLargerThanZero() {
+        assertThat(mController.getMaxEphemeralKeyLifetime()).isGreaterThan(Duration.ZERO);
+    }
+
+    @Test
+    @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+    public void activateEphemeralKeyMode_withPrivilegedPermission_succeeds() throws Exception {
+        joinRandomizedDatasetAndWait(mController);
+        CompletableFuture<Void> startFuture = new CompletableFuture<>();
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () ->
+                        mController.activateEphemeralKeyMode(
+                                EPHEMERAL_KEY_LIFETIME,
+                                mExecutor,
+                                newOutcomeReceiver(startFuture)));
+
+        startFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+    }
+
+    @Test
+    @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+    public void activateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException()
+            throws Exception {
+        dropAllPermissions();
+
+        assertThrows(
+                SecurityException.class,
+                () ->
+                        mController.activateEphemeralKeyMode(
+                                EPHEMERAL_KEY_LIFETIME, mExecutor, v -> {}));
+    }
+
+    @Test
+    @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+    public void activateEphemeralKeyMode_withZeroLifetime_throwsIllegalArgumentException()
+            throws Exception {
+        grantPermissions(THREAD_NETWORK_PRIVILEGED);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.activateEphemeralKeyMode(Duration.ZERO, mExecutor, v -> {}));
+    }
+
+    @Test
+    @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+    public void activateEphemeralKeyMode_withInvalidLargeLifetime_throwsIllegalArgumentException()
+            throws Exception {
+        grantPermissions(THREAD_NETWORK_PRIVILEGED);
+        Duration lifetime = mController.getMaxEphemeralKeyLifetime().plusMillis(1);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mController.activateEphemeralKeyMode(lifetime, Runnable::run, v -> {}));
+    }
+
+    @Test
+    @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+    public void activateEphemeralKeyMode_concurrentRequests_secondOneFailsWithBusyError()
+            throws Exception {
+        joinRandomizedDatasetAndWait(mController);
+        CompletableFuture<Void> future1 = new CompletableFuture<>();
+        CompletableFuture<Void> future2 = new CompletableFuture<>();
+
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () -> {
+                    mController.activateEphemeralKeyMode(
+                            EPHEMERAL_KEY_LIFETIME, mExecutor, newOutcomeReceiver(future1));
+                    mController.activateEphemeralKeyMode(
+                            EPHEMERAL_KEY_LIFETIME, mExecutor, newOutcomeReceiver(future2));
+                });
+
+        var thrown =
+                assertThrows(
+                        ExecutionException.class,
+                        () -> {
+                            future2.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+                        });
+        var threadException = (ThreadNetworkException) thrown.getCause();
+        assertThat(threadException.getErrorCode()).isEqualTo(ERROR_BUSY);
+    }
+
+    @Test
+    @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+    public void deactivateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException()
+            throws Exception {
+        dropAllPermissions();
+
+        assertThrows(
+                SecurityException.class,
+                () -> mController.deactivateEphemeralKeyMode(mExecutor, v -> {}));
+    }
+
+    @Test
+    @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+    public void subscribeEpskcState_permissionsGranted_returnsCurrentState() throws Exception {
+        CompletableFuture<Integer> stateFuture = new CompletableFuture<>();
+        CompletableFuture<String> ephemeralKeyFuture = new CompletableFuture<>();
+        CompletableFuture<Instant> expiryFuture = new CompletableFuture<>();
+        StateCallback callback =
+                new ThreadNetworkController.StateCallback() {
+                    @Override
+                    public void onDeviceRoleChanged(int r) {}
+
+                    @Override
+                    public void onEphemeralKeyStateChanged(
+                            int state, String ephemeralKey, Instant expiry) {
+                        stateFuture.complete(state);
+                        ephemeralKeyFuture.complete(ephemeralKey);
+                        expiryFuture.complete(expiry);
+                    }
+                };
+
+        runAsShell(
+                ACCESS_NETWORK_STATE,
+                THREAD_NETWORK_PRIVILEGED,
+                () -> mController.registerStateCallback(mExecutor, callback));
+
+        try {
+            assertThat(stateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+                    .isEqualTo(EPHEMERAL_KEY_DISABLED);
+            assertThat(ephemeralKeyFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+            assertThat(expiryFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+        } finally {
+            runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+    public void subscribeEpskcState_withoutThreadPriviledgedPermission_returnsNullEphemeralKey()
+            throws Exception {
+        CompletableFuture<Integer> stateFuture = new CompletableFuture<>();
+        CompletableFuture<String> ephemeralKeyFuture = new CompletableFuture<>();
+        CompletableFuture<Instant> expiryFuture = new CompletableFuture<>();
+        StateCallback callback =
+                new ThreadNetworkController.StateCallback() {
+                    @Override
+                    public void onDeviceRoleChanged(int r) {}
+
+                    @Override
+                    public void onEphemeralKeyStateChanged(
+                            int state, String ephemeralKey, Instant expiry) {
+                        stateFuture.complete(state);
+                        ephemeralKeyFuture.complete(ephemeralKey);
+                        expiryFuture.complete(expiry);
+                    }
+                };
+        joinRandomizedDatasetAndWait(mController);
+        activateEphemeralKeyModeAndWait(mController);
+
+        runAsShell(
+                ACCESS_NETWORK_STATE, () -> mController.registerStateCallback(mExecutor, callback));
+
+        try {
+            assertThat(stateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
+                    .isEqualTo(EPHEMERAL_KEY_ENABLED);
+            assertThat(ephemeralKeyFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
+            assertThat(
+                            expiryFuture
+                                    .get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)
+                                    .isAfter(Instant.now()))
+                    .isTrue();
+        } finally {
+            runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+    public void subscribeEpskcState_ephemralKeyStateChanged_returnsUpdatedState() throws Exception {
+        EphemeralKeyStateListener listener = new EphemeralKeyStateListener(mController);
+        joinRandomizedDatasetAndWait(mController);
+
+        try {
+            activateEphemeralKeyModeAndWait(mController);
+            deactivateEphemeralKeyModeAndWait(mController);
+
+            listener.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_DISABLED);
+            listener.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_ENABLED);
+            listener.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_DISABLED);
+        } finally {
+            listener.unregisterStateCallback();
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
+    public void subscribeEpskcState_epskcEnabled_returnsSameExpiry() throws Exception {
+        EphemeralKeyStateListener listener1 = new EphemeralKeyStateListener(mController);
+        Triple<Integer, String, Instant> epskc1;
+        try {
+            joinRandomizedDatasetAndWait(mController);
+            activateEphemeralKeyModeAndWait(mController);
+            epskc1 = listener1.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_ENABLED);
+        } finally {
+            listener1.unregisterStateCallback();
+        }
+
+        EphemeralKeyStateListener listener2 = new EphemeralKeyStateListener(mController);
+        try {
+            Triple<Integer, String, Instant> epskc2 =
+                    listener2.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_ENABLED);
+
+            assertThat(epskc2.getSecond()).isEqualTo(epskc1.getSecond());
+            assertThat(epskc2.getThird()).isEqualTo(epskc1.getThird());
+        } finally {
+            listener2.unregisterStateCallback();
+        }
+    }
+
     // TODO (b/322437869): add test case to verify when Thread is in DISABLING state, any commands
     // (join/leave/scheduleMigration/setEnabled) fail with ERROR_BUSY. This is not currently tested
     // because DISABLING has very short lifecycle, it's not possible to guarantee the command can be
@@ -1274,6 +1503,71 @@
         setFuture.get(SET_CONFIGURATION_TIMEOUT_MILLIS, MILLISECONDS);
     }
 
+    private void deactivateEphemeralKeyModeAndWait(ThreadNetworkController controller)
+            throws Exception {
+        CompletableFuture<Void> clearFuture = new CompletableFuture<>();
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () ->
+                        controller.deactivateEphemeralKeyMode(
+                                mExecutor, newOutcomeReceiver(clearFuture)));
+        clearFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+    }
+
+    private void activateEphemeralKeyModeAndWait(ThreadNetworkController controller)
+            throws Exception {
+        CompletableFuture<Void> startFuture = new CompletableFuture<>();
+        runAsShell(
+                THREAD_NETWORK_PRIVILEGED,
+                () ->
+                        controller.activateEphemeralKeyMode(
+                                EPHEMERAL_KEY_LIFETIME,
+                                mExecutor,
+                                newOutcomeReceiver(startFuture)));
+        startFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+    }
+
+    private class EphemeralKeyStateListener {
+        private ArrayTrackRecord<Triple<Integer, String, Instant>> mEphemeralKeyStates =
+                new ArrayTrackRecord<>();
+        private final ArrayTrackRecord<Triple<Integer, String, Instant>>.ReadHead mReadHead =
+                mEphemeralKeyStates.newReadHead();
+        ThreadNetworkController mController;
+        StateCallback mCallback =
+                new ThreadNetworkController.StateCallback() {
+                    @Override
+                    public void onDeviceRoleChanged(int r) {}
+
+                    @Override
+                    public void onEphemeralKeyStateChanged(
+                            int state, String ephemeralKey, Instant expiry) {
+                        mEphemeralKeyStates.add(new Triple<>(state, ephemeralKey, expiry));
+                    }
+                };
+
+        EphemeralKeyStateListener(ThreadNetworkController controller) {
+            this.mController = controller;
+            runAsShell(
+                    ACCESS_NETWORK_STATE,
+                    THREAD_NETWORK_PRIVILEGED,
+                    () -> controller.registerStateCallback(mExecutor, mCallback));
+        }
+
+        // Expect that EphemeralKey has the expected state, and return a Triple of <state,
+        // passcode, expiry>.
+        public Triple<Integer, String, Instant> expectThreadEphemeralKeyMode(int state) {
+            Triple<Integer, String, Instant> epskc =
+                    mReadHead.poll(
+                            ENABLED_TIMEOUT_MILLIS, e -> Objects.equals(e.getFirst(), state));
+            assertThat(epskc).isNotNull();
+            return epskc;
+        }
+
+        public void unregisterStateCallback() {
+            runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(mCallback));
+        }
+    }
+
     private CompletableFuture joinRandomizedDataset(
             ThreadNetworkController controller, String networkName) throws Exception {
         ActiveOperationalDataset activeDataset = newRandomizedDataset(networkName, controller);
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
index 0423578..62801bf 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -17,6 +17,8 @@
 package android.net.thread;
 
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
+import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
 import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
 import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
 import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_FEATURE;
@@ -26,6 +28,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doAnswer;
 
@@ -134,6 +137,16 @@
         return (IOperationalDatasetCallback) invocation.getArguments()[0];
     }
 
+    private static IOperationReceiver getActivateEphemeralKeyModeReceiver(
+            InvocationOnMock invocation) {
+        return (IOperationReceiver) invocation.getArguments()[1];
+    }
+
+    private static IOperationReceiver getDeactivateEphemeralKeyModeReceiver(
+            InvocationOnMock invocation) {
+        return (IOperationReceiver) invocation.getArguments()[0];
+    }
+
     @Test
     public void registerStateCallback_callbackIsInvokedWithCallingAppIdentity() throws Exception {
         setBinderUid(SYSTEM_UID);
@@ -440,4 +453,88 @@
         assertThat(callbackUid.get()).isNotEqualTo(SYSTEM_UID);
         assertThat(callbackUid.get()).isEqualTo(Process.myUid());
     }
+
+    @Test
+    public void activateEphemeralKeyMode_callbackIsInvokedWithCallingAppIdentity()
+            throws Exception {
+        setBinderUid(SYSTEM_UID);
+        AtomicInteger successCallbackUid = new AtomicInteger(0);
+        AtomicInteger errorCallbackUid = new AtomicInteger(0);
+        Duration lifetime = Duration.ofSeconds(100);
+        doAnswer(
+                        invoke -> {
+                            getActivateEphemeralKeyModeReceiver(invoke).onSuccess();
+                            return null;
+                        })
+                .when(mMockService)
+                .activateEphemeralKeyMode(anyLong(), any(IOperationReceiver.class));
+        mController.activateEphemeralKeyMode(
+                lifetime, Runnable::run, v -> successCallbackUid.set(Binder.getCallingUid()));
+        doAnswer(
+                        invoke -> {
+                            getActivateEphemeralKeyModeReceiver(invoke)
+                                    .onError(ERROR_FAILED_PRECONDITION, "");
+                            return null;
+                        })
+                .when(mMockService)
+                .activateEphemeralKeyMode(anyLong(), any(IOperationReceiver.class));
+        mController.activateEphemeralKeyMode(
+                lifetime,
+                Runnable::run,
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(Void unused) {}
+
+                    @Override
+                    public void onError(ThreadNetworkException e) {
+                        errorCallbackUid.set(Binder.getCallingUid());
+                    }
+                });
+
+        assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+        assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+        assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+        assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+    }
+
+    @Test
+    public void deactivateEphemeralKeyMode_callbackIsInvokedWithCallingAppIdentity()
+            throws Exception {
+        setBinderUid(SYSTEM_UID);
+        AtomicInteger successCallbackUid = new AtomicInteger(0);
+        AtomicInteger errorCallbackUid = new AtomicInteger(0);
+        doAnswer(
+                        invoke -> {
+                            getDeactivateEphemeralKeyModeReceiver(invoke).onSuccess();
+                            return null;
+                        })
+                .when(mMockService)
+                .deactivateEphemeralKeyMode(any(IOperationReceiver.class));
+        mController.deactivateEphemeralKeyMode(
+                Runnable::run, v -> successCallbackUid.set(Binder.getCallingUid()));
+        doAnswer(
+                        invoke -> {
+                            getDeactivateEphemeralKeyModeReceiver(invoke)
+                                    .onError(ERROR_INTERNAL_ERROR, "");
+                            return null;
+                        })
+                .when(mMockService)
+                .deactivateEphemeralKeyMode(any(IOperationReceiver.class));
+        mController.deactivateEphemeralKeyMode(
+                Runnable::run,
+                new OutcomeReceiver<>() {
+                    @Override
+                    public void onResult(Void unused) {}
+
+                    @Override
+                    public void onError(ThreadNetworkException e) {
+                        errorCallbackUid.set(Binder.getCallingUid());
+                    }
+                });
+
+        assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+        assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+        assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+        assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+    }
 }
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index d8cdbc4..b97e2b7 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -838,4 +838,26 @@
 
         verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString());
     }
+
+    @Test
+    public void activateEphemeralKeyMode_succeed() throws Exception {
+        mService.initialize();
+        final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+
+        mService.activateEphemeralKeyMode(1_000L, mockReceiver);
+        mTestLooper.dispatchAll();
+
+        verify(mockReceiver, times(1)).onSuccess();
+    }
+
+    @Test
+    public void deactivateEphemeralKeyMode_succeed() throws Exception {
+        mService.initialize();
+        final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+
+        mService.deactivateEphemeralKeyMode(mockReceiver);
+        mTestLooper.dispatchAll();
+
+        verify(mockReceiver, times(1)).onSuccess();
+    }
 }