Merge changes I6cc2507d,I187ea15d into main

* changes:
  Add flag for QUEUE_CALLBACKS_FOR_FROZEN_APPS
  Add a CallbackQueue class
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 8b3102a..0f5a014 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -198,6 +198,10 @@
 
     /**
      * VIRTUAL tethering type.
+     *
+     * This tethering type is for providing external network to virtual machines
+     * running on top of Android devices, which are created and managed by
+     * AVF(Android Virtualization Framework).
      * @hide
      */
     @FlaggedApi(Flags.TETHERING_REQUEST_VIRTUAL)
@@ -1379,6 +1383,9 @@
     @RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
     public void registerTetheringEventCallback(@NonNull Executor executor,
             @NonNull TetheringEventCallback callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
         final String callerPkg = mContext.getOpPackageName();
         Log.i(TAG, "registerTetheringEventCallback caller:" + callerPkg);
 
@@ -1533,6 +1540,8 @@
             Manifest.permission.ACCESS_NETWORK_STATE
     })
     public void unregisterTetheringEventCallback(@NonNull final TetheringEventCallback callback) {
+        Objects.requireNonNull(callback);
+
         final String callerPkg = mContext.getOpPackageName();
         Log.i(TAG, "unregisterTetheringEventCallback caller:" + callerPkg);
 
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 5cdd6ab..b807544 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -72,7 +72,6 @@
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.RoutingCoordinatorManager;
-import com.android.net.module.util.SdkUtil.LateSdk;
 import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.SyncStateMachine.StateInfo;
 import com.android.net.module.util.ip.InterfaceController;
@@ -239,11 +238,8 @@
     private final INetd mNetd;
     @NonNull
     private final BpfCoordinator mBpfCoordinator;
-    // Contains null if the connectivity module is unsupported, as the routing coordinator is not
-    // available. Must use LateSdk because MessageUtils enumerates fields in this class, so it
-    // must be able to find all classes at runtime.
     @NonNull
-    private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinator;
+    private final RoutingCoordinatorManager mRoutingCoordinator;
     private final Callback mCallback;
     private final InterfaceController mInterfaceCtrl;
     private final PrivateAddressCoordinator mPrivateAddressCoordinator;
@@ -301,7 +297,7 @@
     public IpServer(
             String ifaceName, Handler handler, int interfaceType, SharedLog log,
             INetd netd, @NonNull BpfCoordinator bpfCoordinator,
-            @Nullable LateSdk<RoutingCoordinatorManager> routingCoordinator, Callback callback,
+            RoutingCoordinatorManager routingCoordinatorManager, Callback callback,
             TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
             TetheringMetrics tetheringMetrics, Dependencies deps) {
         super(ifaceName, USE_SYNC_SM ? null : handler.getLooper());
@@ -309,7 +305,7 @@
         mLog = log.forSubComponent(ifaceName);
         mNetd = netd;
         mBpfCoordinator = bpfCoordinator;
-        mRoutingCoordinator = routingCoordinator;
+        mRoutingCoordinator = routingCoordinatorManager;
         mCallback = callback;
         mInterfaceCtrl = new InterfaceController(ifaceName, mNetd, mLog);
         mIfaceName = ifaceName;
@@ -825,47 +821,25 @@
 
     private void addInterfaceToNetwork(final int netId, @NonNull final String ifaceName) {
         try {
-            if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
-                // TODO : remove this call in favor of using the LocalNetworkConfiguration
-                // correctly, which will let ConnectivityService do it automatically.
-                mRoutingCoordinator.value.addInterfaceToNetwork(netId, ifaceName);
-            } else {
-                mNetd.networkAddInterface(netId, ifaceName);
-            }
-        } catch (ServiceSpecificException | RemoteException e) {
+            // TODO : remove this call in favor of using the LocalNetworkConfiguration
+            // correctly, which will let ConnectivityService do it automatically.
+            mRoutingCoordinator.addInterfaceToNetwork(netId, ifaceName);
+        } catch (ServiceSpecificException e) {
             mLog.e("Failed to add " + mIfaceName + " to local table: ", e);
         }
     }
 
-    private void addInterfaceForward(@NonNull final String fromIface,
-            @NonNull final String toIface) throws ServiceSpecificException, RemoteException {
-        if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
-            mRoutingCoordinator.value.addInterfaceForward(fromIface, toIface);
-        } else {
-            mNetd.tetherAddForward(fromIface, toIface);
-            mNetd.ipfwdAddInterfaceForward(fromIface, toIface);
-        }
+    private void addInterfaceForward(@NonNull final String fromIface, @NonNull final String toIface)
+            throws ServiceSpecificException {
+        mRoutingCoordinator.addInterfaceForward(fromIface, toIface);
     }
 
     private void removeInterfaceForward(@NonNull final String fromIface,
             @NonNull final String toIface) {
-        if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
-            try {
-                mRoutingCoordinator.value.removeInterfaceForward(fromIface, toIface);
-            } catch (ServiceSpecificException e) {
-                mLog.e("Exception in removeInterfaceForward", e);
-            }
-        } else {
-            try {
-                mNetd.ipfwdRemoveInterfaceForward(fromIface, toIface);
-            } catch (RemoteException | ServiceSpecificException e) {
-                mLog.e("Exception in ipfwdRemoveInterfaceForward", e);
-            }
-            try {
-                mNetd.tetherRemoveForward(fromIface, toIface);
-            } catch (RemoteException | ServiceSpecificException e) {
-                mLog.e("Exception in disableNat", e);
-            }
+        try {
+            mRoutingCoordinator.removeInterfaceForward(fromIface, toIface);
+        } catch (RuntimeException e) {
+            mLog.e("Exception in removeInterfaceForward", e);
         }
     }
 
@@ -1370,7 +1344,7 @@
                         mBpfCoordinator.maybeAttachProgram(mIfaceName, ifname);
                         try {
                             addInterfaceForward(mIfaceName, ifname);
-                        } catch (RemoteException | ServiceSpecificException e) {
+                        } catch (RuntimeException e) {
                             mLog.e("Exception enabling iface forward", e);
                             cleanupUpstream();
                             mLastError = TETHER_ERROR_ENABLE_FORWARDING_ERROR;
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 163fe24..d62f18f 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -138,7 +138,6 @@
 import com.android.net.module.util.HandlerUtils;
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.RoutingCoordinatorManager;
-import com.android.net.module.util.SdkUtil.LateSdk;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
 import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
@@ -246,10 +245,7 @@
     private final Handler mHandler;
     private final INetd mNetd;
     private final NetdCallback mNetdCallback;
-    // Contains null if the connectivity module is unsupported, as the routing coordinator is not
-    // available. Must use LateSdk because MessageUtils enumerates fields in this class, so it
-    // must be able to find all classes at runtime.
-    @NonNull private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinator;
+    private final RoutingCoordinatorManager mRoutingCoordinator;
     private final UserRestrictionActionListener mTetheringRestriction;
     private final ActiveDataSubIdListener mActiveDataSubIdListener;
     private final ConnectedClientsTracker mConnectedClientsTracker;
@@ -294,11 +290,11 @@
         mLog.mark("Tethering.constructed");
         mDeps = deps;
         mContext = mDeps.getContext();
-        mNetd = mDeps.getINetd(mContext);
-        mRoutingCoordinator = mDeps.getRoutingCoordinator(mContext);
+        mNetd = mDeps.getINetd(mContext, mLog);
+        mRoutingCoordinator = mDeps.getRoutingCoordinator(mContext, mLog);
         mLooper = mDeps.makeTetheringLooper();
         mNotificationUpdater = mDeps.makeNotificationUpdater(mContext, mLooper);
-        mTetheringMetrics = mDeps.makeTetheringMetrics();
+        mTetheringMetrics = mDeps.makeTetheringMetrics(mContext);
 
         // This is intended to ensrure that if something calls startTethering(bluetooth) just after
         // bluetooth is enabled. Before onServiceConnected is called, store the calls into this
@@ -2397,9 +2393,6 @@
                 hasCallingPermission(NETWORK_SETTINGS)
                         || hasCallingPermission(PERMISSION_MAINLINE_NETWORK_STACK)
                         || hasCallingPermission(NETWORK_STACK);
-        if (callback == null) {
-            throw new NullPointerException();
-        }
         mHandler.post(() -> {
             mTetheringEventCallbacks.register(callback, new CallbackCookie(hasListPermission));
             final TetheringCallbackStartedParcel parcel = new TetheringCallbackStartedParcel();
@@ -2437,9 +2430,6 @@
 
     /** Unregister tethering event callback */
     void unregisterTetheringEventCallback(ITetheringEventCallback callback) {
-        if (callback == null) {
-            throw new NullPointerException();
-        }
         mHandler.post(() -> {
             mTetheringEventCallbacks.unregister(callback);
         });
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 54dbf6c..5d9d349 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -36,7 +36,7 @@
 
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.RoutingCoordinatorManager;
-import com.android.net.module.util.SdkUtil.LateSdk;
+import com.android.net.module.util.RoutingCoordinatorService;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.apishim.BluetoothPanShimImpl;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
@@ -120,20 +120,26 @@
     /**
      * Get a reference to INetd to be used by tethering.
      */
-    public INetd getINetd(Context context) {
-        return INetd.Stub.asInterface(
-                (IBinder) context.getSystemService(Context.NETD_SERVICE));
+    public INetd getINetd(Context context, SharedLog log) {
+        final INetd netd =
+                INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE));
+        if (netd == null) {
+            log.wtf("INetd is null");
+        }
+        return netd;
     }
 
     /**
-     * Get the routing coordinator, or null if below S.
+     * Get the routing coordinator.
      */
-    @Nullable
-    public LateSdk<RoutingCoordinatorManager> getRoutingCoordinator(Context context) {
-        if (!SdkLevel.isAtLeastS()) return new LateSdk<>(null);
-        return new LateSdk<>(
-                new RoutingCoordinatorManager(
-                        context, ConnectivityInternalApiUtil.getRoutingCoordinator(context)));
+    public RoutingCoordinatorManager getRoutingCoordinator(Context context, SharedLog log) {
+        IBinder binder;
+        if (!SdkLevel.isAtLeastS()) {
+            binder = new RoutingCoordinatorService(getINetd(context, log));
+        } else {
+            binder = ConnectivityInternalApiUtil.getRoutingCoordinator(context);
+        }
+        return new RoutingCoordinatorManager(context, binder);
     }
 
     /**
@@ -187,8 +193,8 @@
     /**
      * Make the TetheringMetrics to be used by tethering.
      */
-    public TetheringMetrics makeTetheringMetrics() {
-        return new TetheringMetrics();
+    public TetheringMetrics makeTetheringMetrics(Context ctx) {
+        return new TetheringMetrics(ctx);
     }
 
     /**
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index a147a4a..454cbf1 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -164,6 +164,8 @@
         @Override
         public void registerTetheringEventCallback(ITetheringEventCallback callback,
                 String callerPkg) {
+            // Silently ignore call if the callback is null. This can only happen via reflection.
+            if (callback == null) return;
             try {
                 if (!hasTetherAccessPermission()) {
                     callback.onCallbackStopped(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION);
@@ -176,6 +178,8 @@
         @Override
         public void unregisterTetheringEventCallback(ITetheringEventCallback callback,
                 String callerPkg) {
+            // Silently ignore call if the callback is null. This can only happen via reflection.
+            if (callback == null) return;
             try {
                 if (!hasTetherAccessPermission()) {
                     callback.onCallbackStopped(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION);
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index 814afcd..136dfb1 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -46,6 +46,7 @@
 import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
 
 import android.annotation.Nullable;
+import android.content.Context;
 import android.net.NetworkCapabilities;
 import android.stats.connectivity.DownstreamType;
 import android.stats.connectivity.ErrorCode;
@@ -81,16 +82,50 @@
     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 Context mContext;
+    private final Dependencies mDependencies;
     private UpstreamType mCurrentUpstream = null;
     private Long mCurrentUpStreamStartTime = 0L;
 
+    /**
+     * Dependencies of TetheringMetrics, for injection in tests.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /**
+         * @see TetheringStatsLog
+         */
+        public void write(NetworkTetheringReported reported) {
+            TetheringStatsLog.write(
+                    TetheringStatsLog.NETWORK_TETHERING_REPORTED,
+                    reported.getErrorCode().getNumber(),
+                    reported.getDownstreamType().getNumber(),
+                    reported.getUpstreamType().getNumber(),
+                    reported.getUserType().getNumber(),
+                    reported.getUpstreamEvents().toByteArray(),
+                    reported.getDurationMillis());
+        }
+
+        /**
+         * @see System#currentTimeMillis()
+         */
+        public long timeNow() {
+            return System.currentTimeMillis();
+        }
+    }
 
     /**
-     * Return the current system time in milliseconds.
-     * @return the current system time in milliseconds.
+     * Constructor for the TetheringMetrics class.
+     *
+     * @param context The Context object used to access system services.
      */
-    public long timeNow() {
-        return System.currentTimeMillis();
+    public TetheringMetrics(Context context) {
+        this(context, new Dependencies());
+    }
+
+    TetheringMetrics(Context context, Dependencies dependencies) {
+        mContext = context;
+        mDependencies = dependencies;
     }
 
     private static class RecordUpstreamEvent {
@@ -123,7 +158,7 @@
                 .setUpstreamEvents(UpstreamEvents.newBuilder())
                 .setDurationMillis(0);
         mBuilderMap.put(downstreamType, statsBuilder);
-        mDownstreamStartTime.put(downstreamType, timeNow());
+        mDownstreamStartTime.put(downstreamType, mDependencies.timeNow());
     }
 
     /**
@@ -149,7 +184,7 @@
         UpstreamType upstream = transportTypeToUpstreamTypeEnum(ns);
         if (upstream.equals(mCurrentUpstream)) return;
 
-        final long newTime = timeNow();
+        final long newTime = mDependencies.timeNow();
         if (mCurrentUpstream != null) {
             mUpstreamEventList.add(new RecordUpstreamEvent(mCurrentUpStreamStartTime, newTime,
                     mCurrentUpstream));
@@ -206,7 +241,7 @@
                     event.mUpstreamType, 0L /* txBytes */, 0L /* rxBytes */);
         }
         final long startTime = Math.max(downstreamStartTime, mCurrentUpStreamStartTime);
-        final long stopTime = timeNow();
+        final long stopTime = mDependencies.timeNow();
         // Handle the last upstream event.
         addUpstreamEvent(upstreamEventsBuilder, startTime, stopTime, mCurrentUpstream,
                 0L /* txBytes */, 0L /* rxBytes */);
@@ -248,15 +283,7 @@
     @VisibleForTesting
     public void write(@NonNull final NetworkTetheringReported reported) {
         final byte[] upstreamEvents = reported.getUpstreamEvents().toByteArray();
-
-        TetheringStatsLog.write(
-                TetheringStatsLog.NETWORK_TETHERING_REPORTED,
-                reported.getErrorCode().getNumber(),
-                reported.getDownstreamType().getNumber(),
-                reported.getUpstreamType().getNumber(),
-                reported.getUserType().getNumber(),
-                upstreamEvents,
-                reported.getDurationMillis());
+        mDependencies.write(reported);
         if (DBG) {
             Log.d(
                     TAG,
@@ -374,4 +401,22 @@
 
         return UpstreamType.UT_UNKNOWN;
     }
+
+    /**
+     * Check whether tethering metrics' data usage can be collected for a given upstream type.
+     *
+     * @param type the upstream type
+     */
+    public static boolean isUsageSupportedForUpstreamType(@NonNull UpstreamType type) {
+        switch(type) {
+            case UT_CELLULAR:
+            case UT_WIFI:
+            case UT_BLUETOOTH:
+            case UT_ETHERNET:
+                return true;
+            default:
+                break;
+        }
+        return false;
+    }
 }
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 748f23c..177296a 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -51,6 +51,7 @@
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doThrow;
@@ -174,8 +175,7 @@
     @Mock private RouterAdvertisementDaemon mRaDaemon;
     @Mock private IpServer.Dependencies mDependencies;
     @Mock private PrivateAddressCoordinator mAddressCoordinator;
-    private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinatorManager =
-            new LateSdk<>(SdkLevel.isAtLeastS() ? mock(RoutingCoordinatorManager.class) : null);
+    @Mock private RoutingCoordinatorManager mRoutingCoordinatorManager;
     @Mock private NetworkStatsManager mStatsManager;
     @Mock private TetheringConfiguration mTetherConfig;
     @Mock private TetheringMetrics mTetheringMetrics;
@@ -280,24 +280,6 @@
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(DEFAULT_USING_BPF_OFFLOAD);
         when(mTetherConfig.useLegacyDhcpServer()).thenReturn(false /* default value */);
 
-        // Simulate the behavior of RoutingCoordinator
-        if (null != mRoutingCoordinatorManager.value) {
-            doAnswer(it -> {
-                final String fromIface = (String) it.getArguments()[0];
-                final String toIface = (String) it.getArguments()[1];
-                mNetd.tetherAddForward(fromIface, toIface);
-                mNetd.ipfwdAddInterfaceForward(fromIface, toIface);
-                return null;
-            }).when(mRoutingCoordinatorManager.value).addInterfaceForward(any(), any());
-            doAnswer(it -> {
-                final String fromIface = (String) it.getArguments()[0];
-                final String toIface = (String) it.getArguments()[1];
-                mNetd.ipfwdRemoveInterfaceForward(fromIface, toIface);
-                mNetd.tetherRemoveForward(fromIface, toIface);
-                return null;
-            }).when(mRoutingCoordinatorManager.value).removeInterfaceForward(any(), any());
-        }
-
         setUpDhcpServer();
     }
 
@@ -455,105 +437,114 @@
         // Telling the state machine about its upstream interface triggers
         // a little more configuration.
         dispatchTetherConnectionChanged(UPSTREAM_IFACE);
-        InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+        InOrder inOrder = inOrder(mBpfCoordinator, mRoutingCoordinatorManager);
 
         // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
         inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX,
                 UPSTREAM_IFACE);
         inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
+        inOrder.verify(mRoutingCoordinatorManager).addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
 
-        verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator);
+        verifyNoMoreInteractions(mCallback, mBpfCoordinator, mRoutingCoordinatorManager);
     }
 
     @Test
     public void handlesChangingUpstream() throws Exception {
         initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
 
+        clearInvocations(mBpfCoordinator, mRoutingCoordinatorManager);
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
-        InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+        InOrder inOrder = inOrder(mBpfCoordinator, mRoutingCoordinatorManager);
 
         // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
 
         // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2>.
         inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
                 UPSTREAM_IFACE2);
         inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
+        inOrder.verify(mRoutingCoordinatorManager).addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
 
-        verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator);
+        verifyNoMoreInteractions(mCallback, mBpfCoordinator, mRoutingCoordinatorManager);
     }
 
     @Test
     public void handlesChangingUpstreamNatFailure() throws Exception {
         initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
 
-        doThrow(RemoteException.class).when(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
+        doThrow(RuntimeException.class)
+                .when(mRoutingCoordinatorManager)
+                .addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
 
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
-        InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+        InOrder inOrder = inOrder(mBpfCoordinator, mRoutingCoordinatorManager);
 
         // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
 
         // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
-        // tetherAddForward.
+        // addInterfaceForward.
         inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
                 UPSTREAM_IFACE2);
         inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
+        inOrder.verify(mRoutingCoordinatorManager).addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
 
         // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> to fallback.
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
     }
 
     @Test
     public void handlesChangingUpstreamInterfaceForwardingFailure() throws Exception {
         initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
 
-        doThrow(RemoteException.class).when(mNetd).ipfwdAddInterfaceForward(
-                IFACE_NAME, UPSTREAM_IFACE2);
+        doThrow(RuntimeException.class)
+                .when(mRoutingCoordinatorManager)
+                .addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
 
         dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
-        InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+        InOrder inOrder = inOrder(mBpfCoordinator, mRoutingCoordinatorManager);
 
         // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
 
         // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
         // ipfwdAddInterfaceForward.
         inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
                 UPSTREAM_IFACE2);
         inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
