Merge "Add CtsNetTestCase as presubmit for automotive multi-user multi-display" into main
diff --git a/Tethering/AndroidManifest.xml b/Tethering/AndroidManifest.xml
index 6a363b0..2a6f6d5 100644
--- a/Tethering/AndroidManifest.xml
+++ b/Tethering/AndroidManifest.xml
@@ -32,8 +32,10 @@
     <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
     <uses-permission android:name="android.permission.BROADCAST_STICKY" />
     <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
     <uses-permission android:name="android.permission.MANAGE_USB" />
     <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
+    <uses-permission android:name="android.permission.QUERY_USERS"/>
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
     <uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" />
     <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
diff --git a/Tethering/apex/permissions/permissions.xml b/Tethering/apex/permissions/permissions.xml
index f26a961..fcb287e 100644
--- a/Tethering/apex/permissions/permissions.xml
+++ b/Tethering/apex/permissions/permissions.xml
@@ -18,8 +18,10 @@
 <permissions>
     <privapp-permissions package="com.android.networkstack.tethering">
         <permission name="android.permission.BLUETOOTH_PRIVILEGED" />
+        <permission name="android.permission.INTERACT_ACROSS_USERS"/>
         <permission name="android.permission.MANAGE_USB"/>
         <permission name="android.permission.MODIFY_PHONE_STATE"/>
+        <permission name="android.permission.QUERY_USERS"/>
         <permission name="android.permission.READ_NETWORK_USAGE_HISTORY"/>
         <permission name="android.permission.TETHER_PRIVILEGED"/>
         <permission name="android.permission.UPDATE_APP_OPS_STATS"/>
diff --git a/Tethering/common/TetheringLib/api/module-lib-current.txt b/Tethering/common/TetheringLib/api/module-lib-current.txt
index 460c216..a680590 100644
--- a/Tethering/common/TetheringLib/api/module-lib-current.txt
+++ b/Tethering/common/TetheringLib/api/module-lib-current.txt
@@ -46,5 +46,10 @@
     method @Deprecated @NonNull public java.util.List<java.lang.String> getTetherableWifiRegexs();
   }
 
+  public static final class TetheringManager.TetheringRequest implements android.os.Parcelable {
+    method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") @Nullable public String getPackageName();
+    method @FlaggedApi("com.android.net.flags.tethering_request_with_soft_ap_config") public int getUid();
+  }
+
 }
 
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 411971d..7c7a4e0 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -33,6 +33,7 @@
 import android.os.IBinder;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.util.ArrayMap;
@@ -746,6 +747,7 @@
                 mBuilderParcel.exemptFromEntitlementCheck = false;
                 mBuilderParcel.showProvisioningUi = true;
                 mBuilderParcel.connectivityScope = getDefaultConnectivityScope(type);
+                mBuilderParcel.uid = Process.INVALID_UID;
                 mBuilderParcel.softApConfig = null;
             }
 
@@ -920,6 +922,47 @@
         }
 
         /**
+         * Sets the UID of the app that sent this request. This should always be overridden when
+         * receiving TetheringRequest from an external source.
+         * @hide
+         */
+        public void setUid(int uid) {
+            mRequestParcel.uid = uid;
+        }
+
+        /**
+         * Sets the package name of the app that sent this request. This should always be overridden
+         * when receiving a TetheringRequest from an external source.
+         * @hide
+         */
+        public void setPackageName(String packageName) {
+            mRequestParcel.packageName = packageName;
+        }
+
+        /**
+         * Gets the UID of the app that sent this request. This defaults to
+         * {@link Process#INVALID_UID} if unset.
+         * @hide
+         */
+        @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+        @SystemApi(client = MODULE_LIBRARIES)
+        public int getUid() {
+            return mRequestParcel.uid;
+        }
+
+        /**
+         * Gets the package name of the app that sent this request. This defaults to {@code null} if
+         * unset.
+         * @hide
+         */
+        @FlaggedApi(Flags.FLAG_TETHERING_REQUEST_WITH_SOFT_AP_CONFIG)
+        @SystemApi(client = MODULE_LIBRARIES)
+        @Nullable
+        public String getPackageName() {
+            return mRequestParcel.packageName;
+        }
+
+        /**
          * Get a TetheringRequestParcel from the configuration
          * @hide
          */
@@ -935,6 +978,8 @@
                     + ", exemptFromEntitlementCheck= " + mRequestParcel.exemptFromEntitlementCheck
                     + ", showProvisioningUi= " + mRequestParcel.showProvisioningUi
                     + ", softApConfig= " + mRequestParcel.softApConfig
+                    + ", uid= " + mRequestParcel.uid
+                    + ", packageName= " + mRequestParcel.packageName
                     + " ]";
         }
 
@@ -950,7 +995,9 @@
                     && parcel.exemptFromEntitlementCheck == otherParcel.exemptFromEntitlementCheck
                     && parcel.showProvisioningUi == otherParcel.showProvisioningUi
                     && parcel.connectivityScope == otherParcel.connectivityScope
-                    && Objects.equals(parcel.softApConfig, otherParcel.softApConfig);
+                    && Objects.equals(parcel.softApConfig, otherParcel.softApConfig)
+                    && parcel.uid == otherParcel.uid
+                    && Objects.equals(parcel.packageName, otherParcel.packageName);
         }
 
         @Override
@@ -958,7 +1005,8 @@
             TetheringRequestParcel parcel = getParcel();
             return Objects.hash(parcel.tetheringType, parcel.localIPv4Address,
                     parcel.staticClientAddress, parcel.exemptFromEntitlementCheck,
-                    parcel.showProvisioningUi, parcel.connectivityScope, parcel.softApConfig);
+                    parcel.showProvisioningUi, parcel.connectivityScope, parcel.softApConfig,
+                    parcel.uid, parcel.packageName);
         }
     }
 
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
index ea7a353..789d5bb 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl
@@ -31,4 +31,6 @@
     boolean showProvisioningUi;
     int connectivityScope;
     SoftApConfiguration softApConfig;
+    int uid;
+    String packageName;
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index b88b13b..cd57c8d 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -33,9 +33,12 @@
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
 
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
 import static com.android.networkstack.apishim.ConstantsShim.ACTION_TETHER_UNSUPPORTED_CARRIER_UI;
 import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_NOT_EXPORTED;
 
+import android.annotation.NonNull;
+import android.app.ActivityManager;
 import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
@@ -50,9 +53,13 @@
 import android.os.ResultReceiver;
 import android.os.SystemClock;
 import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.UserManager;
 import android.provider.Settings;
 import android.util.SparseIntArray;
 
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.SharedLog;
@@ -85,7 +92,6 @@
     // Indicate tethering is not supported by carrier.
     private static final int TETHERING_PROVISIONING_CARRIER_UNSUPPORT = 1002;
 
-    private final ComponentName mSilentProvisioningService;
     private static final int MS_PER_HOUR = 60 * 60 * 1000;
     private static final int DUMP_TIMEOUT = 10_000;
 
@@ -109,9 +115,115 @@
     private boolean mNeedReRunProvisioningUi = false;
     private OnTetherProvisioningFailedListener mListener;
     private TetheringConfigurationFetcher mFetcher;
+    private final Dependencies mDeps;
+
+    @VisibleForTesting(visibility = PRIVATE)
+    static class Dependencies {
+        @NonNull
+        private final Context mContext;
+        @NonNull
+        private final SharedLog mLog;
+        private final ComponentName mSilentProvisioningService;
+
+        Dependencies(@NonNull Context context, @NonNull SharedLog log) {
+            mContext = context;
+            mLog = log;
+            mSilentProvisioningService = ComponentName.unflattenFromString(
+                    mContext.getResources().getString(R.string.config_wifi_tether_enable));
+        }
+
+        /**
+         * Run the UI-enabled tethering provisioning check.
+         * @param type tethering type from TetheringManager.TETHERING_{@code *}
+         * @param receiver to receive entitlement check result.
+         *
+         * @return the broadcast intent, or null if the current user is not allowed to
+         *         perform entitlement check.
+         */
+        @Nullable
+        protected Intent runUiTetherProvisioning(int type, final TetheringConfiguration config,
+                ResultReceiver receiver) {
+            if (DBG) mLog.i("runUiTetherProvisioning: " + type);
+
+            Intent intent = new Intent(Settings.ACTION_TETHER_PROVISIONING_UI);
+            intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
+            intent.putExtra(EXTRA_TETHER_UI_PROVISIONING_APP_NAME, config.provisioningApp);
+            intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
+            intent.putExtra(EXTRA_TETHER_SUBID, config.activeDataSubId);
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+            // Only launch entitlement UI for the current user if it is allowed to
+            // change tethering. This usually means the system user or the admin users in HSUM.
+            if (SdkLevel.isAtLeastT()) {
+                // Create a user context for the current foreground user as UserManager#isAdmin()
+                // operates on the context user.
+                final int currentUserId = getCurrentUser();
+                final UserHandle currentUser = UserHandle.of(currentUserId);
+                final Context userContext = mContext.createContextAsUser(currentUser, 0);
+                final UserManager userManager = userContext.getSystemService(UserManager.class);
+
+                if (userManager.isAdminUser()) {
+                    mContext.startActivityAsUser(intent, currentUser);
+                } else {
+                    mLog.e("Current user (" + currentUserId
+                            + ") is not allowed to perform entitlement check.");
+                    return null;
+                }
+            } else {
+                // For T- devices, there is no other admin user other than the system user.
+                mContext.startActivity(intent);
+            }
+            return intent;
+        }
+
+        /**
+         * Run no UI tethering provisioning check.
+         * @param type tethering type from TetheringManager.TETHERING_{@code *}
+         */
+        protected Intent runSilentTetherProvisioning(
+                int type, final TetheringConfiguration config, ResultReceiver receiver) {
+            if (DBG) mLog.i("runSilentTetherProvisioning: " + type);
+
+            Intent intent = new Intent();
+            intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
+            intent.putExtra(EXTRA_RUN_PROVISION, true);
+            intent.putExtra(EXTRA_TETHER_SILENT_PROVISIONING_ACTION, config.provisioningAppNoUi);
+            intent.putExtra(EXTRA_TETHER_PROVISIONING_RESPONSE, config.provisioningResponse);
+            intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
+            intent.putExtra(EXTRA_TETHER_SUBID, config.activeDataSubId);
+            intent.setComponent(mSilentProvisioningService);
+            // Only admin user can change tethering and SilentTetherProvisioning don't need to
+            // show UI, it is fine to always start setting's background service as system user.
+            mContext.startService(intent);
+            return intent;
+        }
+
+        /**
+         * Create a PendingIntent for the provisioning recheck alarm.
+         * @param pkgName the package name of the PendingIntent.
+         */
+        PendingIntent createRecheckAlarmIntent(final String pkgName) {
+            final Intent intent = new Intent(ACTION_PROVISIONING_ALARM);
+            intent.setPackage(pkgName);
+            return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE);
+        }
+
+        /**
+         * Get the current user id.
+         */
+        int getCurrentUser() {
+            return ActivityManager.getCurrentUser();
+        }
+    }
 
     public EntitlementManager(Context ctx, Handler h, SharedLog log,
             Runnable callback) {
+        this(ctx, h, log, callback, new Dependencies(ctx, log));
+    }
+
+    @VisibleForTesting(visibility = PRIVATE)
+    EntitlementManager(Context ctx, Handler h, SharedLog log,
+            Runnable callback, @NonNull Dependencies deps) {
         mContext = ctx;
         mLog = log.forSubComponent(TAG);
         mCurrentDownstreams = new BitSet();
@@ -120,6 +232,7 @@
         mEntitlementCacheValue = new SparseIntArray();
         mPermissionChangeCallback = callback;
         mHandler = h;
+        mDeps = deps;
         if (SdkLevel.isAtLeastU()) {
             mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_PROVISIONING_ALARM),
                     null, mHandler, RECEIVER_NOT_EXPORTED);
@@ -127,8 +240,6 @@
             mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_PROVISIONING_ALARM),
                     null, mHandler);
         }
-        mSilentProvisioningService = ComponentName.unflattenFromString(
-                mContext.getResources().getString(R.string.config_wifi_tether_enable));
     }
 
     public void setOnTetherProvisioningFailedListener(
@@ -382,53 +493,6 @@
         }
     }
 
-    /**
-     * Run no UI tethering provisioning check.
-     * @param type tethering type from TetheringManager.TETHERING_{@code *}
-     * @param subId default data subscription ID.
-     */
-    @VisibleForTesting
-    protected Intent runSilentTetherProvisioning(
-            int type, final TetheringConfiguration config, ResultReceiver receiver) {
-        if (DBG) mLog.i("runSilentTetherProvisioning: " + type);
-
-        Intent intent = new Intent();
-        intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
-        intent.putExtra(EXTRA_RUN_PROVISION, true);
-        intent.putExtra(EXTRA_TETHER_SILENT_PROVISIONING_ACTION, config.provisioningAppNoUi);
-        intent.putExtra(EXTRA_TETHER_PROVISIONING_RESPONSE, config.provisioningResponse);
-        intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
-        intent.putExtra(EXTRA_TETHER_SUBID, config.activeDataSubId);
-        intent.setComponent(mSilentProvisioningService);
-        // Only admin user can change tethering and SilentTetherProvisioning don't need to
-        // show UI, it is fine to always start setting's background service as system user.
-        mContext.startService(intent);
-        return intent;
-    }
-
-    /**
-     * Run the UI-enabled tethering provisioning check.
-     * @param type tethering type from TetheringManager.TETHERING_{@code *}
-     * @param subId default data subscription ID.
-     * @param receiver to receive entitlement check result.
-     */
-    @VisibleForTesting
-    protected Intent runUiTetherProvisioning(int type, final TetheringConfiguration config,
-            ResultReceiver receiver) {
-        if (DBG) mLog.i("runUiTetherProvisioning: " + type);
-
-        Intent intent = new Intent(Settings.ACTION_TETHER_PROVISIONING_UI);
-        intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
-        intent.putExtra(EXTRA_TETHER_UI_PROVISIONING_APP_NAME, config.provisioningApp);
-        intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
-        intent.putExtra(EXTRA_TETHER_SUBID, config.activeDataSubId);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        // Only launch entitlement UI for system user. Entitlement UI should not appear for other
-        // user because only admin user is allowed to change tethering.
-        mContext.startActivity(intent);
-        return intent;
-    }
-
     private void runTetheringProvisioning(
             boolean showProvisioningUi, int downstreamType, final TetheringConfiguration config) {
         if (!config.isCarrierSupportTethering) {
@@ -442,9 +506,9 @@
         ResultReceiver receiver =
                 buildProxyReceiver(downstreamType, showProvisioningUi/* notifyFail */, null);
         if (showProvisioningUi) {
-            runUiTetherProvisioning(downstreamType, config, receiver);
+            mDeps.runUiTetherProvisioning(downstreamType, config, receiver);
         } else {
-            runSilentTetherProvisioning(downstreamType, config, receiver);
+            mDeps.runSilentTetherProvisioning(downstreamType, config, receiver);
         }
     }
 
@@ -458,20 +522,13 @@
         mContext.startActivity(intent);
     }
 
-    @VisibleForTesting
-    PendingIntent createRecheckAlarmIntent(final String pkgName) {
-        final Intent intent = new Intent(ACTION_PROVISIONING_ALARM);
-        intent.setPackage(pkgName);
-        return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE);
-    }
-
     // Not needed to check if this don't run on the handler thread because it's private.
     private void scheduleProvisioningRecheck(final TetheringConfiguration config) {
         if (mProvisioningRecheckAlarm == null) {
             final int period = config.provisioningCheckPeriod;
             if (period <= 0) return;
 
-            mProvisioningRecheckAlarm = createRecheckAlarmIntent(mContext.getPackageName());
+            mProvisioningRecheckAlarm = mDeps.createRecheckAlarmIntent(mContext.getPackageName());
             AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(
                     Context.ALARM_SERVICE);
             long triggerAtMillis = SystemClock.elapsedRealtime() + (period * MS_PER_HOUR);
@@ -697,7 +754,7 @@
             receiver.send(cacheValue, null);
         } else {
             ResultReceiver proxy = buildProxyReceiver(downstream, false/* notifyFail */, receiver);
-            runUiTetherProvisioning(downstream, config, proxy);
+            mDeps.runUiTetherProvisioning(downstream, config, proxy);
         }
     }
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index 454cbf1..cea7e82 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -55,6 +55,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.PermissionUtils;
 import com.android.networkstack.apishim.SettingsShimImpl;
 import com.android.networkstack.apishim.common.SettingsShim;
 
@@ -138,8 +139,10 @@
                     listener)) {
                 return;
             }
-            // TODO(b/216524590): Add UID/packageName of caller to TetheringRequest here
-            mTethering.startTethering(new TetheringRequest(request), callerPkg, listener);
+            TetheringRequest external = new TetheringRequest(request);
+            external.setUid(getBinderCallingUid());
+            external.setPackageName(callerPkg);
+            mTethering.startTethering(external, callerPkg, listener);
         }
 
         @Override