+        inOrder.verify(mRoutingCoordinatorManager).addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
 
         // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> to fallback.
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
     }
 
     @Test
     public void canUnrequestTetheringWithUpstream() throws Exception {
         initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
 
+        clearInvocations(
+                mNetd, mCallback, mAddressCoordinator, mBpfCoordinator, mRoutingCoordinatorManager);
         dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
-        InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
+        InOrder inOrder =
+                inOrder(
+                        mNetd,
+                        mCallback,
+                        mAddressCoordinator,
+                        mBpfCoordinator,
+                        mRoutingCoordinatorManager);
         inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
-        inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+        inOrder.verify(mRoutingCoordinatorManager)
+                .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
         inOrder.verify(mBpfCoordinator).updateIpv6UpstreamInterface(
                 mIpServer, NO_UPSTREAM, NO_PREFIXES);
         // When tethering stops, upstream interface is set to zero and thus clearing all upstream
@@ -572,7 +563,8 @@
                 mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
         inOrder.verify(mCallback).updateLinkProperties(
                 eq(mIpServer), any(LinkProperties.class));
-        verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
+        verifyNoMoreInteractions(
+                mNetd, mCallback, mAddressCoordinator, mBpfCoordinator, mRoutingCoordinatorManager);
     }
 
     @Test