@@ -238,6 +241,12 @@
                 final String callingAttributionTag, final boolean onlyAllowPrivileged,
                 final IIntResultListener listener) {
             try {
+                if (!checkPackageNameMatchesUid(getBinderCallingUid(), callerPkg)) {
+                    Log.e(TAG, "Package name " + callerPkg + " does not match UID "
+                            + getBinderCallingUid());
+                    listener.onResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+                    return true;
+                }
                 if (!hasTetherChangePermission(callerPkg, callingAttributionTag,
                         onlyAllowPrivileged)) {
                     listener.onResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
@@ -256,6 +265,12 @@
 
         private boolean checkAndNotifyCommonError(final String callerPkg,
                 final String callingAttributionTag, final ResultReceiver receiver) {
+            if (!checkPackageNameMatchesUid(getBinderCallingUid(), callerPkg)) {
+                Log.e(TAG, "Package name " + callerPkg + " does not match UID "
+                        + getBinderCallingUid());
+                receiver.send(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION, null);
+                return true;
+            }
             if (!hasTetherChangePermission(callerPkg, callingAttributionTag,
                     false /* onlyAllowPrivileged */)) {
                 receiver.send(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION, null);
@@ -290,9 +305,9 @@
 
             if (mTethering.isTetherProvisioningRequired()) return false;
 
-            int uid = Binder.getCallingUid();
+            int uid = getBinderCallingUid();
 
-            // If callerPkg's uid is not same as Binder.getCallingUid(),
+            // If callerPkg's uid is not same as getBinderCallingUid(),
             // checkAndNoteWriteSettingsOperation will return false and the operation will be
             // denied.
             return mService.checkAndNoteWriteSettingsOperation(mService, uid, callerPkg,
@@ -305,6 +320,14 @@
             return mService.checkCallingOrSelfPermission(
                     ACCESS_NETWORK_STATE) == PERMISSION_GRANTED;
         }
+
+        private int getBinderCallingUid() {
+            return mService.getBinderCallingUid();
+        }
+
+        private boolean checkPackageNameMatchesUid(final int uid, final String callerPkg) {
+            return mService.checkPackageNameMatchesUid(mService, uid, callerPkg);
+        }
     }
 
     /**
@@ -322,6 +345,28 @@
     }
 
     /**
+     * Check if the package name matches the uid.
+     */
+    @VisibleForTesting
+    boolean checkPackageNameMatchesUid(@NonNull Context context, int uid,
+            @NonNull String callingPackage) {
+        try {
+            PermissionUtils.enforcePackageNameMatchesUid(context, uid, callingPackage);
+        } catch (SecurityException e) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Wrapper for the Binder calling UID, used for mocks.
+     */
+    @VisibleForTesting
+    int getBinderCallingUid() {
+        return Binder.getCallingUid();
+    }
+
+    /**
      * An injection method for testing.
      */
     @VisibleForTesting
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
index c2e1617..8626b18 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -38,6 +38,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
@@ -71,11 +72,13 @@
 import android.os.ResultReceiver;
 import android.os.SystemProperties;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.os.test.TestLooper;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.telephony.CarrierConfigManager;
 
+import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -114,6 +117,7 @@
     @Mock private EntitlementManager
             .OnTetherProvisioningFailedListener mTetherProvisioningFailedListener;
     @Mock private AlarmManager mAlarmManager;
+    @Mock private UserManager mUserManager;
     @Mock private PendingIntent mAlarmIntent;
 
     @Rule
@@ -126,9 +130,10 @@
     private MockContext mMockContext;
     private Runnable mPermissionChangeCallback;
 
-    private WrappedEntitlementManager mEnMgr;
+    private EntitlementManager mEnMgr;
     private TetheringConfiguration mConfig;
     private MockitoSession mMockingSession;
+    private TestDependencies mDeps;
 
     private class MockContext extends BroadcastInterceptingContext {
         MockContext(Context base) {
@@ -143,19 +148,30 @@
         @Override
         public Object getSystemService(String name) {
             if (Context.ALARM_SERVICE.equals(name)) return mAlarmManager;
+            if (Context.USER_SERVICE.equals(name)) return mUserManager;
 
             return super.getSystemService(name);
         }
+
+        @Override
+        public String getSystemServiceName(Class<?> serviceClass) {
+            if (UserManager.class.equals(serviceClass)) return Context.USER_SERVICE;
+            return super.getSystemServiceName(serviceClass);
+        }
+
+        @Override
+        public Context createContextAsUser(UserHandle user, int flags) {
+            return mMockContext; // Return self for easier test injection.
+        }
     }
 
-    public class WrappedEntitlementManager extends EntitlementManager {
+    class TestDependencies extends EntitlementManager.Dependencies {
         public int fakeEntitlementResult = TETHER_ERROR_ENTITLEMENT_UNKNOWN;
         public int uiProvisionCount = 0;
         public int silentProvisionCount = 0;
-
-        public WrappedEntitlementManager(Context ctx, Handler h, SharedLog log,
-                Runnable callback) {
-            super(ctx, h, log, callback);
+        TestDependencies(@NonNull Context context,
+                @NonNull SharedLog log) {
+            super(context, log);
         }
 
         public void reset() {
@@ -168,8 +184,10 @@
         protected Intent runUiTetherProvisioning(int type,
                 final TetheringConfiguration config, final ResultReceiver receiver) {
             Intent intent = super.runUiTetherProvisioning(type, config, receiver);
-            assertUiTetherProvisioningIntent(type, config, receiver, intent);
-            uiProvisionCount++;
+            if (intent != null) {
+                assertUiTetherProvisioningIntent(type, config, receiver, intent);
+                uiProvisionCount++;
+            }
             receiver.send(fakeEntitlementResult, null);
             return intent;
         }
@@ -195,7 +213,7 @@
             Intent intent = super.runSilentTetherProvisioning(type, config, receiver);
             assertSilentTetherProvisioning(type, config, intent);
             silentProvisionCount++;
-            addDownstreamMapping(type, fakeEntitlementResult);
+            mEnMgr.addDownstreamMapping(type, fakeEntitlementResult);
             return intent;
         }
 
@@ -217,6 +235,13 @@
             assertEquals(TEST_PACKAGE_NAME, pkgName);
             return mAlarmIntent;
         }
+
+        @Override
+        int getCurrentUser() {
+            // The result is not used, just override to bypass the need of accessing
+            // the static method.
+            return 0;
+        }
     }
 
     @Before
@@ -253,11 +278,13 @@
                 false);
         when(mResources.getString(R.string.config_wifi_tether_enable)).thenReturn("");
         when(mLog.forSubComponent(anyString())).thenReturn(mLog);
+        doReturn(true).when(mUserManager).isAdminUser();
 
         mMockContext = new MockContext(mContext);
+        mDeps = new TestDependencies(mMockContext, mLog);
         mPermissionChangeCallback = spy(() -> { });
-        mEnMgr = new WrappedEntitlementManager(mMockContext, new Handler(mLooper.getLooper()), mLog,
-                mPermissionChangeCallback);
+        mEnMgr = new EntitlementManager(mMockContext, new Handler(mLooper.getLooper()), mLog,
+                mPermissionChangeCallback, mDeps);
         mEnMgr.setOnTetherProvisioningFailedListener(mTetherProvisioningFailedListener);
         mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
         mEnMgr.setTetheringConfigurationFetcher(() -> {
@@ -320,7 +347,7 @@
     @Test
     public void testRequestLastEntitlementCacheValue() throws Exception {
         // 1. Entitlement check is not required.
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         ResultReceiver receiver = new ResultReceiver(null) {
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
@@ -329,8 +356,8 @@
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
         mLooper.dispatchAll();
-        assertEquals(0, mEnMgr.uiProvisionCount);
-        mEnMgr.reset();
+        assertEquals(0, mDeps.uiProvisionCount);
+        mDeps.reset();
 
         setupForRequiredProvisioning();
         // 2. No cache value and don't need to run entitlement check.
@@ -342,10 +369,10 @@
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
         mLooper.dispatchAll();
-        assertEquals(0, mEnMgr.uiProvisionCount);
-        mEnMgr.reset();
+        assertEquals(0, mDeps.uiProvisionCount);
+        mDeps.reset();
         // 3. No cache value and ui entitlement check is needed.
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
         receiver = new ResultReceiver(null) {
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
@@ -354,11 +381,11 @@
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
         mLooper.dispatchAll();
-        assertEquals(1, mEnMgr.uiProvisionCount);
-        mEnMgr.reset();
+        assertEquals(1, mDeps.uiProvisionCount);
+        mDeps.reset();
         // 4. Cache value is TETHER_ERROR_PROVISIONING_FAILED and don't need to run entitlement
         // check.
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         receiver = new ResultReceiver(null) {
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
@@ -367,10 +394,10 @@
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
         mLooper.dispatchAll();
-        assertEquals(0, mEnMgr.uiProvisionCount);
-        mEnMgr.reset();
+        assertEquals(0, mDeps.uiProvisionCount);
+        mDeps.reset();
         // 5. Cache value is TETHER_ERROR_PROVISIONING_FAILED and ui entitlement check is needed.
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         receiver = new ResultReceiver(null) {
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
@@ -379,10 +406,10 @@
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
         mLooper.dispatchAll();
-        assertEquals(1, mEnMgr.uiProvisionCount);
-        mEnMgr.reset();
+        assertEquals(1, mDeps.uiProvisionCount);
+        mDeps.reset();
         // 6. Cache value is TETHER_ERROR_NO_ERROR.
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         receiver = new ResultReceiver(null) {
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
@@ -391,8 +418,8 @@
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
         mLooper.dispatchAll();
-        assertEquals(0, mEnMgr.uiProvisionCount);
-        mEnMgr.reset();
+        assertEquals(0, mDeps.uiProvisionCount);
+        mDeps.reset();
         // 7. Test get value for other downstream type.
         receiver = new ResultReceiver(null) {
             @Override
@@ -402,10 +429,10 @@
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_USB, receiver, false);
         mLooper.dispatchAll();
-        assertEquals(0, mEnMgr.uiProvisionCount);
-        mEnMgr.reset();
+        assertEquals(0, mDeps.uiProvisionCount);
+        mDeps.reset();
         // 8. Test get value for invalid downstream type.
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         receiver = new ResultReceiver(null) {
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
@@ -414,8 +441,8 @@
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI_P2P, receiver, true);
         mLooper.dispatchAll();
-        assertEquals(0, mEnMgr.uiProvisionCount);
-        mEnMgr.reset();
+        assertEquals(0, mDeps.uiProvisionCount);
+        mDeps.reset();
     }
 
     private void assertPermissionChangeCallback(InOrder inOrder) {
@@ -431,7 +458,7 @@
         final InOrder inOrder = inOrder(mPermissionChangeCallback);
         setupForRequiredProvisioning();
         mEnMgr.notifyUpstream(true);
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
         mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
         mLooper.dispatchAll();
         // Permitted: true -> false
@@ -443,7 +470,7 @@
         // Permitted: false -> false
         assertNoPermissionChange(inOrder);
 
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
         mLooper.dispatchAll();
         // Permitted: false -> true
@@ -456,21 +483,21 @@
         final InOrder inOrder = inOrder(mPermissionChangeCallback);
         setupForRequiredProvisioning();
         mEnMgr.notifyUpstream(true);
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
         mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
         mLooper.dispatchAll();
         // Permitted: true -> false
         assertPermissionChangeCallback(inOrder);
         assertFalse(mEnMgr.isCellularUpstreamPermitted());
 
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
         mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
         mLooper.dispatchAll();
         // Permitted: false -> false
         assertNoPermissionChange(inOrder);
         assertFalse(mEnMgr.isCellularUpstreamPermitted());
 
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
         mEnMgr.startProvisioningIfNeeded(TETHERING_BLUETOOTH, true);
         mLooper.dispatchAll();
         // Permitted: false -> false
@@ -483,14 +510,14 @@
         final InOrder inOrder = inOrder(mPermissionChangeCallback);
         setupForRequiredProvisioning();
         mEnMgr.notifyUpstream(true);
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
         mLooper.dispatchAll();
         // Permitted: true -> true
         assertNoPermissionChange(inOrder);
         assertTrue(mEnMgr.isCellularUpstreamPermitted());
 
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
         mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
         mLooper.dispatchAll();
         // Permitted: true -> true
@@ -519,89 +546,89 @@
         final InOrder inOrder = inOrder(mPermissionChangeCallback);
         setupForRequiredProvisioning();
         // 1. start ui provisioning, upstream is mobile
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         mEnMgr.notifyUpstream(true);
         mLooper.dispatchAll();
         mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
         mLooper.dispatchAll();
-        assertEquals(1, mEnMgr.uiProvisionCount);
-        assertEquals(0, mEnMgr.silentProvisionCount);
+        assertEquals(1, mDeps.uiProvisionCount);
+        assertEquals(0, mDeps.silentProvisionCount);
         // Permitted: true -> true
         assertNoPermissionChange(inOrder);
         assertTrue(mEnMgr.isCellularUpstreamPermitted());
-        mEnMgr.reset();
+        mDeps.reset();
 
         // 2. start no-ui provisioning
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, false);
         mLooper.dispatchAll();
-        assertEquals(0, mEnMgr.uiProvisionCount);
-        assertEquals(1, mEnMgr.silentProvisionCount);
+        assertEquals(0, mDeps.uiProvisionCount);
+        assertEquals(1, mDeps.silentProvisionCount);
         // Permitted: true -> true
         assertNoPermissionChange(inOrder);
         assertTrue(mEnMgr.isCellularUpstreamPermitted());
-        mEnMgr.reset();
+        mDeps.reset();
 
         // 3. tear down mobile, then start ui provisioning
         mEnMgr.notifyUpstream(false);
         mLooper.dispatchAll();
         mEnMgr.startProvisioningIfNeeded(TETHERING_BLUETOOTH, true);
         mLooper.dispatchAll();
-        assertEquals(0, mEnMgr.uiProvisionCount);
-        assertEquals(0, mEnMgr.silentProvisionCount);
+        assertEquals(0, mDeps.uiProvisionCount);
+        assertEquals(0, mDeps.silentProvisionCount);
         assertNoPermissionChange(inOrder);
-        mEnMgr.reset();
+        mDeps.reset();
 
         // 4. switch upstream back to mobile
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         mEnMgr.notifyUpstream(true);
         mLooper.dispatchAll();
-        assertEquals(1, mEnMgr.uiProvisionCount);
-        assertEquals(0, mEnMgr.silentProvisionCount);
+        assertEquals(1, mDeps.uiProvisionCount);
+        assertEquals(0, mDeps.silentProvisionCount);
         // Permitted: true -> true
         assertNoPermissionChange(inOrder);
         assertTrue(mEnMgr.isCellularUpstreamPermitted());
-        mEnMgr.reset();
+        mDeps.reset();
 
         // 5. tear down mobile, then switch SIM
         mEnMgr.notifyUpstream(false);
         mLooper.dispatchAll();
         mEnMgr.reevaluateSimCardProvisioning(mConfig);
-        assertEquals(0, mEnMgr.uiProvisionCount);
-        assertEquals(0, mEnMgr.silentProvisionCount);
+        assertEquals(0, mDeps.uiProvisionCount);
+        assertEquals(0, mDeps.silentProvisionCount);
         assertNoPermissionChange(inOrder);
-        mEnMgr.reset();
+        mDeps.reset();
 
         // 6. switch upstream back to mobile again
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
         mEnMgr.notifyUpstream(true);
         mLooper.dispatchAll();
-        assertEquals(0, mEnMgr.uiProvisionCount);
-        assertEquals(3, mEnMgr.silentProvisionCount);
+        assertEquals(0, mDeps.uiProvisionCount);
+        assertEquals(3, mDeps.silentProvisionCount);
         // Permitted: true -> false
         assertPermissionChangeCallback(inOrder);
         assertFalse(mEnMgr.isCellularUpstreamPermitted());
-        mEnMgr.reset();
+        mDeps.reset();
 
         // 7. start ui provisioning, upstream is mobile, downstream is ethernet
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         mEnMgr.startProvisioningIfNeeded(TETHERING_ETHERNET, true);
         mLooper.dispatchAll();
-        assertEquals(1, mEnMgr.uiProvisionCount);
-        assertEquals(0, mEnMgr.silentProvisionCount);
+        assertEquals(1, mDeps.uiProvisionCount);
+        assertEquals(0, mDeps.silentProvisionCount);
         // Permitted: false -> true
         assertPermissionChangeCallback(inOrder);
         assertTrue(mEnMgr.isCellularUpstreamPermitted());
-        mEnMgr.reset();
+        mDeps.reset();
 
         // 8. downstream is invalid
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI_P2P, true);
         mLooper.dispatchAll();
-        assertEquals(0, mEnMgr.uiProvisionCount);
-        assertEquals(0, mEnMgr.silentProvisionCount);
+        assertEquals(0, mDeps.uiProvisionCount);
+        assertEquals(0, mDeps.silentProvisionCount);
         assertNoPermissionChange(inOrder);
-        mEnMgr.reset();
+        mDeps.reset();
     }
 
     @Test
@@ -609,16 +636,43 @@
         setupForRequiredProvisioning();
         verify(mTetherProvisioningFailedListener, times(0))
                 .onTetherProvisioningFailed(TETHERING_WIFI, FAILED_TETHERING_REASON);
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
         mEnMgr.notifyUpstream(true);
         mLooper.dispatchAll();
         mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
         mLooper.dispatchAll();
-        assertEquals(1, mEnMgr.uiProvisionCount);
+        assertEquals(1, mDeps.uiProvisionCount);
         verify(mTetherProvisioningFailedListener, times(1))
                 .onTetherProvisioningFailed(TETHERING_WIFI, FAILED_TETHERING_REASON);
     }
 
+    @IgnoreUpTo(SC_V2)
+    @Test
+    public void testUiProvisioningMultiUser_aboveT() {
+        doTestUiProvisioningMultiUser(true, 1);
+        doTestUiProvisioningMultiUser(false, 0);
+    }
+
+    @IgnoreAfter(SC_V2)
+    @Test
+    public void testUiProvisioningMultiUser_belowT() {
+        doTestUiProvisioningMultiUser(true, 1);
+        doTestUiProvisioningMultiUser(false, 1);
+    }
+
+    private void doTestUiProvisioningMultiUser(boolean isAdminUser, int expectedUiProvisionCount) {
+        setupForRequiredProvisioning();
+        doReturn(isAdminUser).when(mUserManager).isAdminUser();
+
+        mDeps.reset();
+        mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mEnMgr.notifyUpstream(true);
+        mLooper.dispatchAll();
+        mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
+        mLooper.dispatchAll();
+        assertEquals(expectedUiProvisionCount, mDeps.uiProvisionCount);
+    }
+
     @Test
     public void testsetExemptedDownstreamType() throws Exception {
         setupForRequiredProvisioning();
@@ -631,7 +685,7 @@
         assertTrue(mEnMgr.isCellularUpstreamPermitted());
 
         // If second downstream run entitlement check fail, cellular upstream is not permitted.
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
         mEnMgr.notifyUpstream(true);
         mLooper.dispatchAll();
         mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
@@ -639,7 +693,7 @@
         assertFalse(mEnMgr.isCellularUpstreamPermitted());
 
         // When second downstream is down, exempted downstream can use cellular upstream.
-        assertEquals(1, mEnMgr.uiProvisionCount);
+        assertEquals(1, mDeps.uiProvisionCount);
         verify(mTetherProvisioningFailedListener).onTetherProvisioningFailed(TETHERING_USB,
                 FAILED_TETHERING_REASON);
         mEnMgr.stopProvisioningIfNeeded(TETHERING_USB);
@@ -660,7 +714,7 @@
         setupForRequiredProvisioning();
         assertFalse(mEnMgr.isCellularUpstreamPermitted());
 
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         mEnMgr.notifyUpstream(true);
         mLooper.dispatchAll();
         mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
@@ -682,7 +736,7 @@
             throws Exception {
         setupCarrierConfig(false);
         setupForRequiredProvisioning();
-        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
         ResultReceiver receiver = new ResultReceiver(null) {
             @Override
             protected void onReceiveResult(int resultCode, Bundle resultData) {
@@ -691,8 +745,8 @@
         };
         mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
         mLooper.dispatchAll();
-        assertEquals(0, mEnMgr.uiProvisionCount);
-        mEnMgr.reset();
+        assertEquals(0, mDeps.uiProvisionCount);
+        mDeps.reset();
     }
 
     @Test
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
index 3c07580..7fcc5f1 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
@@ -32,6 +32,8 @@
 public class MockTetheringService extends TetheringService {
     private final Tethering mTethering = mock(Tethering.class);
     private final ArrayMap<String, Integer> mMockedPermissions = new ArrayMap<>();
+    private final ArrayMap<String, Integer> mMockedPackageUids = new ArrayMap<>();
+    private int mMockCallingUid;
 
     @Override
     public IBinder onBind(Intent intent) {
@@ -61,6 +63,17 @@
         return super.checkCallingOrSelfPermission(permission);
     }
 
+    @Override
+    boolean checkPackageNameMatchesUid(@NonNull Context context, int uid,
+            @NonNull String callingPackage) {
+        return mMockedPackageUids.getOrDefault(callingPackage, 0) == uid;
+    }
+
+    @Override
+    int getBinderCallingUid() {
+        return mMockCallingUid;
+    }
+
     public Tethering getTethering() {
         return mTethering;
     }
@@ -91,5 +104,19 @@
                 mMockedPermissions.put(permission, granted);
             }
         }
+
+        /**
+         * Mock a package name matching a uid.
+         */
+        public void setPackageNameUid(String packageName, int uid) {
+            mMockedPackageUids.put(packageName, uid);
+        }
+
+        /**
+         * Mock a package name matching a uid.
+         */
+        public void setCallingUid(int uid) {
+            mMockCallingUid = uid;
+        }
     }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
index c0d7ad4..1988311 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -34,6 +34,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -79,6 +80,7 @@
 public final class TetheringServiceTest {
     private static final String TEST_IFACE_NAME = "test_wlan0";
     private static final String TEST_CALLER_PKG = "com.android.shell";
+    private static final int TEST_CALLER_UID = 1234;
     private static final String TEST_ATTRIBUTION_TAG = null;
     @Mock private ITetheringEventCallback mITetheringEventCallback;
     @Rule public ServiceTestRule mServiceTestRule;
@@ -128,6 +130,8 @@
         mTetheringConnector = ITetheringConnector.Stub.asInterface(mMockConnector.getIBinder());
         final MockTetheringService service = mMockConnector.getService();
         mTethering = service.getTethering();
+        mMockConnector.setCallingUid(TEST_CALLER_UID);
+        mMockConnector.setPackageNameUid(TEST_CALLER_PKG, TEST_CALLER_UID);
     }
 
     @After
@@ -330,6 +334,16 @@
         });
 
         runAsTetherPrivileged((result) -> {
+            String wrongPackage = "wrong.package";
+            mTetheringConnector.startTethering(request, wrongPackage,
+                    TEST_ATTRIBUTION_TAG, result);
+            verify(mTethering, never()).startTethering(
+                    eq(new TetheringRequest(request)), eq(wrongPackage), eq(result));
+            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+            verifyNoMoreInteractionsForTethering();
+        });
+
+        runAsTetherPrivileged((result) -> {
             runStartTethering(result, request);
             verifyNoMoreInteractionsForTethering();
         });
@@ -445,6 +459,13 @@
             verifyNoMoreInteractionsForTethering();
         });
 
+        runAsTetherPrivileged((none) -> {
+            mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result,
+                    true /* showEntitlementUi */, "wrong.package", TEST_ATTRIBUTION_TAG);
+            result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
+            verifyNoMoreInteractions(mTethering);
+        });
+
         runAsWriteSettings((none) -> {
             runRequestLatestTetheringEntitlementResult();
             verify(mTethering).isTetherProvisioningRequired();
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 08129eb..5f8f0e3 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -500,7 +500,6 @@
 
   @FlaggedApi("com.android.net.thread.flags.configuration_enabled") public final class ThreadConfiguration implements android.os.Parcelable {
     method public int describeContents();
-    method public boolean isDhcpv6PdEnabled();
     method public boolean isNat64Enabled();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.ThreadConfiguration> CREATOR;
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index fd73b29..16f32c4 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -73,7 +73,9 @@
                 new CertificateTransparencyInstaller());
     }
 
-    void registerReceiver() {
+    void initialize() {
+        mInstaller.addCompatibilityVersion(Config.COMPATIBILITY_VERSION);
+
         IntentFilter intentFilter = new IntentFilter();
         intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
         mContext.registerReceiver(this, intentFilter, Context.RECEIVER_EXPORTED);
@@ -185,7 +187,7 @@
         String contentUrl = mDataStore.getProperty(Config.CONTENT_URL_PENDING);
         String metadataUrl = mDataStore.getProperty(Config.METADATA_URL_PENDING);
         try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
-            success = mInstaller.install(inputStream, version);
+            success = mInstaller.install(Config.COMPATIBILITY_VERSION, inputStream, version);
         } catch (IOException e) {
             Log.e(TAG, "Could not install new content", e);
             return;
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
index 914af06..0ae982d 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
@@ -43,7 +43,7 @@
 
     void initialize() {
         mDataStore.load();
-        mCertificateTransparencyDownloader.registerReceiver();
+        mCertificateTransparencyDownloader.initialize();
         DeviceConfig.addOnPropertiesChangedListener(
                 Config.NAMESPACE_NETWORK_SECURITY, Executors.newSingleThreadExecutor(), this);
         if (Config.DEBUG) {
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
index 82dcadf..4ca97eb 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
@@ -15,148 +15,78 @@
  */
 package com.android.server.net.ct;
 
-import android.annotation.SuppressLint;
-import android.system.ErrnoException;
-import android.system.Os;
 import android.util.Log;
 
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
-import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.Map;
 
 /** Installer of CT log lists. */
 public class CertificateTransparencyInstaller {
 
     private static final String TAG = "CertificateTransparencyInstaller";
-    private static final String CT_DIR_NAME = "/data/misc/keychain/ct/";
 
-    static final String LOGS_DIR_PREFIX = "logs-";
-    static final String LOGS_LIST_FILE_NAME = "log_list.json";
-    static final String CURRENT_DIR_SYMLINK_NAME = "current";
+    private final Map<String, CompatibilityVersion> mCompatVersions = new HashMap<>();
 
-    private final File mCertificateTransparencyDir;
-    private final File mCurrentDirSymlink;
+    // The CT root directory.
+    private final File mRootDirectory;
 
-    CertificateTransparencyInstaller(File certificateTransparencyDir) {
-        mCertificateTransparencyDir = certificateTransparencyDir;
-        mCurrentDirSymlink = new File(certificateTransparencyDir, CURRENT_DIR_SYMLINK_NAME);
+    public CertificateTransparencyInstaller(File rootDirectory) {
+        mRootDirectory = rootDirectory;
     }
 
-    CertificateTransparencyInstaller() {
-        this(new File(CT_DIR_NAME));
+    public CertificateTransparencyInstaller(String rootDirectoryPath) {
+        this(new File(rootDirectoryPath));
+    }
+
+    public CertificateTransparencyInstaller() {
+        this(Config.CT_ROOT_DIRECTORY_PATH);
+    }
+
+    void addCompatibilityVersion(String versionName) {
+        removeCompatibilityVersion(versionName);
+        CompatibilityVersion newCompatVersion =
+                new CompatibilityVersion(new File(mRootDirectory, versionName));
+        mCompatVersions.put(versionName, newCompatVersion);
+    }
+
+    void removeCompatibilityVersion(String versionName) {
+        CompatibilityVersion compatVersion = mCompatVersions.remove(versionName);
+        if (compatVersion != null && !compatVersion.delete()) {
+            Log.w(TAG, "Could not delete compatibility version directory.");
+        }
+    }
+
+    CompatibilityVersion getCompatibilityVersion(String versionName) {
+        return mCompatVersions.get(versionName);
     }
 
     /**
      * Install a new log list to use during SCT verification.
      *
+     * @param compatibilityVersion the compatibility version of the new log list
      * @param newContent an input stream providing the log list
-     * @param version the version of the new log list
+     * @param version the minor version of the new log list
      * @return true if the log list was installed successfully, false otherwise.
      * @throws IOException if the list cannot be saved in the CT directory.
      */
-    public boolean install(InputStream newContent, String version) throws IOException {
-        // To support atomically replacing the old configuration directory with the new there's a
-        // bunch of steps. We create a new directory with the logs and then do an atomic update of
-        // the current symlink to point to the new directory.
-        // 1. Ensure that the update dir exists and is readable.
-        makeDir(mCertificateTransparencyDir);
-
-        File newLogsDir = new File(mCertificateTransparencyDir, LOGS_DIR_PREFIX + version);
-        // 2. Handle the corner case where the new directory already exists.
-        if (newLogsDir.exists()) {
-            // If the symlink has already been updated then the update died between steps 6 and 7
-            // and so we cannot delete the directory since it is in use.
-            if (newLogsDir.getCanonicalPath().equals(mCurrentDirSymlink.getCanonicalPath())) {
-                deleteOldLogDirectories();
-                return false;
-            }
-            // If the symlink has not been updated then the previous installation failed and this is
-            // a re-attempt. Clean-up leftover files and try again.
-            deleteContentsAndDir(newLogsDir);
-        }
-        try {
-            // 3. Create /data/misc/keychain/ct/logs-<new_version>/ .
-            makeDir(newLogsDir);
-
-            // 4. Move the log list json file in logs-<new_version>/ .
-            File logListFile = new File(newLogsDir, LOGS_LIST_FILE_NAME);
-            if (Files.copy(newContent, logListFile.toPath()) == 0) {
-                throw new IOException("The log list appears empty");
-            }
-            setWorldReadable(logListFile);
-
-            // 5. Create temp symlink. We rename this to the target symlink to get an atomic update.
-            File tempSymlink = new File(mCertificateTransparencyDir, "new_symlink");
-            try {
-                Os.symlink(newLogsDir.getCanonicalPath(), tempSymlink.getCanonicalPath());
-            } catch (ErrnoException e) {
-                throw new IOException("Failed to create symlink", e);
-            }
-
-            // 6. Update the symlink target, this is the actual update step.
-            tempSymlink.renameTo(mCurrentDirSymlink.getAbsoluteFile());
-        } catch (IOException | RuntimeException e) {
-            deleteContentsAndDir(newLogsDir);
-            throw e;
-        }
-        Log.i(TAG, "CT log directory updated to " + newLogsDir.getAbsolutePath());
-        // 7. Cleanup
-        deleteOldLogDirectories();
-        return true;
-    }
-
-    private void makeDir(File dir) throws IOException {
-        dir.mkdir();
-        if (!dir.isDirectory()) {
-            throw new IOException("Unable to make directory " + dir.getCanonicalPath());
-        }
-        setWorldReadable(dir);
-    }
-
-    // CT files and directories are readable by all apps.
-    @SuppressLint("SetWorldReadable")
-    private void setWorldReadable(File file) throws IOException {
-        if (!file.setReadable(true, false)) {
-            throw new IOException("Failed to set " + file.getCanonicalPath() + " readable");
-        }
-    }
-
-    private void deleteOldLogDirectories() throws IOException {
-        if (!mCertificateTransparencyDir.exists()) {
-            return;
-        }
-        File currentTarget = mCurrentDirSymlink.getCanonicalFile();
-        for (File file : mCertificateTransparencyDir.listFiles()) {
-            if (!currentTarget.equals(file.getCanonicalFile())
-                    && file.getName().startsWith(LOGS_DIR_PREFIX)) {
-                deleteContentsAndDir(file);
-            }
-        }
-    }
-
-    static boolean deleteContentsAndDir(File dir) {
-        if (deleteContents(dir)) {
-            return dir.delete();
-        } else {
+    public boolean install(String compatibilityVersion, InputStream newContent, String version)
+            throws IOException {
+        CompatibilityVersion compatVersion = mCompatVersions.get(compatibilityVersion);
+        if (compatVersion == null) {
+            Log.e(TAG, "No compatibility version for " + compatibilityVersion);
             return false;
         }
-    }
+        // Ensure root directory exists and is readable.
+        DirectoryUtils.makeDir(mRootDirectory);
 
-    private static boolean deleteContents(File dir) {
-        File[] files = dir.listFiles();
-        boolean success = true;
-        if (files != null) {
-            for (File file : files) {
-                if (file.isDirectory()) {
-                    success &= deleteContents(file);
-                }
-                if (!file.delete()) {
-                    Log.w(TAG, "Failed to delete " + file);
-                    success = false;
-                }
-            }
+        if (!compatVersion.install(newContent, version)) {
+            Log.e(TAG, "Failed to install logs for compatibility version " + compatibilityVersion);
+            return false;
         }
-        return success;
+        Log.i(TAG, "New logs installed at " + compatVersion.getLogsDir());
+        return true;
     }
 }
diff --git a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
new file mode 100644
index 0000000..27488b5
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net.ct;
+
+import android.system.ErrnoException;
+import android.system.Os;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+
+/** Represents a compatibility version directory. */
+class CompatibilityVersion {
+
+    static final String LOGS_DIR_PREFIX = "logs-";
+    static final String LOGS_LIST_FILE_NAME = "log_list.json";
+
+    private static final String CURRENT_LOGS_DIR_SYMLINK_NAME = "current";
+
+    private final File mRootDirectory;
+    private final File mCurrentLogsDirSymlink;
+
+    private File mCurrentLogsDir = null;
+
+    CompatibilityVersion(File rootDirectory) {
+        mRootDirectory = rootDirectory;
+        mCurrentLogsDirSymlink = new File(mRootDirectory, CURRENT_LOGS_DIR_SYMLINK_NAME);
+    }
+
+    /**
+     * Installs a log list within this compatibility version directory.
+     *
+     * @param newContent an input stream providing the log list
+     * @param version the version number of the log list
+     * @return true if the log list was installed successfully, false otherwise.
+     * @throws IOException if the list cannot be saved in the CT directory.
+     */
+    boolean install(InputStream newContent, String version) throws IOException {
+        // To support atomically replacing the old configuration directory with the new there's a
+        // bunch of steps. We create a new directory with the logs and then do an atomic update of
+        // the current symlink to point to the new directory.
+        // 1. Ensure that the root directory exists and is readable.
+        DirectoryUtils.makeDir(mRootDirectory);
+
+        File newLogsDir = new File(mRootDirectory, LOGS_DIR_PREFIX + version);
+        // 2. Handle the corner case where the new directory already exists.
+        if (newLogsDir.exists()) {
+            // If the symlink has already been updated then the update died between steps 6 and 7
+            // and so we cannot delete the directory since it is in use.
+            if (newLogsDir.getCanonicalPath().equals(mCurrentLogsDirSymlink.getCanonicalPath())) {
+                deleteOldLogDirectories();
+                return false;
+            }
+            // If the symlink has not been updated then the previous installation failed and this is
+            // a re-attempt. Clean-up leftover files and try again.
+            DirectoryUtils.removeDir(newLogsDir);
+        }
+        try {
+            // 3. Create a new logs-<new_version>/ directory to store the new list.
+            DirectoryUtils.makeDir(newLogsDir);
+
+            // 4. Move the log list json file in logs-<new_version>/ .
+            File logListFile = new File(newLogsDir, LOGS_LIST_FILE_NAME);
+            if (Files.copy(newContent, logListFile.toPath()) == 0) {
+                throw new IOException("The log list appears empty");
+            }
+            DirectoryUtils.setWorldReadable(logListFile);
+
+            // 5. Create temp symlink. We rename this to the target symlink to get an atomic update.
+            File tempSymlink = new File(mRootDirectory, "new_symlink");
+            try {
+                Os.symlink(newLogsDir.getCanonicalPath(), tempSymlink.getCanonicalPath());
+            } catch (ErrnoException e) {
+                throw new IOException("Failed to create symlink", e);
+            }
+
+            // 6. Update the symlink target, this is the actual update step.
+            tempSymlink.renameTo(mCurrentLogsDirSymlink.getAbsoluteFile());
+        } catch (IOException | RuntimeException e) {
+            DirectoryUtils.removeDir(newLogsDir);
+            throw e;
+        }
+        // 7. Cleanup
+        mCurrentLogsDir = newLogsDir;
+        deleteOldLogDirectories();
+        return true;
+    }
+
+    File getRootDir() {
+        return mRootDirectory;
+    }
+
+    File getLogsDir() {
+        return mCurrentLogsDir;
+    }
+
+    File getLogsDirSymlink() {
+        return mCurrentLogsDirSymlink;
+    }
+
+    File getLogsFile() {
+        return new File(mCurrentLogsDir, LOGS_LIST_FILE_NAME);
+    }
+
+    boolean delete() {
+        return DirectoryUtils.removeDir(mRootDirectory);
+    }
+
+    private void deleteOldLogDirectories() throws IOException {
+        if (!mRootDirectory.exists()) {
+            return;
+        }
+        File currentTarget = mCurrentLogsDirSymlink.getCanonicalFile();
+        for (File file : mRootDirectory.listFiles()) {
+            if (!currentTarget.equals(file.getCanonicalFile())
+                    && file.getName().startsWith(LOGS_DIR_PREFIX)) {
+                DirectoryUtils.removeDir(file);
+            }
+        }
+    }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
index 611a5c7..242f13a 100644
--- a/networksecurity/service/src/com/android/server/net/ct/Config.java
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -33,6 +33,10 @@
     private static final String PREFERENCES_FILE_NAME = "ct.preferences";
     static final File PREFERENCES_FILE = new File(DEVICE_PROTECTED_DATA_DIR, PREFERENCES_FILE_NAME);
 
+    // CT directory
+    static final String CT_ROOT_DIRECTORY_PATH = "/data/misc/keychain/ct/";
+    static final String COMPATIBILITY_VERSION = "v1";
+
     // Phenotype flags
     static final String NAMESPACE_NETWORK_SECURITY = "network_security";
     private static final String FLAGS_PREFIX = "CertificateTransparencyLogList__";
diff --git a/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java b/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java
new file mode 100644
index 0000000..e3b4124
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.net.ct;
+
+import android.annotation.SuppressLint;
+
+import java.io.File;
+import java.io.IOException;
+
+/** Utility class to manipulate CT directories. */
+class DirectoryUtils {
+
+    static void makeDir(File dir) throws IOException {
+        dir.mkdir();
+        if (!dir.isDirectory()) {
+            throw new IOException("Unable to make directory " + dir.getCanonicalPath());
+        }
+        setWorldReadable(dir);
+    }
+
+    // CT files and directories are readable by all apps.
+    @SuppressLint("SetWorldReadable")
+    static void setWorldReadable(File file) throws IOException {
+        if (!file.setReadable(true, false)) {
+            throw new IOException("Failed to set " + file.getCanonicalPath() + " readable");
+        }
+    }
+
+    static boolean removeDir(File dir) {
+        return deleteContentsAndDir(dir);
+    }
+
+    private static boolean deleteContentsAndDir(File dir) {
+        if (deleteContents(dir)) {
+            return dir.delete();
+        } else {
+            return false;
+        }
+    }
+
+    private static boolean deleteContents(File dir) {
+        File[] files = dir.listFiles();
+        boolean success = true;
+        if (files != null) {
+            for (File file : files) {
+                if (file.isDirectory()) {
+                    success &= deleteContents(file);
+                }
+                if (!file.delete()) {
+                    success = false;
+                }
+            }
+        }
+        return success;
+    }
+}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index 1aad028..df02446 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -159,7 +159,9 @@
                 Base64.getEncoder().encodeToString(mPublicKey.getEncoded()));
 
         setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
-        when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(true);
+        when(mCertificateTransparencyInstaller.install(
+                        eq(Config.COMPATIBILITY_VERSION), any(), eq(version)))
+                .thenReturn(true);
 
         assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
         assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
@@ -168,7 +170,8 @@
         mCertificateTransparencyDownloader.onReceive(
                 mContext, makeDownloadCompleteIntent(contentId));
 
-        verify(mCertificateTransparencyInstaller, times(1)).install(any(), eq(version));
+        verify(mCertificateTransparencyInstaller, times(1))
+                .install(eq(Config.COMPATIBILITY_VERSION), any(), eq(version));
         assertThat(mDataStore.getProperty(Config.VERSION)).isEqualTo(version);
         assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isEqualTo(contentUri.toString());
         assertThat(mDataStore.getProperty(Config.METADATA_URL)).isEqualTo(metadataUri.toString());
@@ -185,7 +188,9 @@
         Uri metadataUri = Uri.fromFile(metadataFile);
 
         setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
-        when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(false);
+        when(mCertificateTransparencyInstaller.install(
+                        eq(Config.COMPATIBILITY_VERSION), any(), eq(version)))
+                .thenReturn(false);
 
         mCertificateTransparencyDownloader.onReceive(
                 mContext, makeDownloadCompleteIntent(contentId));
@@ -208,7 +213,8 @@
         mCertificateTransparencyDownloader.onReceive(
                 mContext, makeDownloadCompleteIntent(contentId));
 
-        verify(mCertificateTransparencyInstaller, never()).install(any(), eq(version));
+        verify(mCertificateTransparencyInstaller, never())
+                .install(eq(Config.COMPATIBILITY_VERSION), any(), eq(version));
         assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
         assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
         assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
@@ -230,7 +236,8 @@
         mCertificateTransparencyDownloader.onReceive(
                 mContext, makeDownloadCompleteIntent(contentId));
 
-        verify(mCertificateTransparencyInstaller, never()).install(any(), eq(version));
+        verify(mCertificateTransparencyInstaller, never())
+                .install(eq(Config.COMPATIBILITY_VERSION), any(), eq(version));
         assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
         assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
         assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
index bfb8bdf..50d3f23 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
@@ -17,11 +17,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.system.ErrnoException;
-import android.system.Os;
-
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -39,98 +37,134 @@
 @RunWith(JUnit4.class)
 public class CertificateTransparencyInstallerTest {
 
+    private static final String TEST_VERSION = "test-v1";
+
     private File mTestDir =
             new File(
                     InstrumentationRegistry.getInstrumentation().getContext().getFilesDir(),
                     "test-dir");
-    private File mTestSymlink =
-            new File(mTestDir, CertificateTransparencyInstaller.CURRENT_DIR_SYMLINK_NAME);
     private CertificateTransparencyInstaller mCertificateTransparencyInstaller =
             new CertificateTransparencyInstaller(mTestDir);
 
     @Before
     public void setUp() {
-        CertificateTransparencyInstaller.deleteContentsAndDir(mTestDir);
+        mCertificateTransparencyInstaller.addCompatibilityVersion(TEST_VERSION);
+    }
+
+    @After
+    public void tearDown() {
+        mCertificateTransparencyInstaller.removeCompatibilityVersion(TEST_VERSION);
+        DirectoryUtils.removeDir(mTestDir);
+    }
+
+    @Test
+    public void testCompatibilityVersion_installSuccessful() throws IOException {
+        assertThat(mTestDir.mkdir()).isTrue();
+        String content = "i_am_compatible";
+        String version = "i_am_version";
+        CompatibilityVersion compatVersion =
+                mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
+
+        try (InputStream inputStream = asStream(content)) {
+            assertThat(compatVersion.install(inputStream, version)).isTrue();
+        }
+        File logsDir = compatVersion.getLogsDir();
+        assertThat(logsDir.exists()).isTrue();
+        assertThat(logsDir.isDirectory()).isTrue();
+        assertThat(logsDir.getAbsolutePath())
+                .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION);
+        File logsListFile = compatVersion.getLogsFile();
+        assertThat(logsListFile.exists()).isTrue();
+        assertThat(logsListFile.getAbsolutePath()).startsWith(logsDir.getAbsolutePath());
+        assertThat(readAsString(logsListFile)).isEqualTo(content);
+        File logsSymlink = compatVersion.getLogsDirSymlink();
+        assertThat(logsSymlink.exists()).isTrue();
+        assertThat(logsSymlink.isDirectory()).isTrue();
+        assertThat(logsSymlink.getAbsolutePath())
+                .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION + "/current");
+        assertThat(logsSymlink.getCanonicalPath()).isEqualTo(logsDir.getCanonicalPath());
+
+        assertThat(compatVersion.delete()).isTrue();
+        assertThat(logsDir.exists()).isFalse();
+        assertThat(logsSymlink.exists()).isFalse();
+        assertThat(logsListFile.exists()).isFalse();
+    }
+
+    @Test
+    public void testCompatibilityVersion_versionInstalledFailed() throws IOException {
+        assertThat(mTestDir.mkdir()).isTrue();
+
+        CompatibilityVersion compatVersion =
+                mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
+        File rootDir = compatVersion.getRootDir();
+        assertThat(rootDir.mkdir()).isTrue();
+
+        String existingVersion = "666";
+        File existingLogDir =
+                new File(rootDir, CompatibilityVersion.LOGS_DIR_PREFIX + existingVersion);
+        assertThat(existingLogDir.mkdir()).isTrue();
+
+        String existingContent = "somebody_tried_to_install_me_but_failed_halfway_through";
+        File logsListFile = new File(existingLogDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
+        assertThat(logsListFile.createNewFile()).isTrue();
+        writeToFile(logsListFile, existingContent);
+
+        String newContent = "i_am_the_real_content";
+        try (InputStream inputStream = asStream(newContent)) {
+            assertThat(compatVersion.install(inputStream, existingVersion)).isTrue();
+        }
+
+        assertThat(readAsString(logsListFile)).isEqualTo(newContent);
     }
 
     @Test
     public void testCertificateTransparencyInstaller_installSuccessfully() throws IOException {
         String content = "i_am_a_certificate_and_i_am_transparent";
         String version = "666";
-        boolean success = false;
 
         try (InputStream inputStream = asStream(content)) {
-            success = mCertificateTransparencyInstaller.install(inputStream, version);
+            assertThat(
+                            mCertificateTransparencyInstaller.install(
+                                    TEST_VERSION, inputStream, version))
+                    .isTrue();
         }
 
-        assertThat(success).isTrue();
         assertThat(mTestDir.exists()).isTrue();
         assertThat(mTestDir.isDirectory()).isTrue();
-        assertThat(mTestSymlink.exists()).isTrue();
-        assertThat(mTestSymlink.isDirectory()).isTrue();
-
-        File logsDir =
-                new File(mTestDir, CertificateTransparencyInstaller.LOGS_DIR_PREFIX + version);
+        CompatibilityVersion compatVersion =
+                mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
+        File logsDir = compatVersion.getLogsDir();
         assertThat(logsDir.exists()).isTrue();
         assertThat(logsDir.isDirectory()).isTrue();
-        assertThat(mTestSymlink.getCanonicalPath()).isEqualTo(logsDir.getCanonicalPath());
-
-        File logsListFile = new File(logsDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME);
+        assertThat(logsDir.getAbsolutePath())
+                .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION);
+        File logsListFile = compatVersion.getLogsFile();
         assertThat(logsListFile.exists()).isTrue();
+        assertThat(logsListFile.getAbsolutePath()).startsWith(logsDir.getAbsolutePath());
         assertThat(readAsString(logsListFile)).isEqualTo(content);
     }
 
     @Test
     public void testCertificateTransparencyInstaller_versionIsAlreadyInstalled()
-            throws IOException, ErrnoException {
+            throws IOException {
         String existingVersion = "666";
         String existingContent = "i_was_already_installed_successfully";
-        File existingLogDir =
-                new File(
-                        mTestDir,
-                        CertificateTransparencyInstaller.LOGS_DIR_PREFIX + existingVersion);
-        assertThat(mTestDir.mkdir()).isTrue();
-        assertThat(existingLogDir.mkdir()).isTrue();
-        Os.symlink(existingLogDir.getCanonicalPath(), mTestSymlink.getCanonicalPath());
-        File logsListFile =
-                new File(existingLogDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME);
-        logsListFile.createNewFile();
-        writeToFile(logsListFile, existingContent);
-        boolean success = false;
+        CompatibilityVersion compatVersion =
+                mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
+
+        DirectoryUtils.makeDir(mTestDir);
+        try (InputStream inputStream = asStream(existingContent)) {
+            assertThat(compatVersion.install(inputStream, existingVersion)).isTrue();
+        }
 
         try (InputStream inputStream = asStream("i_will_be_ignored")) {
-            success = mCertificateTransparencyInstaller.install(inputStream, existingVersion);
+            assertThat(
+                            mCertificateTransparencyInstaller.install(
+                                    TEST_VERSION, inputStream, existingVersion))
+                    .isFalse();
         }
 
-        assertThat(success).isFalse();
-        assertThat(readAsString(logsListFile)).isEqualTo(existingContent);
-    }
-
-    @Test
-    public void testCertificateTransparencyInstaller_versionInstalledFailed()
-            throws IOException, ErrnoException {
-        String existingVersion = "666";
-        String existingContent = "somebody_tried_to_install_me_but_failed_halfway_through";
-        String newContent = "i_am_the_real_certificate";
-        File existingLogDir =
-                new File(
-                        mTestDir,
-                        CertificateTransparencyInstaller.LOGS_DIR_PREFIX + existingVersion);
-        assertThat(mTestDir.mkdir()).isTrue();
-        assertThat(existingLogDir.mkdir()).isTrue();
-        File logsListFile =
-                new File(existingLogDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME);
-        logsListFile.createNewFile();
-        writeToFile(logsListFile, existingContent);
-        boolean success = false;
-
-        try (InputStream inputStream = asStream(newContent)) {
-            success = mCertificateTransparencyInstaller.install(inputStream, existingVersion);
-        }
-
-        assertThat(success).isTrue();
-        assertThat(mTestSymlink.getCanonicalPath()).isEqualTo(existingLogDir.getCanonicalPath());
-        assertThat(readAsString(logsListFile)).isEqualTo(newContent);
+        assertThat(readAsString(compatVersion.getLogsFile())).isEqualTo(existingContent);
     }
 
     private static InputStream asStream(String string) throws IOException {
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 4f01599..a43486e 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -59,6 +59,7 @@
 public class MdnsServiceTypeClient {
 
     private static final String TAG = MdnsServiceTypeClient.class.getSimpleName();
+    private static final boolean DBG = MdnsDiscoveryManager.DBG;
     @VisibleForTesting
     static final int EVENT_START_QUERYTASK = 1;
     static final int EVENT_QUERY_RESULT = 2;
@@ -184,10 +185,14 @@
                                     searchOptions.numOfQueriesBeforeBackoff(),
                                     false /* forceEnableBackoff */
                             );
+                    final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
+                    sharedLog.log(String.format("Query sent with transactionId: %d. "
+                                    + "Next run: sessionId: %d, in %d ms",
+                            sentResult.transactionId, args.sessionId, timeToNextTaskMs));
                     dependencies.sendMessageDelayed(
                             handler,
                             handler.obtainMessage(EVENT_START_QUERYTASK, args),
-                            calculateTimeToNextTask(args, now, sharedLog));
+                            timeToNextTaskMs);
                     break;
                 }
                 default:
@@ -369,10 +374,13 @@
                             searchOptions.numOfQueriesBeforeBackoff(),
                             forceEnableBackoff
                     );
+            final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
+            sharedLog.log(String.format("Schedule a query. Next run: sessionId: %d, in %d ms",
+                    args.sessionId, timeToNextTaskMs));
             dependencies.sendMessageDelayed(
                     handler,
                     handler.obtainMessage(EVENT_START_QUERYTASK, args),
-                    calculateTimeToNextTask(args, now, sharedLog));
+                    timeToNextTaskMs);
         } else {
             final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
             final QueryTask queryTask = new QueryTask(
@@ -492,6 +500,10 @@
                 // If the response is not modified and already in the cache. The cache will
                 // need to be updated to refresh the last receipt time.
                 serviceCache.addOrUpdateService(cacheKey, response);
+                if (DBG) {
+                    sharedLog.v("Update the last receipt time for service:"
+                            + serviceInstanceName);
+                }
             }
         }
         if (dependencies.hasMessages(handler, EVENT_START_QUERYTASK)) {
@@ -503,10 +515,13 @@
                             searchOptions.numOfQueriesBeforeBackoff());
             if (args != null) {
                 removeScheduledTask();
+                final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
+                sharedLog.log(String.format("Reschedule a query. Next run: sessionId: %d, in %d ms",
+                        args.sessionId, timeToNextTaskMs));
                 dependencies.sendMessageDelayed(
                         handler,
                         handler.obtainMessage(EVENT_START_QUERYTASK, args),
-                        calculateTimeToNextTask(args, now, sharedLog));
+                        timeToNextTaskMs);
             }
         }
     }
@@ -757,11 +772,8 @@
     }
 
     private static long calculateTimeToNextTask(MdnsQueryScheduler.ScheduledQueryTaskArgs args,
-            long now, SharedLog sharedLog) {
-        long timeToNextTasksWithBackoffInMs = Math.max(args.timeToRun - now, 0);
-        sharedLog.log(String.format("Next run: sessionId: %d, in %d ms",
-                args.sessionId, timeToNextTasksWithBackoffInMs));
-        return timeToNextTasksWithBackoffInMs;
+            long now) {
+        return Math.max(args.timeToRun - now, 0);
     }
 
     /**
diff --git a/service/Android.bp b/service/Android.bp
index e6caf9d..567c079 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -311,7 +311,7 @@
     apex_available: ["com.android.tethering"],
 }
 
-genrule {
+java_genrule {
     name: "connectivity-jarjar-rules",
     defaults: ["jarjar-rules-combine-defaults"],
     srcs: [
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 665e6f9..e503312 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -6003,12 +6003,10 @@
             // TODO : The only way out of this is to diff old defaults and new defaults, and only
             // remove ranges for those requests that won't have a replacement
             final NetworkAgentInfo satisfier = nri.getSatisfier();
-            if (null != satisfier && !satisfier.isDestroyed()) {
+            if (null != satisfier) {
                 try {
-                    mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig(
-                            satisfier.network.getNetId(),
-                            toUidRangeStableParcels(nri.getUids()),
-                            nri.getPreferenceOrderForNetd()));
+                    modifyNetworkUidRanges(false /* add */, satisfier, nri.getUids(),
+                            nri.getPreferenceOrderForNetd());
                 } catch (RemoteException e) {
                     loge("Exception setting network preference default network", e);
                 }
@@ -10267,8 +10265,7 @@
         return stableRanges;
     }
 
-    private void maybeCloseSockets(NetworkAgentInfo nai, Set<UidRange> ranges,
-            UidRangeParcel[] uidRangeParcels, int[] exemptUids) {
+    private void maybeCloseSockets(NetworkAgentInfo nai, Set<UidRange> ranges, int[] exemptUids) {
         if (nai.isVPN() && !nai.networkAgentConfig.allowBypass) {
             try {
                 if (mDeps.isAtLeastU()) {
@@ -10278,7 +10275,7 @@
                     }
                     mDeps.destroyLiveTcpSockets(UidRange.toIntRanges(ranges), exemptUidSet);
                 } else {
-                    mNetd.socketDestroy(uidRangeParcels, exemptUids);
+                    mNetd.socketDestroy(toUidRangeStableParcels(ranges), exemptUids);
                 }
             } catch (Exception e) {
                 loge("Exception in socket destroy: ", e);
@@ -10286,6 +10283,28 @@
         }
     }
 
+    private void modifyNetworkUidRanges(boolean add, NetworkAgentInfo nai, UidRangeParcel[] ranges,
+            int preference) throws RemoteException {
+        // UID ranges can be added or removed to a network that has already been destroyed (e.g., if
+        // the network disconnects, or a a multilayer request is filed after
+        // unregisterAfterReplacement is called).
+        if (nai.isDestroyed()) {
+            return;
+        }
+        final NativeUidRangeConfig config = new NativeUidRangeConfig(nai.network.netId,
+                ranges, preference);
+        if (add) {
+            mNetd.networkAddUidRangesParcel(config);
+        } else {
+            mNetd.networkRemoveUidRangesParcel(config);
+        }
+    }
+
+    private void modifyNetworkUidRanges(boolean add, NetworkAgentInfo nai, Set<UidRange> uidRanges,
+            int preference) throws RemoteException {
+        modifyNetworkUidRanges(add, nai, toUidRangeStableParcels(uidRanges), preference);
+    }
+
     private void updateVpnUidRanges(boolean add, NetworkAgentInfo nai, Set<UidRange> uidRanges) {
         int[] exemptUids = new int[2];
         // TODO: Excluding VPN_UID is necessary in order to not to kill the TCP connection used
@@ -10293,24 +10312,17 @@
         // starting a legacy VPN, and remove VPN_UID here. (b/176542831)
         exemptUids[0] = VPN_UID;
         exemptUids[1] = nai.networkCapabilities.getOwnerUid();
-        UidRangeParcel[] ranges = toUidRangeStableParcels(uidRanges);
 
         // Close sockets before modifying uid ranges so that RST packets can reach to the server.
-        maybeCloseSockets(nai, uidRanges, ranges, exemptUids);
+        maybeCloseSockets(nai, uidRanges, exemptUids);
         try {
-            if (add) {
-                mNetd.networkAddUidRangesParcel(new NativeUidRangeConfig(
-                        nai.network.netId, ranges, PREFERENCE_ORDER_VPN));
-            } else {
-                mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig(
-                        nai.network.netId, ranges, PREFERENCE_ORDER_VPN));
-            }
+            modifyNetworkUidRanges(add, nai, uidRanges, PREFERENCE_ORDER_VPN);
         } catch (Exception e) {
             loge("Exception while " + (add ? "adding" : "removing") + " uid ranges " + uidRanges +
                     " on netId " + nai.network.netId + ". " + e);
         }
         // Close sockets that established connection while requesting netd.
-        maybeCloseSockets(nai, uidRanges, ranges, exemptUids);
+        maybeCloseSockets(nai, uidRanges, exemptUids);
     }
 
     private boolean isProxySetOnAnyDefaultNetwork() {
@@ -10424,16 +10436,12 @@
         toAdd.removeAll(prevUids);
         try {
             if (!toAdd.isEmpty()) {
-                mNetd.networkAddUidRangesParcel(new NativeUidRangeConfig(
-                        nai.network.netId,
-                        intsToUidRangeStableParcels(toAdd),
-                        PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT));
+                modifyNetworkUidRanges(true /* add */, nai, intsToUidRangeStableParcels(toAdd),
+                        PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT);
             }
             if (!toRemove.isEmpty()) {
-                mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig(
-                        nai.network.netId,
-                        intsToUidRangeStableParcels(toRemove),
-                        PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT));
+                modifyNetworkUidRanges(false /* add */, nai, intsToUidRangeStableParcels(toRemove),
+                        PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT);
             }
         } catch (ServiceSpecificException e) {
             // Has the interface disappeared since the network was built ?
@@ -10788,16 +10796,12 @@
                         + " any applications to set as the default." + nri);
             }
             if (null != newDefaultNetwork) {
-                mNetd.networkAddUidRangesParcel(new NativeUidRangeConfig(
-                        newDefaultNetwork.network.getNetId(),
-                        toUidRangeStableParcels(nri.getUids()),
-                        nri.getPreferenceOrderForNetd()));
+                modifyNetworkUidRanges(true /* add */, newDefaultNetwork, nri.getUids(),
+                        nri.getPreferenceOrderForNetd());
             }
             if (null != oldDefaultNetwork) {
-                mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig(
-                        oldDefaultNetwork.network.getNetId(),
-                        toUidRangeStableParcels(nri.getUids()),
-                        nri.getPreferenceOrderForNetd()));
+                modifyNetworkUidRanges(false /* add */, oldDefaultNetwork, nri.getUids(),
+                        nri.getPreferenceOrderForNetd());
             }
         } catch (RemoteException | ServiceSpecificException e) {
             loge("Exception setting app default network", e);
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 66e1dad..a825b87 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -626,6 +626,31 @@
     visibility: ["//visibility:private"],
 }
 
+// Filegroup to build lib used by IPsec/IKE framework
+// Any class here *must* have a corresponding jarjar rule in the IPsec build rules.
+filegroup {
+    name: "net-utils-framework-ipsec-common-srcs",
+    srcs: [
+        "framework/com/android/net/module/util/HexDump.java",
+    ],
+    path: "framework",
+    visibility: ["//visibility:private"],
+}
+
+java_library {
+    name: "net-utils-framework-ipsec",
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    srcs: [":net-utils-framework-ipsec-common-srcs"],
+    libs: [
+        "androidx.annotation_annotation",
+    ],
+    visibility: [
+        "//packages/modules/IPsec",
+    ],
+    apex_available: ["com.android.ipsec"],
+}
+
 // Use a file group containing classes necessary for framework-connectivity. The file group should
 // be as small as possible because because the classes end up in the bootclasspath and R8 is not
 // used to remove unused classes.
diff --git a/staticlibs/tests/unit/host/python/assert_utils_test.py b/staticlibs/tests/unit/host/python/assert_utils_test.py
index 7a33373..1d85a12 100644
--- a/staticlibs/tests/unit/host/python/assert_utils_test.py
+++ b/staticlibs/tests/unit/host/python/assert_utils_test.py
@@ -14,7 +14,9 @@
 
 from mobly import asserts
 from mobly import base_test
-from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError, expect_with_retry
+from net_tests_utils.host.python.assert_utils import (
+    UnexpectedBehaviorError, UnexpectedExceptionError, expect_with_retry, expect_throws
+)
 
 
 class TestAssertUtils(base_test.BaseTestClass):
@@ -92,3 +94,22 @@
           retry_interval_sec=0,
       )
     asserts.assert_true(retry_action_called, "retry_action not called.")