@@ -624,10 +616,14 @@
     public void shouldTearDownUsbOnUpstreamError() throws Exception {
         initTetheredStateMachine(TETHERING_USB, null);
 
-        doThrow(RemoteException.class).when(mNetd).tetherAddForward(anyString(), anyString());
+        doThrow(RuntimeException.class)
+                .when(mRoutingCoordinatorManager)
+                .addInterfaceForward(anyString(), anyString());
         dispatchTetherConnectionChanged(UPSTREAM_IFACE);
-        InOrder usbTeardownOrder = inOrder(mNetd, mCallback);
-        usbTeardownOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE);
+        InOrder usbTeardownOrder = inOrder(mNetd, mCallback, mRoutingCoordinatorManager);
+        usbTeardownOrder
+                .verify(mRoutingCoordinatorManager)
+                .addInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
 
         usbTeardownOrder.verify(mNetd, times(2)).interfaceSetCfg(
                 argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
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 df7141f..6ba5d48 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -191,7 +191,6 @@
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.RoutingCoordinatorManager;
-import com.android.net.module.util.SdkUtil.LateSdk;
 import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.ip.IpNeighborMonitor;
 import com.android.networkstack.apishim.common.BluetoothPanShim;
@@ -292,6 +291,7 @@
     @Mock private BluetoothPanShim mBluetoothPanShim;
     @Mock private TetheredInterfaceRequestShim mTetheredInterfaceRequestShim;
     @Mock private TetheringMetrics mTetheringMetrics;
+    @Mock private RoutingCoordinatorManager mRoutingCoordinatorManager;
 
     private final MockIpServerDependencies mIpServerDependencies =
             spy(new MockIpServerDependencies());
@@ -484,10 +484,10 @@
             return mEntitleMgr;
         }
 
-        @Nullable
         @Override
-        public LateSdk<RoutingCoordinatorManager> getRoutingCoordinator(final Context context) {
-            return new LateSdk<>(null);
+        public RoutingCoordinatorManager getRoutingCoordinator(final Context context,
+                SharedLog log) {
+            return mRoutingCoordinatorManager;
         }
 
         @Override
@@ -498,7 +498,7 @@
         }
 
         @Override
-        public INetd getINetd(Context context) {
+        public INetd getINetd(Context context, SharedLog log) {
             return mNetd;
         }
 
@@ -528,7 +528,7 @@
         }
 
         @Override
-        public TetheringMetrics makeTetheringMetrics() {
+        public TetheringMetrics makeTetheringMetrics(Context ctx) {
             return mTetheringMetrics;
         }
 
@@ -1082,8 +1082,8 @@
         UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
 
         sendIPv6TetherUpdates(upstreamState);
         assertSetIfaceToDadProxy(0 /* numOfCalls */, "" /* ifaceName */);
@@ -1111,8 +1111,8 @@
         UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
 
         sendIPv6TetherUpdates(upstreamState);
         // TODO: add interfaceParams to compare in verify.
@@ -1127,8 +1127,8 @@
         UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
         verify(mRouterAdvertisementDaemon, times(1)).start();
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
@@ -1145,13 +1145,12 @@
         UpstreamNetworkState upstreamState = buildMobile464xlatUpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_XLAT_MOBILE_IFNAME);
-        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_XLAT_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME,
-                TEST_XLAT_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
 
         sendIPv6TetherUpdates(upstreamState);
         assertSetIfaceToDadProxy(1 /* numOfCalls */, TEST_MOBILE_IFNAME /* ifaceName */);
@@ -1172,10 +1171,10 @@
         UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
 
         // Then 464xlat comes up
         upstreamState = buildMobile464xlatUpstreamState();
@@ -1187,12 +1186,11 @@
         mLooper.dispatchAll();
 
         // Forwarding is added for 464xlat
-        verify(mNetd, times(1)).tetherAddForward(TEST_NCM_IFNAME, TEST_XLAT_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_NCM_IFNAME,
-                TEST_XLAT_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_NCM_IFNAME, TEST_XLAT_MOBILE_IFNAME);
         // Forwarding was not re-added for v6 (still times(1))
-        verify(mNetd, times(1)).tetherAddForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager, times(1))
+                .addInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
         // DHCP not restarted on downstream (still times(1))
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
@@ -3435,8 +3433,7 @@
         runUsbTethering(upstreamState);
 
         verify(mNetd).interfaceGetList();
-        verify(mNetd).tetherAddForward(expectedIface, TEST_MOBILE_IFNAME);
-        verify(mNetd).ipfwdAddInterfaceForward(expectedIface, TEST_MOBILE_IFNAME);
+        verify(mRoutingCoordinatorManager).addInterfaceForward(expectedIface, TEST_MOBILE_IFNAME);
 
         verify(mRouterAdvertisementDaemon).start();
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
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 e2c924c..7cef9cb 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
@@ -46,10 +46,11 @@
 import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
 import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
 
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
 
+import android.content.Context;
 import android.net.NetworkCapabilities;
 import android.stats.connectivity.DownstreamType;
 import android.stats.connectivity.ErrorCode;
@@ -60,10 +61,12 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.networkstack.tethering.UpstreamNetworkState;
+import com.android.networkstack.tethering.metrics.TetheringMetrics.Dependencies;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 @RunWith(AndroidJUnit4.class)
@@ -76,28 +79,23 @@
     private static final long TEST_START_TIME = 1670395936033L;
     private static final long SECOND_IN_MILLIS = 1_000L;
 
+    @Mock private Context mContext;
+    @Mock private Dependencies mDeps;
+
     private TetheringMetrics mTetheringMetrics;
     private final NetworkTetheringReported.Builder mStatsBuilder =
             NetworkTetheringReported.newBuilder();
 
     private long mElapsedRealtime;
 
-    private class MockTetheringMetrics extends TetheringMetrics {
-        @Override
-        public void write(final NetworkTetheringReported reported) {}
-        @Override
-        public long timeNow() {
-            return currentTimeMillis();
-        }
-    }
-
     private long currentTimeMillis() {
         return TEST_START_TIME + mElapsedRealtime;
     }
 
     private void incrementCurrentTime(final long duration) {
         mElapsedRealtime += duration;
-        mTetheringMetrics.timeNow();
+        final long currentTimeMillis = currentTimeMillis();
+        doReturn(currentTimeMillis).when(mDeps).timeNow();
     }
 
     private long getElapsedRealtime() {
@@ -106,12 +104,14 @@
 
     private void clearElapsedRealtime() {
         mElapsedRealtime = 0;
+        doReturn(TEST_START_TIME).when(mDeps).timeNow();
     }
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mTetheringMetrics = spy(new MockTetheringMetrics());
+        doReturn(TEST_START_TIME).when(mDeps).timeNow();
+        mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
         mElapsedRealtime = 0L;
     }
 
@@ -126,7 +126,7 @@
                 .setUpstreamEvents(upstreamEvents)
                 .setDurationMillis(duration)
                 .build();
-        verify(mTetheringMetrics).write(expectedReport);
+        verify(mDeps).write(expectedReport);
     }
 
     private void updateErrorAndSendReport(final int downstream, final int error) {
@@ -162,6 +162,7 @@
 
     private void runDownstreamTypesTest(final int type, final DownstreamType expectedResult)
             throws Exception {
+        mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
         mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG);
         final long duration = 2 * SECOND_IN_MILLIS;
         incrementCurrentTime(duration);
@@ -172,9 +173,7 @@
 
         verifyReport(expectedResult, ErrorCode.EC_NO_ERROR, UserType.USER_UNKNOWN,
                 upstreamEvents, getElapsedRealtime());
-        reset(mTetheringMetrics);
         clearElapsedRealtime();
-        mTetheringMetrics.cleanup();
     }
 
     @Test
@@ -189,6 +188,7 @@
 
     private void runErrorCodesTest(final int errorCode, final ErrorCode expectedResult)
             throws Exception {
+        mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
         mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
         mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI));
         final long duration = 2 * SECOND_IN_MILLIS;
@@ -199,9 +199,7 @@
         addUpstreamEvent(upstreamEvents, UpstreamType.UT_WIFI, duration, 0L, 0L);
         verifyReport(DownstreamType.DS_TETHERING_WIFI, expectedResult, UserType.USER_UNKNOWN,
                     upstreamEvents, getElapsedRealtime());
-        reset(mTetheringMetrics);
         clearElapsedRealtime();
-        mTetheringMetrics.cleanup();
     }
 
     @Test
@@ -231,6 +229,7 @@
 
     private void runUserTypesTest(final String callerPkg, final UserType expectedResult)
             throws Exception {
+        mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
         mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg);
         final long duration = 1 * SECOND_IN_MILLIS;
         incrementCurrentTime(duration);
@@ -241,9 +240,7 @@
         addUpstreamEvent(upstreamEvents, UpstreamType.UT_NO_NETWORK, duration, 0L, 0L);
         verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR, expectedResult,
                     upstreamEvents, getElapsedRealtime());
-        reset(mTetheringMetrics);
         clearElapsedRealtime();
-        mTetheringMetrics.cleanup();
     }
 
     @Test
@@ -256,6 +253,7 @@
 
     private void runUpstreamTypesTest(final UpstreamNetworkState ns,
             final UpstreamType expectedResult) throws Exception {
+        mTetheringMetrics = new TetheringMetrics(mContext, mDeps);
         mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
         mTetheringMetrics.maybeUpdateUpstreamType(ns);
         final long duration = 2 * SECOND_IN_MILLIS;
@@ -266,9 +264,7 @@
         addUpstreamEvent(upstreamEvents, expectedResult, duration, 0L, 0L);
         verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
                 UserType.USER_UNKNOWN, upstreamEvents, getElapsedRealtime());
-        reset(mTetheringMetrics);
         clearElapsedRealtime();
-        mTetheringMetrics.cleanup();
     }
 
     @Test
@@ -379,4 +375,21 @@
                 UserType.USER_SETTINGS, upstreamEvents,
                 currentTimeMillis() - wifiTetheringStartTime);
     }
+
+    private void runUsageSupportedForUpstreamTypeTest(final UpstreamType upstreamType,
+            final boolean isSupported) {
+        final boolean result = TetheringMetrics.isUsageSupportedForUpstreamType(upstreamType);
+        assertEquals(isSupported, result);
+    }
+
+    @Test
+    public void testUsageSupportedForUpstreamTypeTest() {
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_CELLULAR, true /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_WIFI, true /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_BLUETOOTH, true /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_ETHERNET, true /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_WIFI_AWARE, false /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_LOWPAN, false /* isSupported */);
+        runUsageSupportedForUpstreamTypeTest(UpstreamType.UT_UNKNOWN, false /* isSupported */);
+    }
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
index fcfb15f..c575d40 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -29,7 +29,9 @@
 import android.util.ArrayMap;
 import android.util.Log;
 
+import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.net.DatagramPacket;
@@ -225,6 +227,12 @@
             Log.wtf(TAG, "No mDns packets to send");
             return;
         }
+        // Check all packets with the same address
+        if (!MdnsUtils.checkAllPacketsWithSameAddress(packets)) {
+            Log.wtf(TAG, "Some mDNS packets have a different target address. addresses="
+                    + CollectionUtils.map(packets, DatagramPacket::getSocketAddress));
+            return;
+        }
 
         final boolean isIpv6 = ((InetSocketAddress) packets.get(0).getSocketAddress())
                 .getAddress() instanceof Inet6Address;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index b3bdbe0..643430a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -303,8 +303,8 @@
         serviceCache.unregisterServiceExpiredCallback(cacheKey);
     }
 
-    private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(
-            @NonNull MdnsResponse response, @NonNull String[] serviceTypeLabels) {
+    private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(@NonNull MdnsResponse response,
+            @NonNull String[] serviceTypeLabels, long elapsedRealtimeMillis) {
         String[] hostName = null;
         int port = 0;
         if (response.hasServiceRecord()) {
@@ -351,7 +351,7 @@
                 textEntries,
                 response.getInterfaceIndex(),
                 response.getNetwork(),
-                now.plusMillis(response.getMinRemainingTtl(now.toEpochMilli())));
+                now.plusMillis(response.getMinRemainingTtl(elapsedRealtimeMillis)));
     }
 
     private List<MdnsResponse> getExistingServices() {
@@ -380,8 +380,8 @@
         if (existingInfo == null) {
             for (MdnsResponse existingResponse : serviceCache.getCachedServices(cacheKey)) {
                 if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
-                final MdnsServiceInfo info =
-                        buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
+                final MdnsServiceInfo info = buildMdnsServiceInfoFromResponse(
+                        existingResponse, serviceTypeLabels, clock.elapsedRealtime());
                 listener.onServiceNameDiscovered(info, true /* isServiceFromCache */);
                 listenerInfo.setServiceDiscovered(info.getServiceInstanceName());
                 if (existingResponse.isComplete()) {
@@ -561,7 +561,7 @@
             if (response.getServiceInstanceName() != null) {
                 listeners.valueAt(i).unsetServiceDiscovered(response.getServiceInstanceName());
                 final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
-                        response, serviceTypeLabels);
+                        response, serviceTypeLabels, clock.elapsedRealtime());
                 if (response.isComplete()) {
                     sharedLog.log(message + ". onServiceRemoved: " + serviceInfo);
                     listener.onServiceRemoved(serviceInfo);
@@ -605,8 +605,8 @@
                         + " %b, responseIsComplete: %b",
                 serviceInstanceName, newInCache, serviceBecomesComplete,
                 response.isComplete()));
-        MdnsServiceInfo serviceInfo =
-                buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
+        final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
+                response, serviceTypeLabels, clock.elapsedRealtime());
 
         for (int i = 0; i < listeners.size(); i++) {
             // If a service stops matching the options (currently can only happen if it loses a
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
index 9cfcba1..17e5b31 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
@@ -28,7 +28,9 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
 import java.net.DatagramPacket;
@@ -249,6 +251,12 @@
             Log.wtf(TAG, "No mDns packets to send");
             return;
         }
+        // Check all packets with the same address
+        if (!MdnsUtils.checkAllPacketsWithSameAddress(packets)) {
+            Log.wtf(TAG, "Some mDNS packets have a different target address. addresses="
+                    + CollectionUtils.map(packets, DatagramPacket::getSocketAddress));
+            return;
+        }
 
         final boolean isIpv4 = ((InetSocketAddress) packets.get(0).getSocketAddress())
                 .getAddress() instanceof Inet4Address;
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index 3c11a24..226867f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -34,6 +34,7 @@
 
 import java.io.IOException;
 import java.net.DatagramPacket;
+import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
@@ -361,4 +362,23 @@
             return SystemClock.elapsedRealtime();
         }
     }
+
+    /**
+     * Check all DatagramPackets with the same destination address.
+     */
+    public static boolean checkAllPacketsWithSameAddress(List<DatagramPacket> packets) {
+        // No packet for address check
+        if (packets.isEmpty()) {
+            return true;
+        }
+
+        final InetAddress address =
+                ((InetSocketAddress) packets.get(0).getSocketAddress()).getAddress();
+        for (DatagramPacket packet : packets) {
+            if (!address.equals(((InetSocketAddress) packet.getSocketAddress()).getAddress())) {
+                return false;
+            }
+        }
+        return true;
+    }
 }
\ No newline at end of file
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
index 02a9ce6..4027038 100644
--- a/service/ServiceConnectivityResources/res/values/config_thread.xml
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -54,9 +54,21 @@
     -->
     <string translatable="false" name="config_thread_model_name">Thread Border Router</string>
 
-    <!-- Whether the Thread network will be managed by the Google Home ecosystem. When this value
-    is set, a TXT entry "vgh=0" or "vgh=1" will be added to the "_mehscop._udp" mDNS service
-    respectively (The TXT value is a string).
+    <!-- Specifies vendor-specific mDNS TXT entries which will be included in the "_meshcop._udp"
+    service. The TXT entries list MUST conform to the format requirement in RFC 6763 section 6. For
+    example, the key and value of each TXT entry MUST be separated with "=". If the value length is
+    0, the trailing "=" may be omitted. Additionally, the TXT keys MUST start with "v" and be at
+    least 2 characters.
+
+    Note, do not include credentials in any of the TXT entries - they will be advertised on Wi-Fi
+    or Ethernet link.
+
+    An example config can be:
+      <string-array name="config_thread_mdns_vendor_specific_txts">
+        <item>vab=123</item>
+        <item>vcd</item>
+      </string-array>
     -->