+
+  def test_expect_exception_throws(self):
+      def raise_unexpected_behavior_error():
+          raise UnexpectedBehaviorError()
+
+      expect_throws(raise_unexpected_behavior_error, UnexpectedBehaviorError)
+
+  def test_unexpect_exception_throws(self):
+      def raise_value_error():
+          raise ValueError()
+
+      with asserts.assert_raises(UnexpectedExceptionError):
+          expect_throws(raise_value_error, UnexpectedBehaviorError)
+
+  def test_no_exception_throws(self):
+      def raise_no_error():
+          return
+
+      expect_throws(raise_no_error, UnexpectedBehaviorError)
\ No newline at end of file
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index 13e1dc0..f4ed9e4 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -103,7 +103,7 @@
         "mcts-wifi",
         "mcts-dnsresolver",
     ],
-    data: [":ConnectivityTestPreparer"],
+    device_common_data: [":ConnectivityTestPreparer"],
 }
 
 python_library_host {
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
index 7fe60bd..55ac860 100644
--- a/staticlibs/testutils/host/python/apf_utils.py
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -182,22 +182,23 @@
         ad: android_device.AndroidDevice,
 ) -> bool:
 
-  # Invoke the shell command with empty argument and see how NetworkStack respond.
-  # If supported, an IllegalArgumentException with help page will be printed.
-  functions_with_args = (
-    # list all functions and args with (func, *args) tuple
-    (start_capture_packets, (ad, "")),
-    (stop_capture_packets, (ad, "")),
-    (get_matched_packet_counts, (ad, "", ""))
-  )
-
-  for func, args in functions_with_args:
-    try:
-      func(*args)
-    except UnsupportedOperationException:
-      return False
-    except Exception:
-      continue
+  try:
+    # Invoke the shell command with empty argument and see how NetworkStack respond.
+    # If supported, an IllegalArgumentException with help page will be printed.
+    assert_utils.expect_throws(
+      lambda: start_capture_packets(ad, ""),
+      assert_utils.UnexpectedBehaviorError
+    )
+    assert_utils.expect_throws(
+      lambda: stop_capture_packets(ad, ""),
+      assert_utils.UnexpectedBehaviorError
+    )
+    assert_utils.expect_throws(
+      lambda: get_matched_packet_counts(ad, "", ""),
+      assert_utils.UnexpectedBehaviorError
+    )
+  except assert_utils.UnexpectedExceptionError:
+    return False
 
   # If no UnsupportOperationException is thrown, regard it as supported
   return True