-    <bool name="config_thread_managed_by_google_home">false</bool>
+    <string-array name="config_thread_mdns_vendor_specific_txts">
+    </string-array>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 158b0c8..fbaae05 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -51,6 +51,7 @@
             <item type="string" name="config_thread_vendor_name" />
             <item type="string" name="config_thread_vendor_oui" />
             <item type="string" name="config_thread_model_name" />
+            <item type="array" name="config_thread_mdns_vendor_specific_txts" />
         </policy>
     </overlayable>
 </resources>
diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
index cf6127f..917ad4d 100644
--- a/service/src/com/android/server/connectivity/ConnectivityNativeService.java
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -23,6 +23,7 @@
 import android.os.Binder;
 import android.os.Process;
 import android.os.ServiceSpecificException;
+import android.os.UserHandle;
 import android.system.ErrnoException;
 import android.util.Log;
 
@@ -67,8 +68,8 @@
     }
 
     private void enforceBlockPortPermission() {
-        final int uid = Binder.getCallingUid();
-        if (uid == Process.ROOT_UID || uid == Process.PHONE_UID) return;
+        final int appId = UserHandle.getAppId(Binder.getCallingUid());
+        if (appId == Process.ROOT_UID || appId == Process.PHONE_UID) return;
         PermissionUtils.enforceNetworkStackPermission(mContext);
     }
 
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 2c3a558..14bd5df 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -345,12 +345,10 @@
     ],
     sdk_version: "module_current",
     min_sdk_version: "30",
-    static_libs: [
-        "modules-utils-build_system",
-    ],
     libs: [
         "framework-annotations-lib",
         "framework-connectivity",
+        "modules-utils-build_system",
     ],
     // TODO: remove "apex_available:platform".
     apex_available: [
diff --git a/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
index e37061c..02e3643 100644
--- a/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
+++ b/staticlibs/device/com/android/net/module/util/RoutingCoordinatorManager.java
@@ -18,12 +18,10 @@
 
 import android.content.Context;
 import android.net.RouteInfo;
-import android.os.Build;
 import android.os.IBinder;
 import android.os.RemoteException;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
 
 /**
  * A manager class for talking to the routing coordinator service.
@@ -32,7 +30,6 @@
  * by the build rules. Do not change build rules to gain access to this class from elsewhere.
  * @hide
  */
-@RequiresApi(Build.VERSION_CODES.S)
 public class RoutingCoordinatorManager {
     @NonNull final Context mContext;
     @NonNull final IRoutingCoordinator mService;
diff --git a/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java b/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java
index cd6bfec..a638cc4 100644
--- a/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/SingleWriterBpfMap.java
@@ -19,6 +19,7 @@
 import android.system.ErrnoException;
 import android.util.Pair;
 
+import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 
@@ -60,6 +61,7 @@
 public class SingleWriterBpfMap<K extends Struct, V extends Struct> extends BpfMap<K, V> {
     // HashMap instead of ArrayMap because it performs better on larger maps, and many maps used in
     // our code can contain hundreds of items.
+    @GuardedBy("this")
     private final HashMap<K, V> mCache = new HashMap<>();
 
     // This should only ever be called (hence private) once for a given 'path'.
@@ -72,10 +74,12 @@
         super(path, BPF_F_RDWR_EXCLUSIVE, key, value);
 
         // Populate cache with the current map contents.
-        K currentKey = super.getFirstKey();
-        while (currentKey != null) {
-            mCache.put(currentKey, super.getValue(currentKey));
-            currentKey = super.getNextKey(currentKey);
+        synchronized (this) {
+            K currentKey = super.getFirstKey();
+            while (currentKey != null) {
+                mCache.put(currentKey, super.getValue(currentKey));
+                currentKey = super.getNextKey(currentKey);
+            }
         }
     }
 
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index a8c50d8..f1ff2e4 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -260,6 +260,18 @@
     public static final short DNS_OVER_TLS_PORT = 853;
 
     /**
+     * Dns query type constants.
+     *
+     * See also:
+     *    - https://datatracker.ietf.org/doc/html/rfc1035#section-3.2.2
+     */
+    public static final int TYPE_A = 1;
+    public static final int TYPE_PTR = 12;
+    public static final int TYPE_TXT = 16;
+    public static final int TYPE_AAAA = 28;
+    public static final int TYPE_SRV = 33;
+
+    /**
      * IEEE802.11 standard constants.
      *
      * See also:
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Log.h b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
index d266cbc..2de5ed7 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/Log.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
@@ -203,7 +203,7 @@
     void record(Level lvl, const std::string& entry);
 
     mutable std::shared_mutex mLock;
-    std::deque<const std::string> mEntries;  // GUARDED_BY(mLock), when supported
+    std::deque<std::string> mEntries;  // GUARDED_BY(mLock), when supported
 };
 
 }  // namespace netdutils
diff --git a/staticlibs/tests/unit/host/python/apf_utils_test.py b/staticlibs/tests/unit/host/python/apf_utils_test.py
index 8b390e3..caaf959 100644
--- a/staticlibs/tests/unit/host/python/apf_utils_test.py
+++ b/staticlibs/tests/unit/host/python/apf_utils_test.py
@@ -13,13 +13,16 @@
 #  limitations under the License.
 
 from unittest.mock import MagicMock, patch
+from absl.testing import parameterized
 from mobly import asserts
 from mobly import base_test
 from mobly import config_parser
 from mobly.controllers.android_device_lib.adb import AdbError
 from net_tests_utils.host.python.apf_utils import (
+    ApfCapabilities,
     PatternNotFoundException,
     UnsupportedOperationException,
+    get_apf_capabilities,
     get_apf_counter,
     get_apf_counters_from_dumpsys,
     get_hardware_address,
@@ -29,7 +32,7 @@
 from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError
 
 
-class TestApfUtils(base_test.BaseTestClass):
+class TestApfUtils(base_test.BaseTestClass, parameterized.TestCase):
 
   def __init__(self, configs: config_parser.TestRunConfig):
     super().__init__(configs)
@@ -150,3 +153,23 @@
     )
     with asserts.assert_raises(UnsupportedOperationException):
       send_raw_packet_downstream(self.mock_ad, "eth0", "AABBCCDDEEFF")
+
+  @parameterized.parameters(
+      ("2,2048,1", ApfCapabilities(2, 2048, 1)),  # Valid input
+      ("3,1024,0", ApfCapabilities(3, 1024, 0)),  # Valid input
+      ("invalid,output", ApfCapabilities(0, 0, 0)),  # Invalid input
+      ("", ApfCapabilities(0, 0, 0)),  # Empty input
+  )
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_get_apf_capabilities(
+      self, mock_output, expected_result, mock_adb_shell
+  ):
+    """Tests the get_apf_capabilities function with various inputs and expected results."""
+    # Configure the mock adb_shell to return the specified output
+    mock_adb_shell.return_value = mock_output
+
+    # Call the function under test
+    result = get_apf_capabilities(self.mock_ad, "wlan0")
+
+    # Assert that the result matches the expected result
+    asserts.assert_equal(result, expected_result)
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
index f71464c..415799c 100644
--- a/staticlibs/testutils/host/python/apf_utils.py
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -12,7 +12,9 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
+from dataclasses import dataclass
 import re
+from mobly import asserts
 from mobly.controllers import android_device
 from mobly.controllers.android_device_lib.adb import AdbError
 from net_tests_utils.host.python import adb_utils, assert_utils
@@ -190,3 +192,65 @@
     raise assert_utils.UnexpectedBehaviorError(
         f"Got unexpected output: {output} for command: {cmd}."
     )
+
+
+@dataclass
+class ApfCapabilities:
+  """APF program support capabilities.
+
+  See android.net.apf.ApfCapabilities.
+
+  Attributes:
+      apf_version_supported (int): Version of APF instruction set supported for
+        packet filtering. 0 indicates no support for packet filtering using APF
+        programs.
+      apf_ram_size (int): Size of APF ram.
+      apf_packet_format (int): Format of packets passed to APF filter. Should be
+        one of ARPHRD_*
+  """
+
+  apf_version_supported: int
+  apf_ram_size: int
+  apf_packet_format: int
+
+  def __init__(
+      self,
+      apf_version_supported: int,
+      apf_ram_size: int,
+      apf_packet_format: int,
+  ):
+    self.apf_version_supported = apf_version_supported
+    self.apf_ram_size = apf_ram_size
+    self.apf_packet_format = apf_packet_format
+
+  def __str__(self):
+    """Returns a user-friendly string representation of the APF capabilities."""
+    return (
+        f"APF Version: {self.apf_version_supported}\n"
+        f"Ram Size: {self.apf_ram_size} bytes\n"
+        f"Packet Format: {self.apf_packet_format}"
+    )
+
+
+def get_apf_capabilities(
+    ad: android_device.AndroidDevice, iface_name: str
+) -> ApfCapabilities:
+  output = adb_utils.adb_shell(
+      ad, f"cmd network_stack apf {iface_name} capabilities"
+  )
+  try:
+    values = [int(value_str) for value_str in output.split(",")]
+  except ValueError:
+    return ApfCapabilities(0, 0, 0)  # Conversion to integer failed
+  return ApfCapabilities(values[0], values[1], values[2])
+
+
+def assume_apf_version_support_at_least(
+    ad: android_device.AndroidDevice, iface_name: str, expected_version: int
+) -> None:
+  caps = get_apf_capabilities(ad, iface_name)
+  asserts.skip_if(
+      caps.apf_version_supported < expected_version,
+      f"Supported apf version {caps.apf_version_supported} < expected version"
+      f" {expected_version}",
+  )
diff --git a/staticlibs/testutils/host/python/mdns_utils.py b/staticlibs/testutils/host/python/mdns_utils.py
index ec1fea0..1234e54 100644
--- a/staticlibs/testutils/host/python/mdns_utils.py
+++ b/staticlibs/testutils/host/python/mdns_utils.py
@@ -12,9 +12,24 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
+from mobly import asserts
 from mobly.controllers import android_device
 
 
+def assume_mdns_test_preconditions(
+    advertising_device: android_device, discovery_device: android_device
+) -> None:
+  advertising = advertising_device.connectivity_multi_devices_snippet
+  discovery = discovery_device.connectivity_multi_devices_snippet
+
+  asserts.skip_if(
+      not advertising.isAtLeastT(), "Advertising device SDK is lower than T."
+  )
+  asserts.skip_if(
+      not discovery.isAtLeastT(), "Discovery device SDK is lower than T."
+  )
+
+
 def register_mdns_service_and_discover_resolve(
     advertising_device: android_device, discovery_device: android_device
 ) -> None:
diff --git a/tests/cts/multidevices/connectivity_multi_devices_test.py b/tests/cts/multidevices/connectivity_multi_devices_test.py
index 0cfc361..7e7bbf5 100644
--- a/tests/cts/multidevices/connectivity_multi_devices_test.py
+++ b/tests/cts/multidevices/connectivity_multi_devices_test.py
@@ -68,6 +68,9 @@
     tether_utils.assume_hotspot_test_preconditions(
         self.serverDevice, self.clientDevice, UpstreamType.NONE
     )
+    mdns_utils.assume_mdns_test_preconditions(
+        self.clientDevice, self.serverDevice
+    )
     try:
       # Connectivity of the client verified by asserting the validated capability.
       tether_utils.setup_hotspot_and_client_for_upstream_type(
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
index 9bdf4a3..7368669 100644
--- a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -36,6 +36,7 @@
 import android.net.wifi.WifiNetworkSpecifier
 import android.net.wifi.WifiSsid
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
 import com.android.testutils.AutoReleaseNetworkCallbackRule
 import com.android.testutils.ConnectUtil
 import com.android.testutils.NetworkCallbackHelper
@@ -71,6 +72,9 @@
     @Rpc(description = "Check whether the device supporters AP + STA concurrency.")
     fun isStaApConcurrencySupported() = wifiManager.isStaApConcurrencySupported()
 
+    @Rpc(description = "Check whether the device SDK is as least T")
+    fun isAtLeastT() = SdkLevel.isAtLeastT()
+
     @Rpc(description = "Request cellular connection and ensure it is the default network.")
     fun requestCellularAndEnsureDefault() {
         ctsNetUtils.disableWifi()
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 2f88c41..ef3ebb0 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -92,6 +92,7 @@
         "frameworks-net-integration-testutils",
         "framework-protos",
         "mockito-target-minus-junit4",
+        "modules-utils-build",
         "net-tests-utils",
         "net-utils-services-common",
         "platform-compat-test-rules",
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
index fb3d183..4c71991 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
@@ -18,8 +18,10 @@
 
 import static com.android.server.connectivity.mdns.MdnsSocketProvider.SocketCallback;
 import static com.android.server.connectivity.mdns.MulticastPacketReader.PacketHandler;
+import static com.android.testutils.Cleanup.testAndCleanup;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
@@ -35,6 +37,7 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.util.Log;
 
 import com.android.net.module.util.HexDump;
 import com.android.net.module.util.SharedLog;
@@ -59,6 +62,7 @@
 import java.net.SocketException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 @RunWith(DevSdkIgnoreRunner.class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -437,4 +441,34 @@
             inOrder.verify(mSocket).send(packets.get(i));
         }
     }
+
+    @Test
+    public void testSendPacketWithMultiplePacketsWithDifferentAddresses() throws IOException {
+        final SocketCallback callback = expectSocketCallback();
+        final DatagramPacket ipv4Packet = new DatagramPacket(BUFFER, 0 /* offset */, BUFFER.length,
+                InetAddresses.parseNumericAddress("192.0.2.1"), 0 /* port */);
+        final DatagramPacket ipv6Packet = new DatagramPacket(BUFFER, 0 /* offset */, BUFFER.length,
+                InetAddresses.parseNumericAddress("2001:db8::"), 0 /* port */);
+        doReturn(true).when(mSocket).hasJoinedIpv4();
+        doReturn(true).when(mSocket).hasJoinedIpv6();
+        doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+
+        // Notify socket created
+        callback.onSocketCreated(mSocketKey, mSocket, List.of());
+        verify(mSocketCreationCallback).onSocketCreated(mSocketKey);
+
+        // Send packets with IPv4 and IPv6 then verify wtf logs and sending has never been called.
+        // Override the default TerribleFailureHandler, as that handler might terminate the process
+        // (if we're on an eng build).
+        final AtomicBoolean hasFailed = new AtomicBoolean(false);
+        final Log.TerribleFailureHandler originalHandler =
+                Log.setWtfHandler((tag, what, system) -> hasFailed.set(true));
+        testAndCleanup(() -> {
+            mSocketClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet, ipv6Packet),
+                    mSocketKey, false /* onlyUseIpv6OnIpv6OnlyNetworks */);
+            HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+            assertTrue(hasFailed.get());
+            verify(mSocket, never()).send(any());
+        }, () -> Log.setWtfHandler(originalHandler));
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
index 1989ed3..ab70e38 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
@@ -16,6 +16,7 @@
 
 package com.android.server.connectivity.mdns;
 
+import static com.android.testutils.Cleanup.testAndCleanup;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
 import static org.junit.Assert.assertFalse;
@@ -38,9 +39,11 @@
 import android.annotation.RequiresPermission;
 import android.content.Context;
 import android.net.ConnectivityManager;
+import android.net.InetAddresses;
 import android.net.wifi.WifiManager;
 import android.net.wifi.WifiManager.MulticastLock;
 import android.text.format.DateUtils;
+import android.util.Log;
 
 import com.android.net.module.util.HexDump;
 import com.android.net.module.util.SharedLog;
@@ -594,6 +597,29 @@
         }
     }
 
+    @Test
+    public void testSendPacketWithMultiplePacketsWithDifferentAddresses() throws IOException {
+        mdnsClient.startDiscovery();
+        final byte[] buffer = new byte[10];
+        final DatagramPacket ipv4Packet = new DatagramPacket(buffer, 0 /* offset */, buffer.length,
+                InetAddresses.parseNumericAddress("192.0.2.1"), 0 /* port */);
+        final DatagramPacket ipv6Packet = new DatagramPacket(buffer, 0 /* offset */, buffer.length,
+                InetAddresses.parseNumericAddress("2001:db8::"), 0 /* port */);
+
+        // Send packets with IPv4 and IPv6 then verify wtf logs and sending has never been called.
+        // Override the default TerribleFailureHandler, as that handler might terminate the process
+        // (if we're on an eng build).
+        final AtomicBoolean hasFailed = new AtomicBoolean(false);
+        final Log.TerribleFailureHandler originalHandler =
+                Log.setWtfHandler((tag, what, system) -> hasFailed.set(true));
+        testAndCleanup(() -> {
+            mdnsClient.sendPacketRequestingMulticastResponse(List.of(ipv4Packet, ipv6Packet),
+                    false /* onlyUseIpv6OnIpv6OnlyNetworks */);
+            assertTrue(hasFailed.get());
+            verify(mockMulticastSocket, never()).send(any());
+        }, () -> Log.setWtfHandler(originalHandler));
+    }
+
     private DatagramPacket getTestDatagramPacket() {
         return new DatagramPacket(buf, 0, 5,
                 new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), 5353 /* port */));
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
index 009205e..cf88d05 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
@@ -16,9 +16,12 @@
 
 package com.android.server.connectivity.mdns.util
 
+import android.net.InetAddresses
 import android.os.Build
 import com.android.server.connectivity.mdns.MdnsConstants
 import com.android.server.connectivity.mdns.MdnsConstants.FLAG_TRUNCATED
+import com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR
+import com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR
 import com.android.server.connectivity.mdns.MdnsPacket
 import com.android.server.connectivity.mdns.MdnsPacketReader
 import com.android.server.connectivity.mdns.MdnsPointerRecord
@@ -193,4 +196,31 @@
         }
         return MdnsPacket(flags, questions, answers, emptyList(), emptyList())
     }