diff --git a/staticlibs/testutils/host/python/assert_utils.py b/staticlibs/testutils/host/python/assert_utils.py
index da1bb9e..40094a2 100644
--- a/staticlibs/testutils/host/python/assert_utils.py
+++ b/staticlibs/testutils/host/python/assert_utils.py
@@ -19,6 +19,8 @@
 class UnexpectedBehaviorError(Exception):
   """Raised when there is an unexpected behavior during applying a procedure."""
 
+class UnexpectedExceptionError(Exception):
+  """Raised when there is an unexpected exception throws during applying a procedure"""
 
 def expect_with_retry(
     predicate: Callable[[], bool],
@@ -41,3 +43,17 @@
   raise UnexpectedBehaviorError(
       "Predicate didn't become true after " + str(max_retries) + " retries."
   )
+
+def expect_throws(runnable: callable, exception_class) -> None:
+  try:
+    runnable()
+    raise UnexpectedBehaviorError("Expected an exception, but none was thrown")
+  except exception_class:
+    pass
+  except UnexpectedBehaviorError as e:
+    raise e
+  except Exception as e:
+      raise UnexpectedExceptionError(
+        f"Expected exception of type {exception_class.__name__}, "
+        f"but got {type(e).__name__}: {e}"
+      )
\ No newline at end of file
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt b/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
index 1883387..d1d5649 100644
--- a/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
@@ -20,11 +20,13 @@
 
 import com.android.testutils.FunctionalUtils.ThrowingRunnable
 import java.lang.reflect.Modifier
+import java.util.function.BooleanSupplier
 import kotlin.system.measureTimeMillis
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
+import kotlin.test.fail
 
 private const val TAG = "Connectivity unit test"
 
@@ -118,4 +120,25 @@
     val actualSet: HashSet<T> = HashSet(actual)
     assertEquals(actualSet.size, actual.size, "actual list contains duplicates")
     assertEquals(expectedSet, actualSet)
+}
+
+@JvmOverloads
+fun assertEventuallyTrue(
+    descr: String,
+    timeoutMs: Long,
+    pollIntervalMs: Long = 10L,
+    fn: BooleanSupplier
+) {
+    // This should use SystemClock.elapsedRealtime() since nanoTime does not include time in deep
+    // sleep, but this is a host-device library and SystemClock is Android-specific (not available
+    // on host). When waiting for a condition during tests the device would generally not go into
+    // deep sleep, and the polling sleep would go over the timeout anyway in that case, so this is
+    // fine.
+    val limit = System.nanoTime() + timeoutMs * 1000
+    while (!fn.asBoolean) {
+        if (System.nanoTime() > limit) {
+            fail(descr)
+        }
+        Thread.sleep(pollIntervalMs)
+    }
 }