+
+    @Test
+    fun testCheckAllPacketsWithSameAddress() {
+        val buffer = ByteArray(10)
+        val v4Packet = DatagramPacket(buffer, buffer.size, IPV4_SOCKET_ADDR)
+        val otherV4Packet = DatagramPacket(
+            buffer,
+            buffer.size,
+            InetAddresses.parseNumericAddress("192.0.2.1"),
+            1234
+        )
+        val v6Packet = DatagramPacket(ByteArray(10), 10, IPV6_SOCKET_ADDR)
+        val otherV6Packet = DatagramPacket(
+            buffer,
+            buffer.size,
+            InetAddresses.parseNumericAddress("2001:db8::"),
+            1234
+        )
+        assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf()))
+        assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet)))
+        assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet, v4Packet)))
+        assertFalse(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet, otherV4Packet)))
+        assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v6Packet)))
+        assertTrue(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v6Packet, v6Packet)))
+        assertFalse(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v6Packet, otherV6Packet)))
+        assertFalse(MdnsUtils.checkAllPacketsWithSameAddress(listOf(v4Packet, v6Packet)))
+    }
 }
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 2f60d9a..e6f272b 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -111,7 +111,6 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserManager;
-import android.provider.Settings;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -139,6 +138,7 @@
 import java.time.Clock;
 import java.time.DateTimeException;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -210,7 +210,6 @@
     private final ThreadPersistentSettings mPersistentSettings;
     private final UserManager mUserManager;
     private boolean mUserRestricted;
-    private boolean mAirplaneModeOn;
     private boolean mForceStopOtDaemonEnabled;
 
     private BorderRouterConfigurationParcel mBorderRouterConfig;
@@ -344,8 +343,8 @@
         final String modelName = resources.getString(R.string.config_thread_model_name);
         final String vendorName = resources.getString(R.string.config_thread_vendor_name);
         final String vendorOui = resources.getString(R.string.config_thread_vendor_oui);
-        final boolean managedByGoogle =
-                resources.getBoolean(R.bool.config_thread_managed_by_google_home);
+        final String[] vendorSpecificTxts =
+                resources.getStringArray(R.array.config_thread_mdns_vendor_specific_txts);
 
         if (!modelName.isEmpty()) {
             if (modelName.getBytes(UTF_8).length > MAX_MODEL_NAME_UTF8_BYTES) {
@@ -375,19 +374,44 @@
         meshcopTxts.modelName = modelName;
         meshcopTxts.vendorName = vendorName;
         meshcopTxts.vendorOui = HexEncoding.decode(vendorOui.replace("-", "").replace(":", ""));
-        meshcopTxts.nonStandardTxtEntries = List.of(makeManagedByGoogleTxtAttr(managedByGoogle));
+        meshcopTxts.nonStandardTxtEntries = makeVendorSpecificTxtAttrs(vendorSpecificTxts);
 
         return meshcopTxts;
     }
 
     /**
-     * Creates a DNS-SD TXT entry for indicating whether Thread on this device is managed by Google.
+     * Parses vendor-specific TXT entries from "=" separated strings into list of {@link
+     * DnsTxtAttribute}.
      *
-     * @return TXT entry "vgh=1" if {@code managedByGoogle} is {@code true}; otherwise, "vgh=0"
+     * @throws IllegalArgumentsException if invalid TXT entries are found in {@code vendorTxts}
      */
-    private static DnsTxtAttribute makeManagedByGoogleTxtAttr(boolean managedByGoogle) {
-        final byte[] value = (managedByGoogle ? "1" : "0").getBytes(UTF_8);
-        return new DnsTxtAttribute("vgh", value);
+    @VisibleForTesting
+    static List<DnsTxtAttribute> makeVendorSpecificTxtAttrs(String[] vendorTxts) {
+        List<DnsTxtAttribute> txts = new ArrayList<>();
+        for (String txt : vendorTxts) {
+            String[] kv = txt.split("=", 2 /* limit */); // Split with only the first '='
+            if (kv.length < 1) {
+                throw new IllegalArgumentException(
+                        "Invalid vendor-specific TXT is found in resources: " + txt);
+            }
+
+            if (kv[0].length() < 2) {
+                throw new IllegalArgumentException(
+                        "Invalid vendor-specific TXT key \""
+                                + kv[0]
+                                + "\": it must contain at least 2 characters");
+            }
+
+            if (!kv[0].startsWith("v")) {
+                throw new IllegalArgumentException(
+                        "Invalid vendor-specific TXT key \""
+                                + kv[0]
+                                + "\": it doesn't start with \"v\"");
+            }
+
+            txts.add(new DnsTxtAttribute(kv[0], (kv.length >= 2 ? kv[1] : "").getBytes(UTF_8)));
+        }
+        return txts;
     }
 
     private void onOtDaemonDied() {
@@ -420,8 +444,6 @@
                     requestThreadNetwork();
                     mUserRestricted = isThreadUserRestricted();
                     registerUserRestrictionsReceiver();
-                    mAirplaneModeOn = isAirplaneModeOn();
-                    registerAirplaneModeReceiver();
                     maybeInitializeOtDaemon();
                 });
     }
@@ -503,15 +525,6 @@
             // the otDaemon set enabled state operation succeeded or not, so that it can recover
             // to the desired value after reboot.
             mPersistentSettings.put(ThreadPersistentSettings.THREAD_ENABLED.key, isEnabled);
-
-            // Remember whether the user wanted to keep Thread enabled in airplane mode. If once
-            // the user disabled Thread again in airplane mode, the persistent settings state is
-            // reset (so that Thread will be auto-disabled again when airplane mode is turned on).
-            // This behavior is consistent with Wi-Fi and bluetooth.
-            if (mAirplaneModeOn) {
-                mPersistentSettings.put(
-                        ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE.key, isEnabled);
-            }
         }
 
         try {
@@ -598,10 +611,12 @@
                 new BroadcastReceiver() {
                     @Override
                     public void onReceive(Context context, Intent intent) {
-                        mHandler.post(() -> onUserRestrictionsChanged(isThreadUserRestricted()));
+                        onUserRestrictionsChanged(isThreadUserRestricted());
                     }
                 },
-                new IntentFilter(UserManager.ACTION_USER_RESTRICTIONS_CHANGED));
+                new IntentFilter(UserManager.ACTION_USER_RESTRICTIONS_CHANGED),
+                null /* broadcastPermission */,
+                mHandler);
     }
 
     private void onUserRestrictionsChanged(boolean newUserRestrictedState) {
@@ -648,72 +663,13 @@
         return mUserManager.hasUserRestriction(DISALLOW_THREAD_NETWORK);
     }
 
-    private void registerAirplaneModeReceiver() {
-        mContext.registerReceiver(
-                new BroadcastReceiver() {
-                    @Override
-                    public void onReceive(Context context, Intent intent) {
-                        mHandler.post(() -> onAirplaneModeChanged(isAirplaneModeOn()));
-                    }
-                },
-                new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
-    }
-
-    private void onAirplaneModeChanged(boolean newAirplaneModeOn) {
-        checkOnHandlerThread();
-        if (mAirplaneModeOn == newAirplaneModeOn) {
-            return;
-        }
-        Log.i(TAG, "Airplane mode changed: " + mAirplaneModeOn + " -> " + newAirplaneModeOn);
-        mAirplaneModeOn = newAirplaneModeOn;
-
-        final boolean shouldEnableThread = shouldEnableThread();
-        final IOperationReceiver receiver =
-                new IOperationReceiver.Stub() {
-                    @Override
-                    public void onSuccess() {
-                        Log.d(
-                                TAG,
-                                (shouldEnableThread ? "Enabled" : "Disabled")
-                                        + " Thread due to airplane mode change");
-                    }
-
-                    @Override
-                    public void onError(int errorCode, String errorMessage) {
-                        Log.e(
-                                TAG,
-                                "Failed to "
-                                        + (shouldEnableThread ? "enable" : "disable")
-                                        + " Thread for airplane mode change");
-                    }
-                };
-        // Do not save the user restriction state to persistent settings so that the user
-        // configuration won't be overwritten
-        setEnabledInternal(
-                shouldEnableThread, false /* persist */, new OperationReceiverWrapper(receiver));
-    }
-
-    /** Returns {@code true} if Airplane mode has been turned on. */
-    private boolean isAirplaneModeOn() {
-        return Settings.Global.getInt(
-                        mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0)
-                == 1;
-    }
-
     /**
      * Returns {@code true} if Thread should be enabled based on current settings, runtime user
-     * restriction and airplane mode state.
+     * restriction state.
      */
     private boolean shouldEnableThread() {
-        final boolean enabledInAirplaneMode =
-                mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE);
-
         return !mForceStopOtDaemonEnabled
                 && !mUserRestricted
-                // FIXME(b/340744397): Note that here we need to call `isAirplaneModeOn()` to get
-                // the latest state of airplane mode but can't use `mIsAirplaneMode`. This is for
-                // avoiding the race conditions described in b/340744397
-                && (!isAirplaneModeOn() || enabledInAirplaneMode)
                 && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
     }
 
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 41f34ff..22e7a98 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -45,7 +45,6 @@
 
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
-
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
 import android.content.Context;
@@ -859,7 +858,6 @@
         assertThat(txtMap.get("rv")).isNotNull();
         assertThat(txtMap.get("tv")).isNotNull();
         assertThat(txtMap.get("sb")).isNotNull();
-        assertThat(new String(txtMap.get("vgh"))).isIn(List.of("0", "1"));
     }
 
     @Test
@@ -886,7 +884,6 @@
         assertThat(txtMap.get("tv")).isNotNull();
         assertThat(txtMap.get("sb")).isNotNull();
         assertThat(txtMap.get("id").length).isEqualTo(16);
-        assertThat(new String(txtMap.get("vgh"))).isIn(List.of("0", "1"));
     }
 
     @Test
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 6e2369f..eaf11b1 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -26,7 +26,6 @@
 import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
 
 import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
-import static com.android.server.thread.ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE;
 import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
 
 import static com.google.common.io.BaseEncoding.base16;
@@ -49,6 +48,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -68,7 +69,6 @@
 import android.os.SystemClock;
 import android.os.UserManager;
 import android.os.test.TestLooper;
-import android.provider.Settings;
 import android.util.AtomicFile;
 
 import androidx.test.annotation.UiThreadTest;
@@ -96,11 +96,11 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.MockitoSession;
 
-import java.nio.charset.StandardCharsets;
 import java.time.Clock;
 import java.time.DateTimeException;
 import java.time.Instant;
 import java.time.ZoneId;
+import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicReference;
@@ -150,7 +150,6 @@
     private static final byte[] TEST_VENDOR_OUI_BYTES = new byte[] {(byte) 0xAC, (byte) 0xDE, 0x48};
     private static final String TEST_VENDOR_NAME = "test vendor";
     private static final String TEST_MODEL_NAME = "test model";
-    private static final boolean TEST_VGH_VALUE = false;
 
     @Mock private ConnectivityManager mMockConnectivityManager;
     @Mock private NetworkAgent mMockNetworkAgent;
@@ -193,8 +192,6 @@
 
         when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
 
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
-
         when(mConnectivityResources.get()).thenReturn(mResources);
         when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
         when(mResources.getString(eq(R.string.config_thread_vendor_name)))
@@ -203,8 +200,8 @@
                 .thenReturn(TEST_VENDOR_OUI);
         when(mResources.getString(eq(R.string.config_thread_model_name)))
                 .thenReturn(TEST_MODEL_NAME);
-        when(mResources.getBoolean(eq(R.bool.config_thread_managed_by_google_home)))
-                .thenReturn(TEST_VGH_VALUE);
+        when(mResources.getStringArray(eq(R.array.config_thread_mdns_vendor_specific_txts)))
+                .thenReturn(new String[] {});
 
         final AtomicFile storageFile = new AtomicFile(tempFolder.newFile("thread_settings.xml"));
         mPersistentSettings = new ThreadPersistentSettings(storageFile, mConnectivityResources);
@@ -247,8 +244,8 @@
                 .thenReturn(TEST_VENDOR_OUI);
         when(mResources.getString(eq(R.string.config_thread_model_name)))
                 .thenReturn(TEST_MODEL_NAME);
-        when(mResources.getBoolean(eq(R.bool.config_thread_managed_by_google_home)))
-                .thenReturn(true);
+        when(mResources.getStringArray(eq(R.array.config_thread_mdns_vendor_specific_txts)))
+                .thenReturn(new String[] {"vt=test"});
 
         mService.initialize();
         mTestLooper.dispatchAll();
@@ -258,19 +255,7 @@
         assertThat(meshcopTxts.vendorOui).isEqualTo(TEST_VENDOR_OUI_BYTES);
         assertThat(meshcopTxts.modelName).isEqualTo(TEST_MODEL_NAME);
         assertThat(meshcopTxts.nonStandardTxtEntries)
-                .containsExactly(new DnsTxtAttribute("vgh", "1".getBytes(StandardCharsets.UTF_8)));
-    }
-
-    @Test
-    public void getMeshcopTxtAttributes_managedByGoogleIsFalse_vghIsZero() {
-        when(mResources.getBoolean(eq(R.bool.config_thread_managed_by_google_home)))
-                .thenReturn(false);
-
-        MeshcopTxtAttributes meshcopTxts =
-                ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources);
-
-        assertThat(meshcopTxts.nonStandardTxtEntries)
-                .containsExactly(new DnsTxtAttribute("vgh", "0".getBytes(StandardCharsets.UTF_8)));
+                .containsExactly(new DnsTxtAttribute("vt", "test".getBytes(UTF_8)));
     }
 
     @Test
@@ -343,6 +328,61 @@
     }
 
     @Test
+    public void makeVendorSpecificTxtAttrs_validTxts_returnsParsedTxtAttrs() {
+        String[] txts = new String[] {"va=123", "vb=", "vc"};
+
+        List<DnsTxtAttribute> attrs = mService.makeVendorSpecificTxtAttrs(txts);
+
+        assertThat(attrs)
+                .containsExactly(
+                        new DnsTxtAttribute("va", "123".getBytes(UTF_8)),
+                        new DnsTxtAttribute("vb", new byte[] {}),
+                        new DnsTxtAttribute("vc", new byte[] {}));
+    }
+
+    @Test
+    public void makeVendorSpecificTxtAttrs_txtKeyNotStartWithV_throwsIllegalArgument() {
+        String[] txts = new String[] {"abc=123"};
+
+        assertThrows(
+                IllegalArgumentException.class, () -> mService.makeVendorSpecificTxtAttrs(txts));
+    }
+
+    @Test
+    public void makeVendorSpecificTxtAttrs_txtIsTooShort_throwsIllegalArgument() {
+        String[] txtEmptyKey = new String[] {"=123"};
+        String[] txtSingleCharKey = new String[] {"v=456"};
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mService.makeVendorSpecificTxtAttrs(txtEmptyKey));
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mService.makeVendorSpecificTxtAttrs(txtSingleCharKey));
+    }
+
+    @Test
+    public void makeVendorSpecificTxtAttrs_txtValueIsEmpty_parseSuccess() {
+        String[] txts = new String[] {"va=", "vb"};
+
+        List<DnsTxtAttribute> attrs = mService.makeVendorSpecificTxtAttrs(txts);
+
+        assertThat(attrs)
+                .containsExactly(
+                        new DnsTxtAttribute("va", new byte[] {}),
+                        new DnsTxtAttribute("vb", new byte[] {}));
+    }
+
+    @Test
+    public void makeVendorSpecificTxtAttrs_multipleEquals_splittedByTheFirstEqual() {
+        String[] txts = new String[] {"va=abc=def=123"};
+
+        List<DnsTxtAttribute> attrs = mService.makeVendorSpecificTxtAttrs(txts);
+
+        assertThat(attrs).containsExactly(new DnsTxtAttribute("va", "abc=def=123".getBytes(UTF_8)));
+    }
+
+    @Test
     public void join_otDaemonRemoteFailure_returnsInternalError() throws Exception {
         mService.initialize();
         final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
@@ -437,100 +477,6 @@
         assertThat(failure.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
     }
 
-    @Test
-    public void airplaneMode_initWithAirplaneModeOn_otDaemonNotStarted() {
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
-
-        mService.initialize();
-        mTestLooper.dispatchAll();
-
-        assertThat(mFakeOtDaemon.isInitialized()).isFalse();
-    }
-
-    @Test
-    public void airplaneMode_initWithAirplaneModeOff_threadIsEnabled() {
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
-
-        mService.initialize();
-        mTestLooper.dispatchAll();
-
-        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
-    }
-
-    @Test
-    public void airplaneMode_changesFromOffToOn_stateIsDisabled() {
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
-        AtomicReference<BroadcastReceiver> receiverRef =
-                captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
-        mService.initialize();
-        mTestLooper.dispatchAll();
-
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
-        receiverRef.get().onReceive(mContext, new Intent());
-        mTestLooper.dispatchAll();
-
-        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
-    }
-
-    @Test
-    public void airplaneMode_changesFromOnToOff_stateIsEnabled() {
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
-        AtomicReference<BroadcastReceiver> receiverRef =
-                captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
-        mService.initialize();
-        mTestLooper.dispatchAll();
-
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
-        receiverRef.get().onReceive(mContext, new Intent());
-        mTestLooper.dispatchAll();
-
-        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
-    }
-
-    @Test
-    public void airplaneMode_setEnabledWhenAirplaneModeIsOn_WillNotAutoDisableSecondTime() {
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
-        AtomicReference<BroadcastReceiver> receiverRef =
-                captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
-        CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
-        mService.initialize();
-
-        mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
-        mTestLooper.dispatchAll();
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
-        receiverRef.get().onReceive(mContext, new Intent());
-        mTestLooper.dispatchAll();
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
-        receiverRef.get().onReceive(mContext, new Intent());
-        mTestLooper.dispatchAll();
-
-        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
-        assertThat(mPersistentSettings.get(THREAD_ENABLED_IN_AIRPLANE_MODE)).isTrue();
-    }
-
-    @Test
-    public void airplaneMode_setDisabledWhenAirplaneModeIsOn_WillAutoDisableSecondTime() {
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
-        AtomicReference<BroadcastReceiver> receiverRef =
-                captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
-        CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
-        mService.initialize();
-        mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
-        mTestLooper.dispatchAll();
-
-        mService.setEnabled(false, newOperationReceiver(setEnabledFuture));
-        mTestLooper.dispatchAll();
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
-        receiverRef.get().onReceive(mContext, new Intent());
-        mTestLooper.dispatchAll();
-        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
-        receiverRef.get().onReceive(mContext, new Intent());
-        mTestLooper.dispatchAll();
-
-        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
-        assertThat(mPersistentSettings.get(THREAD_ENABLED_IN_AIRPLANE_MODE)).isFalse();
-    }
-
     private AtomicReference<BroadcastReceiver> captureBroadcastReceiver(String action) {
         AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
 
@@ -542,7 +488,9 @@
                 .when(mContext)
                 .registerReceiver(
                         any(BroadcastReceiver.class),
-                        argThat(actualIntentFilter -> actualIntentFilter.hasAction(action)));
+                        argThat(actualIntentFilter -> actualIntentFilter.hasAction(action)),
+                        any(),
+                        any());
 
         return receiverRef;
     }