\ No newline at end of file
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 920492f..bb1009b 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -61,7 +61,7 @@
 // Combine Connectivity, NetworkStack and Tethering jarjar rules for coverage target.
 // The jarjar files are simply concatenated in the order specified in srcs.
 // jarjar stops at the first matching rule, so order of concatenation affects the output.
-genrule {
+java_genrule {
     name: "ConnectivityCoverageJarJarRules",
     defaults: ["jarjar-rules-combine-defaults"],
     srcs: [
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index 97be91a..0ac9ce1 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -56,7 +56,7 @@
         "mts-tethering",
         "sts",
     ],
-    data: [
+    device_common_data: [
         ":CtsHostsideNetworkTestsApp",
         ":CtsHostsideNetworkTestsApp2",
         ":CtsHostsideNetworkCapTestsAppWithoutProperty",
diff --git a/tests/cts/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
index 40aa1e4..949be85 100644
--- a/tests/cts/multidevices/Android.bp
+++ b/tests/cts/multidevices/Android.bp
@@ -37,7 +37,7 @@
     test_options: {
         unit_test: false,
     },
-    data: [
+    device_common_data: [
         // Package the snippet with the mobly test
         ":connectivity_multi_devices_snippet",
     ],
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index a65316f..7590a2b 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -60,7 +60,8 @@
     </test>
     <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
         <!-- Pattern matching the fileKey used by ConnectivityDiagnosticsCollector when calling addFileMetric -->
-        <option name="pull-pattern-keys" value="com.android.testutils.ConnectivityDiagnosticsCollector.*"/>
+        <option name="pull-pattern-keys" value="com.android.testutils.ConnectivityDiagnosticsCollector.*" />
+        <option name="log-data-type" value="CONNDIAG" />
         <option name="collect-on-run-ended-only" value="true" />
     </metrics_collector>
     <!-- When this test is run in a Mainline context (e.g. with `mts-tradefed`), only enable it if
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 0e9ea0c..3a8252a 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -113,6 +113,7 @@
 import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_EXPORTED;
 import static com.android.testutils.Cleanup.testAndCleanup;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+import static com.android.testutils.MiscAsserts.assertEventuallyTrue;
 import static com.android.testutils.MiscAsserts.assertThrows;
 import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
 import static com.android.testutils.TestPermissionUtil.runAsShell;
@@ -2934,12 +2935,7 @@
                 mCm.getActiveNetwork(), false /* accept */ , false /* always */));
     }
 
-    private void ensureCellIsValidatedBeforeMockingValidationUrls() {
-        // Verify that current supported network is validated so that the mock http server will not
-        // apply to unexpected networks. Also see aosp/2208680.
-        //
-        // This may also apply to wifi in principle, but in practice methods that mock validation
-        // URL all disconnect wifi forcefully anyway, so don't wait for wifi to validate.
+    private void ensureCellIsValidated() {
         if (mPackageManager.hasSystemFeature(FEATURE_TELEPHONY)) {
             new ConnectUtil(mContext).ensureCellularValidated();
         }
@@ -3022,9 +3018,13 @@
             networkCallbackRule.requestCell();
 
             final Network wifiNetwork = prepareUnvalidatedNetwork();
-            // Default network should not be wifi ,but checking that wifi is not the default doesn't
-            // guarantee that it won't become the default in the future.
-            assertNotEquals(wifiNetwork, mCm.getActiveNetwork());
+            // Default network should not be wifi ,but checking that Wi-Fi is not the default
+            // doesn't guarantee that it won't become the default in the future.
+            // On U 24Q2+ telephony may teardown (unregisterAfterReplacement) its network when Wi-Fi
+            // is toggled (as part of prepareUnvalidatedNetwork here). Give some time for Wi-Fi to
+            // not be default in case telephony is reconnecting.
+            assertEventuallyTrue("Wifi remained default despite being unvalidated",
+                    WIFI_CONNECT_TIMEOUT_MS, () -> !wifiNetwork.equals(mCm.getActiveNetwork()));
 
             final TestableNetworkCallback wifiCb = networkCallbackRule.registerNetworkCallback(
                     makeWifiNetworkRequest());
@@ -3061,7 +3061,7 @@
 
         try {
             final Network cellNetwork = networkCallbackRule.requestCell();
-            ensureCellIsValidatedBeforeMockingValidationUrls();
+            ensureCellIsValidated();
             final Network wifiNetwork = prepareValidatedNetwork();
 
             final TestableNetworkCallback defaultCb =
@@ -3157,7 +3157,12 @@
     }
 
     private Network prepareValidatedNetwork() throws Exception {
-        ensureCellIsValidatedBeforeMockingValidationUrls();
+        // Verify that current supported network is validated so that the mock http server will not
+        // apply to unexpected networks. Also see aosp/2208680.
+        //
+        // This may also apply to wifi in principle, but in practice methods that mock validation
+        // URL all disconnect wifi forcefully anyway, so don't wait for wifi to validate.
+        ensureCellIsValidated();
 
         prepareHttpServer();
         configTestServer(Status.NO_CONTENT, Status.NO_CONTENT);
@@ -3169,7 +3174,7 @@
     }
 
     private Network preparePartialConnectivity() throws Exception {
-        ensureCellIsValidatedBeforeMockingValidationUrls();
+        ensureCellIsValidated();
 
         prepareHttpServer();
         // Configure response code for partial connectivity
@@ -3184,7 +3189,7 @@
     }
 
     private Network prepareUnvalidatedNetwork() throws Exception {
-        ensureCellIsValidatedBeforeMockingValidationUrls();
+        ensureCellIsValidated();
 
         prepareHttpServer();
         // Configure response code for unvalidated network
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index 83818be..d9bc7f7 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -19,7 +19,10 @@
 
 java_defaults {
     name: "CtsTetheringTestDefaults",
-    defaults: ["cts_defaults"],
+    defaults: [
+        "cts_defaults",
+        "framework-connectivity-test-defaults",
+    ],
 
     libs: [
         "android.test.base.stubs.system",
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index 1454d9a..a07c9ea 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -32,6 +32,7 @@
 import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
 import static android.net.cts.util.CtsTetheringUtils.isAnyIfaceMatch;
+import static android.os.Process.INVALID_UID;
 
 import static com.android.testutils.TestPermissionUtil.runAsShell;
 
@@ -244,24 +245,35 @@
         assertFalse(tr.isExemptFromEntitlementCheck());
         assertTrue(tr.getShouldShowEntitlementUi());
         assertEquals(softApConfiguration, tr.getSoftApConfiguration());
+        assertEquals(INVALID_UID, tr.getUid());
+        assertNull(tr.getPackageName());
 
         final LinkAddress localAddr = new LinkAddress("192.168.24.5/24");
         final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24");
         final TetheringRequest tr2 = new TetheringRequest.Builder(TETHERING_USB)
                 .setStaticIpv4Addresses(localAddr, clientAddr)
                 .setExemptFromEntitlementCheck(true)
-                .setShouldShowEntitlementUi(false).build();
+                .setShouldShowEntitlementUi(false)
+                .build();
+        int uid = 1000;
+        String packageName = "package";
+        tr2.setUid(uid);
+        tr2.setPackageName(packageName);
 
         assertEquals(localAddr, tr2.getLocalIpv4Address());
         assertEquals(clientAddr, tr2.getClientStaticIpv4Address());
         assertEquals(TETHERING_USB, tr2.getTetheringType());
         assertTrue(tr2.isExemptFromEntitlementCheck());
         assertFalse(tr2.getShouldShowEntitlementUi());
+        assertEquals(uid, tr2.getUid());
+        assertEquals(packageName, tr2.getPackageName());
 
         final TetheringRequest tr3 = new TetheringRequest.Builder(TETHERING_USB)
                 .setStaticIpv4Addresses(localAddr, clientAddr)
                 .setExemptFromEntitlementCheck(true)
                 .setShouldShowEntitlementUi(false).build();
+        tr3.setUid(uid);
+        tr3.setPackageName(packageName);
         assertEquals(tr2, tr3);
     }
 
diff --git a/tests/deflake/Android.bp b/tests/deflake/Android.bp
index 726e504..70a3655 100644
--- a/tests/deflake/Android.bp
+++ b/tests/deflake/Android.bp
@@ -40,7 +40,7 @@
         "kotlin-test",
         "net-host-tests-utils",
     ],
-    data: [":FrameworksNetTests"],
+    device_common_data: [":FrameworksNetTests"],
     test_suites: ["device-tests"],
     // It will get build error if just set enabled to true. It fails with "windows_common"
     // depends on some disabled modules that are used by this test and it looks like set
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 6892a42..9edf9bd 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -114,7 +114,7 @@
     visibility: ["//packages/modules/Connectivity/tests:__subpackages__"],
 }
 
-genrule {
+java_genrule {
     name: "frameworks-net-tests-jarjar-rules",
     defaults: ["jarjar-rules-combine-defaults"],
     srcs: [
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
index 5ca7fcc..58420c0 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
@@ -163,19 +163,36 @@
         doTestSatelliteNeverBecomeDefaultNetwork(restricted = false)
     }
 
-    private fun doTestUnregisterAfterReplacementSatisfier(destroyed: Boolean) {
+    private fun doTestUnregisterAfterReplacementSatisfier(destroyBeforeRequest: Boolean = false,
+                                                          destroyAfterRequest: Boolean = false) {
         val satelliteAgent = createSatelliteAgent("satellite0")
         satelliteAgent.connect()
 
+        if (destroyBeforeRequest) {
+            satelliteAgent.unregisterAfterReplacement(timeoutMs = 5000)
+        }
+
         val uids = setOf(TEST_PACKAGE_UID)
         updateSatelliteNetworkFallbackUids(uids)
 
-        if (destroyed) {
+        if (destroyBeforeRequest) {
+            verify(netd, never()).networkAddUidRangesParcel(any())
+        } else {
+            verify(netd).networkAddUidRangesParcel(
+                NativeUidRangeConfig(
+                    satelliteAgent.network.netId,
+                    toUidRangeStableParcels(uidRangesForUids(uids)),
+                    PREFERENCE_ORDER_SATELLITE_FALLBACK
+                )
+            )
+        }
+
+        if (destroyAfterRequest) {
             satelliteAgent.unregisterAfterReplacement(timeoutMs = 5000)
         }
 
         updateSatelliteNetworkFallbackUids(setOf())
-        if (destroyed) {
+        if (destroyBeforeRequest || destroyAfterRequest) {
             // If the network is already destroyed, networkRemoveUidRangesParcel should not be
             // called.
             verify(netd, never()).networkRemoveUidRangesParcel(any())
@@ -191,13 +208,18 @@
     }
 
     @Test
-    fun testUnregisterAfterReplacementSatisfier_destroyed() {
-        doTestUnregisterAfterReplacementSatisfier(destroyed = true)
+    fun testUnregisterAfterReplacementSatisfier_destroyBeforeRequest() {
+        doTestUnregisterAfterReplacementSatisfier(destroyBeforeRequest = true)
+    }
+
+    @Test
+    fun testUnregisterAfterReplacementSatisfier_destroyAfterRequest() {
+        doTestUnregisterAfterReplacementSatisfier(destroyAfterRequest = true)
     }
 
     @Test
     fun testUnregisterAfterReplacementSatisfier_notDestroyed() {
-        doTestUnregisterAfterReplacementSatisfier(destroyed = false)
+        doTestUnregisterAfterReplacementSatisfier()
     }
 
     private fun assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(uids: Set<Int>) {
diff --git a/thread/demoapp/res/layout/thread_network_settings_fragment.xml b/thread/demoapp/res/layout/thread_network_settings_fragment.xml
index 84d984b..ac89a2f 100644
--- a/thread/demoapp/res/layout/thread_network_settings_fragment.xml
+++ b/thread/demoapp/res/layout/thread_network_settings_fragment.xml
@@ -19,11 +19,10 @@
   android:layout_width="match_parent"
   android:layout_height="match_parent">
     <LinearLayout
-        xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:app="http://schemas.android.com/apk/res-auto"
         xmlns:tools="http://schemas.android.com/tools"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
         android:padding="8dp"
         android:orientation="vertical"
         tools:context=".ThreadNetworkSettingsFragment" >
@@ -40,28 +39,28 @@
         <TextView
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:textSize="16dp"
+            android:textSize="16sp"
             android:textStyle="bold"
             android:text="State" />
         <TextView
             android:id="@+id/text_state"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:textSize="12dp"
+            android:textSize="12sp"
             android:typeface="monospace" />
 
         <TextView
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginTop="10dp"
-            android:textSize="16dp"
+            android:textSize="16sp"
             android:textStyle="bold"
             android:text="Network Info" />
         <TextView
             android:id="@+id/text_network_info"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:textSize="12dp" />
+            android:textSize="12sp" />
 
         <Button android:id="@+id/button_migrate_network"
             android:layout_width="wrap_content"
@@ -71,7 +70,7 @@
             android:id="@+id/text_migrate_network_state"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:textSize="12dp" />
+            android:textSize="12sp" />
 
         <Button android:id="@+id/button_activate_ephemeral_key_mode"
             android:layout_width="wrap_content"
@@ -86,7 +85,7 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginTop="10dp"
-            android:textSize="16dp"
+            android:textSize="16sp"
             android:textStyle="bold"
             android:text="Ephemeral Key State" />
         <TextView
@@ -94,6 +93,6 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginBottom="50dp"
-            android:textSize="12dp" />
+            android:textSize="12sp" />
     </LinearLayout>
 </ScrollView>
diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.java b/thread/framework/java/android/net/thread/ThreadConfiguration.java
index e6fa1ef..edb5021 100644
--- a/thread/framework/java/android/net/thread/ThreadConfiguration.java
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.java
@@ -61,7 +61,11 @@
         return mNat64Enabled;
     }
 
-    /** Returns {@code true} if DHCPv6 Prefix Delegation is enabled. */
+    /**
+     * Returns {@code true} if DHCPv6 Prefix Delegation is enabled.
+     *
+     * @hide
+     */
     public boolean isDhcpv6PdEnabled() {
         return mDhcpv6PdEnabled;
     }
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 653b2fb..d5d24ac 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -78,6 +78,8 @@
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
 import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.LocalNetworkConfig;
 import android.net.LocalNetworkInfo;
@@ -120,6 +122,8 @@
 
 import com.android.connectivity.resources.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.RoutingCoordinatorManager;
+import com.android.net.module.util.IIpv4PrefixRequest;
 import com.android.net.module.util.SharedLog;
 import com.android.server.ServiceManagerWrapper;
 import com.android.server.connectivity.ConnectivityResources;
@@ -193,10 +197,12 @@
     private final NetworkProvider mNetworkProvider;
     private final Supplier<IOtDaemon> mOtDaemonSupplier;
     private final ConnectivityManager mConnectivityManager;
+    private final RoutingCoordinatorManager mRoutingCoordinatorManager;
     private final TunInterfaceController mTunIfController;
     private final InfraInterfaceController mInfraIfController;
     private final NsdPublisher mNsdPublisher;
     private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
+    private final Nat64CidrController mNat64CidrController = new Nat64CidrController();
     private final ConnectivityResources mResources;
     private final Supplier<String> mCountryCodeSupplier;
     private final Map<IConfigurationReceiver, IBinder.DeathRecipient> mConfigurationReceivers =
@@ -229,6 +235,7 @@
             NetworkProvider networkProvider,
             Supplier<IOtDaemon> otDaemonSupplier,
             ConnectivityManager connectivityManager,
+            RoutingCoordinatorManager routingCoordinatorManager,
             TunInterfaceController tunIfController,
             InfraInterfaceController infraIfController,
             ThreadPersistentSettings persistentSettings,
@@ -242,6 +249,7 @@
         mNetworkProvider = networkProvider;
         mOtDaemonSupplier = otDaemonSupplier;
         mConnectivityManager = connectivityManager;
+        mRoutingCoordinatorManager = routingCoordinatorManager;
         mTunIfController = tunIfController;
         mInfraIfController = infraIfController;
         mUpstreamNetworkRequest = newUpstreamNetworkRequest();
@@ -266,13 +274,19 @@
         NetworkProvider networkProvider =
                 new NetworkProvider(context, handlerThread.getLooper(), "ThreadNetworkProvider");
         Map<Network, LinkProperties> networkToLinkProperties = new HashMap<>();
+        final ConnectivityManager connectivityManager =
+                context.getSystemService(ConnectivityManager.class);
+        final RoutingCoordinatorManager routingCoordinatorManager =
+                new RoutingCoordinatorManager(
+                        context, connectivityManager.getRoutingCoordinatorService());
 
         return new ThreadNetworkControllerService(
                 context,
                 handler,
                 networkProvider,
                 () -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
-                context.getSystemService(ConnectivityManager.class),
+                connectivityManager,
+                routingCoordinatorManager,
                 new TunInterfaceController(TUN_IF_NAME),
                 new InfraInterfaceController(),
                 persistentSettings,
@@ -351,6 +365,7 @@
                 mCountryCodeSupplier.get());
         otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
         mOtDaemon = otDaemon;
+        mHandler.post(mNat64CidrController::maybeUpdateNat64Cidr);
         return mOtDaemon;
     }
 
@@ -589,6 +604,7 @@
         } catch (RemoteException | ThreadNetworkException e) {
             LOG.e("otDaemon.setConfiguration failed. Config: " + configuration, e);
         }
+        mNat64CidrController.maybeUpdateNat64Cidr();
     }
 
     private static OtDaemonConfiguration newOtDaemonConfig(
@@ -833,7 +849,7 @@
                 mHandler.getLooper(),
                 LOG.getTag(),
                 netCaps,
-                mTunIfController.getLinkProperties(),
+                getTunIfLinkProperties(),
                 newLocalNetworkConfig(),
                 score,
                 new NetworkAgentConfig.Builder().build(),
@@ -1391,9 +1407,7 @@
 
         // The OT daemon can send link property updates before the networkAgent is
         // registered
-        if (mNetworkAgent != null) {
-            mNetworkAgent.sendLinkProperties(mTunIfController.getLinkProperties());
-        }
+        maybeSendLinkProperties();
     }
 
     private void handlePrefixChanged(List<OnMeshPrefixConfig> onMeshPrefixConfigList) {
@@ -1403,9 +1417,18 @@
 
         // The OT daemon can send link property updates before the networkAgent is
         // registered
-        if (mNetworkAgent != null) {
-            mNetworkAgent.sendLinkProperties(mTunIfController.getLinkProperties());
+        maybeSendLinkProperties();
+    }
+
+    private void maybeSendLinkProperties() {
+        if (mNetworkAgent == null) {
+            return;
         }
+        mNetworkAgent.sendLinkProperties(getTunIfLinkProperties());
+    }
+
+    private LinkProperties getTunIfLinkProperties() {
+        return mTunIfController.getLinkPropertiesWithNat64Cidr(mNat64CidrController.mNat64Cidr);
     }
 
     @RequiresPermission(
@@ -1851,4 +1874,64 @@
             mHandler.post(() -> handlePrefixChanged(onMeshPrefixConfigList));
         }
     }
+
+    private final class Nat64CidrController extends IIpv4PrefixRequest.Stub {
+        private static final int RETRY_DELAY_ON_FAILURE_MILLIS = 600_000; // 10 minutes
+
+        @Nullable private LinkAddress mNat64Cidr;
+
+        @Override
+        public void onIpv4PrefixConflict(IpPrefix prefix) {
+            mHandler.post(() -> onIpv4PrefixConflictInternal(prefix));
+        }
+
+        private void onIpv4PrefixConflictInternal(IpPrefix prefix) {
+            checkOnHandlerThread();
+
+            LOG.i("Conflict on NAT64 CIDR: " + prefix);
+            maybeReleaseNat64Cidr();
+            maybeUpdateNat64Cidr();
+        }
+
+        public void maybeUpdateNat64Cidr() {
+            checkOnHandlerThread();
+
+            if (mPersistentSettings.getConfiguration().isNat64Enabled()) {
+                maybeRequestNat64Cidr();
+            } else {
+                maybeReleaseNat64Cidr();
+            }
+            try {
+                getOtDaemon()
+                        .setNat64Cidr(
+                                mNat64Cidr == null ? null : mNat64Cidr.toString(),
+                                new LoggingOtStatusReceiver("setNat64Cidr"));
+            } catch (RemoteException | ThreadNetworkException e) {
+                LOG.e("Failed to set NAT64 CIDR at otd-daemon", e);
+            }
+            maybeSendLinkProperties();
+        }
+
+        private void maybeRequestNat64Cidr() {
+            if (mNat64Cidr != null) {
+                return;
+            }
+            final LinkAddress downstreamAddress =
+                    mRoutingCoordinatorManager.requestDownstreamAddress(this);
+            if (downstreamAddress == null) {
+                mHandler.postDelayed(() -> maybeUpdateNat64Cidr(), RETRY_DELAY_ON_FAILURE_MILLIS);
+            }
+            mNat64Cidr = downstreamAddress;
+            LOG.i("Allocated NAT64 CIDR: " + mNat64Cidr);
+        }
+
+        private void maybeReleaseNat64Cidr() {
+            if (mNat64Cidr == null) {
+                return;
+            }
+            LOG.i("Released NAT64 CIDR: " + mNat64Cidr);
+            mNat64Cidr = null;
+            mRoutingCoordinatorManager.releaseDownstream(this);
+        }
+    }
 }
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
index 85a0371..520a434 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -92,8 +92,19 @@
     }
 
     /** Returns link properties of the Thread TUN interface. */
-    public LinkProperties getLinkProperties() {
-        return mLinkProperties;
+    private LinkProperties getLinkProperties() {
+        return new LinkProperties(mLinkProperties);
+    }
+
+    /** Returns link properties of the Thread TUN interface with the given NAT64 CIDR. */
+    // TODO: manage the NAT64 CIDR in the TunInterfaceController
+    public LinkProperties getLinkPropertiesWithNat64Cidr(@Nullable LinkAddress nat64Cidr) {
+        final LinkProperties lp = getLinkProperties();
+        if (nat64Cidr != null) {
+            lp.addLinkAddress(nat64Cidr);
+            lp.addRoute(getRouteForAddress(nat64Cidr));
+        }
+        return lp;
     }
 
     /**
@@ -148,6 +159,9 @@
 
     /** Adds a new address to the interface. */
     public void addAddress(LinkAddress address) {
+        if (!(address.getAddress() instanceof Inet6Address)) {
+            return;
+        }
         LOG.v("Adding address " + address + " with flags: " + address.getFlags());
 
         long preferredLifetimeSeconds;
@@ -172,7 +186,7 @@
                             (address.getExpirationTime() - SystemClock.elapsedRealtime()) / 1000L,
                             0L);
         }
-
+        // Only apply to Ipv6 address
         if (!NetlinkUtils.sendRtmNewAddressRequest(
                 Os.if_nametoindex(mIfName),
                 address.getAddress(),
@@ -190,6 +204,9 @@
 
     /** Removes an address from the interface. */
     public void removeAddress(LinkAddress address) {
+        if (!(address.getAddress() instanceof Inet6Address)) {
+            return;
+        }
         LOG.v("Removing address " + address);
 
         // Intentionally update the mLinkProperties before send netlink message because the
@@ -197,6 +214,7 @@
         // when the netlink request below fails
         mLinkProperties.removeLinkAddress(address);
         mLinkProperties.removeRoute(getRouteForAddress(address));
+        // Only apply to Ipv6 address
         if (!NetlinkUtils.sendRtmDelAddressRequest(
                 Os.if_nametoindex(mIfName),
                 (Inet6Address) address.getAddress(),
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index cf7a4f7..f6dd6b9 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -19,6 +19,7 @@
 import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
 import static android.net.InetAddresses.parseNumericAddress;
 import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET;
+import static android.net.thread.utils.IntegrationTestUtils.buildIcmpv4EchoReply;
 import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
 import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv4Packet;
 import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
@@ -77,9 +78,11 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.IOException;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
+import java.nio.ByteBuffer;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
@@ -101,7 +104,6 @@
             (Inet6Address) parseNumericAddress("ff03::1234");
     private static final Inet4Address IPV4_SERVER_ADDR =
             (Inet4Address) parseNumericAddress("8.8.8.8");
-    private static final String NAT64_CIDR = "192.168.255.0/24";
     private static final IpPrefix DHCP6_PD_PREFIX = new IpPrefix("2001:db8::/64");
     private static final IpPrefix AIL_NAT64_PREFIX = new IpPrefix("2001:db8:1234::/96");
     private static final Inet6Address AIL_NAT64_SYNTHESIZED_SERVER_ADDR =
@@ -647,17 +649,27 @@
     }
 
     @Test
-    public void nat64_threadDevicePingIpv4InfraDevice_outboundPacketIsForwarded() throws Exception {
+    public void nat64_threadDevicePingIpv4InfraDevice_outboundPacketIsForwardedAndReplyIsReceived()
+            throws Exception {
         FullThreadDevice ftd = mFtds.get(0);
         joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
-        // TODO: enable NAT64 via ThreadNetworkController API instead of ot-ctl
-        mOtCtl.setNat64Cidr(NAT64_CIDR);
-        mOtCtl.setNat64Enabled(true);
+        mController.setNat64EnabledAndWait(true);
         waitFor(() -> mOtCtl.hasNat64PrefixInNetdata(), UPDATE_NAT64_PREFIX_TIMEOUT);
+        Thread echoReplyThread = new Thread(() -> respondToEchoRequestOnce(IPV4_SERVER_ADDR));
+        echoReplyThread.start();
 
-        ftd.ping(IPV4_SERVER_ADDR);
+        assertThat(ftd.ping(IPV4_SERVER_ADDR, 1 /* count */)).isEqualTo(1);
 
-        assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMP_ECHO, null, IPV4_SERVER_ADDR));
+        echoReplyThread.join();
+    }
+
+    private void respondToEchoRequestOnce(Inet4Address dstAddress) {
+        byte[] echoRequest = pollForIcmpPacketOnInfraNetwork(ICMP_ECHO, null, dstAddress);
+        assertNotNull(echoRequest);
+        try {
+            mInfraNetworkReader.sendResponse(buildIcmpv4EchoReply(ByteBuffer.wrap(echoRequest)));
+        } catch (IOException ignored) {
+        }
     }
 
     @Test
@@ -685,8 +697,7 @@
         mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
         FullThreadDevice ftd = mFtds.get(0);
         joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
-        // TODO: enable NAT64 via ThreadNetworkController API instead of ot-ctl
-        mOtCtl.setNat64Enabled(true);
+        mController.setNat64EnabledAndWait(true);
         mOtCtl.addPrefixInNetworkData(DHCP6_PD_PREFIX, "paros", "med");
         waitFor(() -> mOtCtl.hasNat64PrefixInNetdata(), UPDATE_NAT64_PREFIX_TIMEOUT);
 
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
index d903636..dc2a9c9 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -38,9 +38,15 @@
 import android.os.Handler
 import android.os.SystemClock
 import android.system.OsConstants
+import android.system.OsConstants.IPPROTO_ICMP
 import androidx.test.core.app.ApplicationProvider
 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
+import com.android.net.module.util.IpUtils
 import com.android.net.module.util.NetworkStackConstants
+import com.android.net.module.util.NetworkStackConstants.ICMP_CHECKSUM_OFFSET
+import com.android.net.module.util.NetworkStackConstants.IPV4_CHECKSUM_OFFSET
+import com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV4_LENGTH_OFFSET
 import com.android.net.module.util.Struct
 import com.android.net.module.util.structs.Icmpv4Header
 import com.android.net.module.util.structs.Icmpv6Header
@@ -307,6 +313,73 @@
         return null
     }
 
+    /** Builds an ICMPv4 Echo Reply packet to respond to the given ICMPv4 Echo Request packet. */
+    @JvmStatic
+    fun buildIcmpv4EchoReply(request: ByteBuffer): ByteBuffer? {
+        val requestIpv4Header = Struct.parse(Ipv4Header::class.java, request) ?: return null
+        val requestIcmpv4Header = Struct.parse(Icmpv4Header::class.java, request) ?: return null
+
+        val id = request.getShort()
+        val seq = request.getShort()
+
+        val payload = ByteBuffer.allocate(4 + request.limit() - request.position())
+        payload.putShort(id)
+        payload.putShort(seq)
+        payload.put(request)
+        payload.rewind()
+
+        val ipv4HeaderLen = Struct.getSize(Ipv4Header::class.java)
+        val Icmpv4HeaderLen = Struct.getSize(Icmpv4Header::class.java)
+        val payloadLen = payload.limit();
+
+        val reply = ByteBuffer.allocate(ipv4HeaderLen + Icmpv4HeaderLen + payloadLen)
+
+        // IPv4 header
+        val replyIpv4Header = Ipv4Header(
+            0 /* TYPE OF SERVICE */,
+            0.toShort().toInt()/* totalLength, calculate later */,
+            requestIpv4Header.id,
+            requestIpv4Header.flagsAndFragmentOffset,
+            0x40 /* ttl */,
+            IPPROTO_ICMP.toByte(),
+            0.toShort()/* checksum, calculate later */,
+            requestIpv4Header.dstIp /* srcIp */,
+            requestIpv4Header.srcIp /* dstIp */
+        )
+        replyIpv4Header.writeToByteBuffer(reply)
+
+        // ICMPv4 header
+        val replyIcmpv4Header = Icmpv4Header(
+            0 /* type, ICMP_ECHOREPLY */,
+            requestIcmpv4Header.code,
+            0.toShort() /* checksum, calculate later */
+        )
+        replyIcmpv4Header.writeToByteBuffer(reply)
+
+        // Payload
+        reply.put(payload)
+        reply.flip()
+
+        // Populate the IPv4 totalLength field.
+        reply.putShort(
+            IPV4_LENGTH_OFFSET, (ipv4HeaderLen + Icmpv4HeaderLen + payloadLen).toShort()
+        )
+
+        // Populate the IPv4 header checksum field.
+        reply.putShort(
+            IPV4_CHECKSUM_OFFSET, IpUtils.ipChecksum(reply, 0 /* headerOffset */)
+        )
+
+        // Populate the ICMP checksum field.
+        reply.putShort(
+            IPV4_HEADER_MIN_LEN + ICMP_CHECKSUM_OFFSET, IpUtils.icmpChecksum(
+                reply, IPV4_HEADER_MIN_LEN, Icmpv4HeaderLen + payloadLen
+            )
+        )
+
+        return reply
+    }
+
     /** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message.  */
     @JvmStatic
     fun getRaPios(raMsg: ByteArray?): List<PrefixInformationOption> {
diff --git a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
index 4a30c45..4354702 100644
--- a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
+++ b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
@@ -200,7 +200,7 @@
         runAsShell(
                 PERMISSION_THREAD_NETWORK_PRIVILEGED,
                 () -> mController.registerConfigurationCallback(directExecutor(), callback));
-        future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS);
+        future.get(CONFIG_TIMEOUT.toSeconds(), SECONDS);
         runAsShell(
                 PERMISSION_THREAD_NETWORK_PRIVILEGED,
                 () -> mController.unregisterConfigurationCallback(callback));
@@ -214,7 +214,14 @@
                 () ->
                         mController.setConfiguration(
                                 config, directExecutor(), newOutcomeReceiver(future)));
-        future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS);
+        future.get(CONFIG_TIMEOUT.toSeconds(), SECONDS);
+    }
+
+    public void setNat64EnabledAndWait(boolean enabled) throws Exception {
+        final ThreadConfiguration config = getConfiguration();
+        final ThreadConfiguration newConfig =
+                new ThreadConfiguration.Builder(config).setNat64Enabled(enabled).build();
+        setConfigurationAndWait(newConfig);
     }
 
     private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
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 7ac404f..e188491 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -44,6 +44,8 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNotNull;
+import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
@@ -64,6 +66,7 @@
 import android.content.Intent;
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
+import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkAgent;
@@ -91,9 +94,12 @@
 
 import com.android.connectivity.resources.R;
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.net.module.util.RoutingCoordinatorManager;
 import com.android.server.connectivity.ConnectivityResources;
 import com.android.server.thread.openthread.DnsTxtAttribute;
+import com.android.server.thread.openthread.IOtStatusReceiver;
 import com.android.server.thread.openthread.MeshcopTxtAttributes;
+import com.android.server.thread.openthread.OtDaemonConfiguration;
 import com.android.server.thread.openthread.testing.FakeOtDaemon;
 
 import org.junit.Before;
@@ -164,8 +170,10 @@
     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 LinkAddress TEST_NAT64_CIDR = new LinkAddress("192.168.255.0/24");
 
     @Mock private ConnectivityManager mMockConnectivityManager;
+    @Mock private RoutingCoordinatorManager mMockRoutingCoordinatorManager;
     @Mock private NetworkAgent mMockNetworkAgent;
     @Mock private TunInterfaceController mMockTunIfController;
     @Mock private ParcelFileDescriptor mMockTunFd;
@@ -208,7 +216,10 @@
         NetworkProvider networkProvider =
                 new NetworkProvider(mContext, mTestLooper.getLooper(), "ThreadNetworkProvider");
 
-        mFakeOtDaemon = new FakeOtDaemon(handler);
+        when(mMockRoutingCoordinatorManager.requestDownstreamAddress(any()))
+                .thenReturn(TEST_NAT64_CIDR);
+
+        mFakeOtDaemon = spy(new FakeOtDaemon(handler));
         when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
 
         when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
@@ -235,6 +246,7 @@
                         networkProvider,
                         () -> mFakeOtDaemon,
                         mMockConnectivityManager,
+                        mMockRoutingCoordinatorManager,
                         mMockTunIfController,
                         mMockInfraIfController,
                         mPersistentSettings,
@@ -281,6 +293,37 @@
     }
 
     @Test
+    public void initialize_nat64Disabled_doesNotRequestNat64CidrAndConfiguresOtDaemon()
+            throws Exception {
+        ThreadConfiguration config =
+                new ThreadConfiguration.Builder().setNat64Enabled(false).build();
+        mPersistentSettings.putConfiguration(config);
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        verify(mMockRoutingCoordinatorManager, never()).requestDownstreamAddress(any());
+        verify(mFakeOtDaemon, times(1)).setNat64Cidr(isNull(), any());
+        verify(mFakeOtDaemon, never()).setNat64Cidr(isNotNull(), any());
+    }
+
+    @Test
+    public void initialize_nat64Enabled_requestsNat64CidrAndConfiguresAtOtDaemon()
+            throws Exception {
+        ThreadConfiguration config =
+                new ThreadConfiguration.Builder().setNat64Enabled(true).build();
+        mPersistentSettings.putConfiguration(config);
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        verify(mMockRoutingCoordinatorManager, times(1)).requestDownstreamAddress(any());
+        verify(mFakeOtDaemon, times(1))
+                .setConfiguration(
+                        new OtDaemonConfiguration.Builder().setNat64Enabled(true).build(),
+                        null /* receiver */);
+        verify(mFakeOtDaemon, times(1)).setNat64Cidr(eq(TEST_NAT64_CIDR.toString()), any());
+    }
+
+    @Test
     public void getMeshcopTxtAttributes_emptyVendorName_accepted() {
         when(mResources.getString(eq(R.string.config_thread_vendor_name))).thenReturn("");
 
@@ -758,6 +801,71 @@
     }
 
     @Test
+    public void setConfiguration_enablesNat64_requestsNat64CidrAndConfiguresOtdaemon()
+            throws Exception {
+        mService.initialize();
+        mTestLooper.dispatchAll();
+        clearInvocations(mMockRoutingCoordinatorManager, mFakeOtDaemon);
+
+        final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+        mService.setConfiguration(
+                new ThreadConfiguration.Builder().setNat64Enabled(true).build(), mockReceiver);
+        mTestLooper.dispatchAll();
+
+        verify(mockReceiver, times(1)).onSuccess();
+        verify(mMockRoutingCoordinatorManager, times(1)).requestDownstreamAddress(any());
+        verify(mFakeOtDaemon, times(1))
+                .setConfiguration(
+                        eq(new OtDaemonConfiguration.Builder().setNat64Enabled(true).build()),
+                        any(IOtStatusReceiver.class));
+        verify(mFakeOtDaemon, times(1))
+                .setNat64Cidr(eq(TEST_NAT64_CIDR.toString()), any(IOtStatusReceiver.class));
+    }
+
+    @Test
+    public void setConfiguration_enablesNat64_otDaemonRemoteFailure_serviceDoesNotCrash()
+            throws Exception {
+        mService.initialize();
+        mTestLooper.dispatchAll();
+        clearInvocations(mMockRoutingCoordinatorManager, mFakeOtDaemon);
+        mFakeOtDaemon.setSetNat64CidrException(
+                new RemoteException("ot-daemon setNat64Cidr() throws"));
+
+        final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+        mService.setConfiguration(
+                new ThreadConfiguration.Builder().setNat64Enabled(true).build(), mockReceiver);
+        mTestLooper.dispatchAll();
+
+        verify(mFakeOtDaemon, times(1))
+                .setNat64Cidr(eq(TEST_NAT64_CIDR.toString()), any(IOtStatusReceiver.class));
+    }
+
+    @Test
+    public void setConfiguration_disablesNat64_releasesNat64CidrAndConfiguresOtdaemon()
+            throws Exception {
+        mPersistentSettings.putConfiguration(
+                new ThreadConfiguration.Builder().setNat64Enabled(true).build());
+        mService.initialize();
+        mTestLooper.dispatchAll();
+        clearInvocations(mMockRoutingCoordinatorManager, mFakeOtDaemon);
+
+        final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+        mService.setConfiguration(
+                new ThreadConfiguration.Builder().setNat64Enabled(false).build(), mockReceiver);
+        mTestLooper.dispatchAll();
+
+        verify(mockReceiver, times(1)).onSuccess();
+        verify(mMockRoutingCoordinatorManager, times(1)).releaseDownstream(any());
+        verify(mMockRoutingCoordinatorManager, never()).requestDownstreamAddress(any());
+        verify(mFakeOtDaemon, times(1))
+                .setConfiguration(
+                        eq(new OtDaemonConfiguration.Builder().setNat64Enabled(false).build()),
+                        any(IOtStatusReceiver.class));
+        verify(mFakeOtDaemon, times(1)).setNat64Cidr(isNull(), any(IOtStatusReceiver.class));
+        verify(mFakeOtDaemon, never()).setNat64Cidr(isNotNull(), any(IOtStatusReceiver.class));
+    }
+
+    @Test
     public void initialize_upstreamNetworkRequestHasCertainTransportTypesAndCapabilities() {
         mService.initialize();
         mTestLooper.dispatchAll();
diff --git a/tools/Android.bp b/tools/Android.bp
index 2c2ed14..1351eb7 100644
--- a/tools/Android.bp
+++ b/tools/Android.bp
@@ -81,7 +81,7 @@
         "gen_jarjar.py",
         "gen_jarjar_test.py",
     ],
-    data: [
+    device_common_data: [
         "testdata/test-jarjar-excludes.txt",
         // txt with Test classes to test they aren't included when added to jarjar excludes
         "testdata/test-jarjar-excludes-testclass.txt",