Merge "Update the imports to androidx.test.filters.*" into main
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 47227e3..b694211 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -37,6 +37,7 @@
         "//frameworks/base/core/tests/benchmarks",
         "//frameworks/base/core/tests/utillib",
         "//frameworks/base/packages/Connectivity/tests:__subpackages__",
+        "//frameworks/base/services/tests/VpnTests",
         "//frameworks/base/tests/vcn",
         "//frameworks/opt/telephony/tests/telephonytests",
         "//packages/modules/CaptivePortalLogin/tests",
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index 468cee4..bc919ac 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -182,6 +182,7 @@
         "//frameworks/base/core/tests/bandwidthtests",
         "//frameworks/base/core/tests/benchmarks",
         "//frameworks/base/core/tests/utillib",
+        "//frameworks/base/services/tests/VpnTests",
         "//frameworks/base/tests/vcn",
         "//frameworks/opt/net/ethernet/tests:__subpackages__",
         "//frameworks/opt/telephony/tests/telephonytests",
diff --git a/framework-t/src/android/net/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java
index b1ef98f..2895b0c 100644
--- a/framework-t/src/android/net/nsd/AdvertisingRequest.java
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -17,11 +17,13 @@
 
 import android.annotation.LongDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.Parcelable;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.time.Duration;
 import java.util.Objects;
 
 /**
@@ -34,7 +36,7 @@
     /**
      * Only update the registration without sending exit and re-announcement.
      */
-    public static final int NSD_ADVERTISING_UPDATE_ONLY = 1;
+    public static final long NSD_ADVERTISING_UPDATE_ONLY = 1;
 
 
     @NonNull
@@ -46,7 +48,9 @@
                             NsdServiceInfo.class.getClassLoader(), NsdServiceInfo.class);
                     final int protocolType = in.readInt();
                     final long advertiseConfig = in.readLong();
-                    return new AdvertisingRequest(serviceInfo, protocolType, advertiseConfig);
+                    final long ttlSeconds = in.readLong();
+                    final Duration ttl = ttlSeconds < 0 ? null : Duration.ofSeconds(ttlSeconds);
+                    return new AdvertisingRequest(serviceInfo, protocolType, advertiseConfig, ttl);
                 }
 
                 @Override
@@ -60,6 +64,9 @@
     // Bitmask of @AdvertisingConfig flags. Uses a long to allow 64 possible flags in the future.
     private final long mAdvertisingConfig;
 
+    @Nullable
+    private final Duration mTtl;
+
     /**
      * @hide
      */
@@ -73,10 +80,11 @@
      * The constructor for the advertiseRequest
      */
     private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, int protocolType,
-            long advertisingConfig) {
+            long advertisingConfig, @NonNull Duration ttl) {
         mServiceInfo = serviceInfo;
         mProtocolType = protocolType;
         mAdvertisingConfig = advertisingConfig;
+        mTtl = ttl;
     }
 
     /**
@@ -101,12 +109,25 @@
         return mAdvertisingConfig;
     }
 
+    /**
+     * Returns the time interval that the resource records may be cached on a DNS resolver or
+     * {@code null} if not specified.
+     *
+     * @hide
+     */
+    // @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_TTL_ENABLED)
+    @Nullable
+    public Duration getTtl() {
+        return mTtl;
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
         sb.append("serviceInfo: ").append(mServiceInfo)
                 .append(", protocolType: ").append(mProtocolType)
-                .append(", advertisingConfig: ").append(mAdvertisingConfig);
+                .append(", advertisingConfig: ").append(mAdvertisingConfig)
+                .append(", ttl: ").append(mTtl);
         return sb.toString();
     }
 
@@ -120,13 +141,14 @@
             final AdvertisingRequest otherRequest = (AdvertisingRequest) other;
             return mServiceInfo.equals(otherRequest.mServiceInfo)
                     && mProtocolType == otherRequest.mProtocolType
-                    && mAdvertisingConfig == otherRequest.mAdvertisingConfig;
+                    && mAdvertisingConfig == otherRequest.mAdvertisingConfig
+                    && Objects.equals(mTtl, otherRequest.mTtl);
         }
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mServiceInfo, mProtocolType, mAdvertisingConfig);
+        return Objects.hash(mServiceInfo, mProtocolType, mAdvertisingConfig, mTtl);
     }
 
     @Override
@@ -139,6 +161,7 @@
         dest.writeParcelable(mServiceInfo, flags);
         dest.writeInt(mProtocolType);
         dest.writeLong(mAdvertisingConfig);
+        dest.writeLong(mTtl == null ? -1 : mTtl.getSeconds());
     }
 
 //    @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
@@ -151,6 +174,8 @@
         private final NsdServiceInfo mServiceInfo;
         private final int mProtocolType;
         private long mAdvertisingConfig;
+        @Nullable
+        private Duration mTtl;
         /**
          * Creates a new {@link Builder} object.
          */
@@ -170,11 +195,44 @@
             return this;
         }
 
+        /**
+         * Sets the time interval that the resource records may be cached on a DNS resolver.
+         *
+         * If this method is not called or {@code ttl} is {@code null}, default TTL values
+         * will be used for the service when it's registered. Otherwise, the {@code ttl}
+         * will be used for all resource records of this service.
+         *
+         * When registering a service, {@link NsdManager#FAILURE_BAD_PARAMETERS} will be returned
+         * if {@code ttl} is smaller than 30 seconds.
+         *
+         * Note: only number of seconds of {@code ttl} is used.
+         *
+         * @param ttl the maximum duration that the DNS resource records will be cached
+         *
+         * @see AdvertisingRequest#getTtl
+         * @hide
+         */
+        // @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_TTL_ENABLED)
+        @NonNull
+        public Builder setTtl(@Nullable Duration ttl) {
+            if (ttl == null) {
+                mTtl = null;
+                return this;
+            }
+            final long ttlSeconds = ttl.getSeconds();
+            if (ttlSeconds < 0 || ttlSeconds > 0xffffffffL) {
+                throw new IllegalArgumentException(
+                        "ttlSeconds exceeds the allowed range (value = " + ttlSeconds
+                                + ", allowedRanged = [0, 0xffffffffL])");
+            }
+            mTtl = Duration.ofSeconds(ttlSeconds);
+            return this;
+        }
 
         /** Creates a new {@link AdvertisingRequest} object. */
         @NonNull
         public AdvertisingRequest build() {
-            return new AdvertisingRequest(mServiceInfo, mProtocolType, mAdvertisingConfig);
+            return new AdvertisingRequest(mServiceInfo, mProtocolType, mAdvertisingConfig, mTtl);
         }
     }
 }
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index f6e1324..1001423 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -160,6 +160,8 @@
                 "com.android.net.flags.advertise_request_api";
         static final String NSD_CUSTOM_HOSTNAME_ENABLED =
                 "com.android.net.flags.nsd_custom_hostname_enabled";
+        static final String NSD_CUSTOM_TTL_ENABLED =
+                "com.android.net.flags.nsd_custom_ttl_enabled";
     }
 
     /**
@@ -327,6 +329,20 @@
     /** Dns based service discovery protocol */
     public static final int PROTOCOL_DNS_SD = 0x0001;
 
+    /**
+     * The minimum TTL seconds which is allowed for a service registration.
+     *
+     * @hide
+     */
+    public static final long TTL_SECONDS_MIN = 30L;
+
+    /**
+     * The maximum TTL seconds which is allowed for a service registration.
+     *
+     * @hide
+     */
+    public static final long TTL_SECONDS_MAX = 10 * 3600L;
+
     private static final SparseArray<String> EVENT_NAMES = new SparseArray<>();
     static {
         EVENT_NAMES.put(DISCOVER_SERVICES, "DISCOVER_SERVICES");
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 146d4ca..f4cc2ac 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -16,8 +16,6 @@
 
 package android.net.nsd;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -35,6 +33,7 @@
 import java.io.UnsupportedEncodingException;
 import java.net.InetAddress;
 import java.nio.charset.StandardCharsets;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -71,6 +70,10 @@
 
     private int mInterfaceIndex;
 
+    // The timestamp that all resource records associated with this service are considered invalid.
+    @Nullable
+    private Instant mExpirationTime;
+
     public NsdServiceInfo() {
         mSubtypes = new ArraySet<>();
         mTxtRecord = new ArrayMap<>();
@@ -99,6 +102,7 @@
         mPort = other.getPort();
         mNetwork = other.getNetwork();
         mInterfaceIndex = other.getInterfaceIndex();
+        mExpirationTime = other.getExpirationTime();
     }
 
     /** Get the service name */
@@ -490,6 +494,38 @@
         return Collections.unmodifiableSet(mSubtypes);
     }
 
+    /**
+     * Sets the timestamp after when this service is expired.
+     *
+     * Note: only number of seconds of {@code expirationTime} is used.
+     *
+     * @hide
+     */
+    public void setExpirationTime(@Nullable Instant expirationTime) {
+        if (expirationTime == null) {
+            mExpirationTime = null;
+        } else {
+            mExpirationTime = Instant.ofEpochSecond(expirationTime.getEpochSecond());
+        }
+    }
+
+    /**
+     * Returns the timestamp after when this service is expired or {@code null} if it's unknown.
+     *
+     * A service is considered expired if any of its DNS record is expired.
+     *
+     * Clients that are depending on the refreshness of the service information should not continue
+     * use this service after the returned timestamp. Instead, clients may re-send queries for the
+     * service to get updated the service information.
+     *
+     * @hide
+     */
+    // @FlaggedApi(NsdManager.Flags.NSD_CUSTOM_TTL_ENABLED)
+    @Nullable
+    public Instant getExpirationTime() {
+        return mExpirationTime;
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
@@ -499,7 +535,8 @@
                 .append(", hostAddresses: ").append(TextUtils.join(", ", mHostAddresses))
                 .append(", hostname: ").append(mHostname)
                 .append(", port: ").append(mPort)
-                .append(", network: ").append(mNetwork);
+                .append(", network: ").append(mNetwork)
+                .append(", expirationTime: ").append(mExpirationTime);
 
         byte[] txtRecord = getTxtRecord();
         sb.append(", txtRecord: ").append(new String(txtRecord, StandardCharsets.UTF_8));
@@ -539,6 +576,7 @@
             InetAddressUtils.parcelInetAddress(dest, address, flags);
         }
         dest.writeString(mHostname);
+        dest.writeLong(mExpirationTime != null ? mExpirationTime.getEpochSecond() : -1);
     }
 
     /** Implement the Parcelable interface */
@@ -569,6 +607,8 @@
                     info.mHostAddresses.add(InetAddressUtils.unparcelInetAddress(in));
                 }
                 info.mHostname = in.readString();
+                final long seconds = in.readLong();
+                info.setExpirationTime(seconds < 0 ? null : Instant.ofEpochSecond(seconds));
                 return info;
             }
 
diff --git a/framework/Android.bp b/framework/Android.bp
index 8fa336a..624eb4b 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -96,7 +96,6 @@
     ],
     impl_only_static_libs: [
         "net-utils-device-common-bpf",
-        "net-utils-device-common-struct",
     ],
     libs: [
         "androidx.annotation_annotation",
@@ -125,7 +124,6 @@
         // Even if the library is included in "impl_only_static_libs" of defaults. This is still
         // needed because java_library which doesn't understand "impl_only_static_libs".
         "net-utils-device-common-bpf",
-        "net-utils-device-common-struct",
     ],
     libs: [
         // This cannot be in the defaults clause above because if it were, it would be used
@@ -178,6 +176,7 @@
         "//frameworks/base/core/tests/bandwidthtests",
         "//frameworks/base/core/tests/benchmarks",
         "//frameworks/base/core/tests/utillib",
+        "//frameworks/base/services/tests/VpnTests",
         "//frameworks/base/tests/vcn",
         "//frameworks/opt/net/ethernet/tests:__subpackages__",
         "//frameworks/opt/telephony/tests/telephonytests",
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index d34fd83..749113d 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -43,6 +43,7 @@
     ],
     static_libs: [
         "androidx.core_core",
+        "android.hardware.bluetooth.finder-V1-java",
         "guava",
         "libprotobuf-java-lite",
         "modules-utils-build",
diff --git a/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java b/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java
index 63ff516..365b099 100644
--- a/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java
@@ -16,26 +16,151 @@
 
 package com.android.server.nearby.managers;
 
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.TargetApi;
+import android.hardware.bluetooth.finder.Eid;
+import android.hardware.bluetooth.finder.IBluetoothFinder;
 import android.nearby.PoweredOffFindingEphemeralId;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
 
 import java.util.List;
 
 /** Connects to {@link IBluetoothFinder} HAL and invokes its API. */
-// A placeholder implementation until the HAL API can be used.
+@TargetApi(Build.VERSION_CODES.TIRAMISU)
 public class BluetoothFinderManager {
 
-    private boolean mPoweredOffFindingModeEnabled = false;
+    private static final String HAL_INSTANCE_NAME = IBluetoothFinder.DESCRIPTOR + "/default";
 
-    /** An empty implementation of the corresponding HAL API call. */
-    public void sendEids(List<PoweredOffFindingEphemeralId> eids) {}
+    private IBluetoothFinder mBluetoothFinder;
+    private IBinder.DeathRecipient mServiceDeathRecipient;
+    private final Object mLock = new Object();
 
-    /** A placeholder implementation of the corresponding HAL API call. */
-    public void setPoweredOffFinderMode(boolean enable) {
-        mPoweredOffFindingModeEnabled = enable;
+    private boolean initBluetoothFinderHal() {
+        final String methodStr = "initBluetoothFinderHal";
+        if (!SdkLevel.isAtLeastV()) return false;
+        synchronized (mLock) {
+            if (mBluetoothFinder != null) {
+                Log.i(TAG, "Bluetooth Finder HAL is already initialized");
+                return true;
+            }
+            try {
+                mBluetoothFinder = getServiceMockable();
+                if (mBluetoothFinder == null) {
+                    Log.e(TAG, "Unable to obtain IBluetoothFinder");
+                    return false;
+                }
+                Log.i(TAG, "Obtained IBluetoothFinder. Local ver: " + IBluetoothFinder.VERSION
+                        + ", Remote ver: " + mBluetoothFinder.getInterfaceVersion());
+
+                IBinder serviceBinder = getServiceBinderMockable();
+                if (serviceBinder == null) {
+                    Log.e(TAG, "Unable to obtain the service binder for IBluetoothFinder");
+                    return false;
+                }
+                mServiceDeathRecipient = new BluetoothFinderDeathRecipient();
+                serviceBinder.linkToDeath(mServiceDeathRecipient, /* flags= */ 0);
+
+                Log.i(TAG, "Bluetooth Finder HAL initialization was successful");
+                return true;
+            } catch (RemoteException e) {
+                handleRemoteException(e, methodStr);
+            } catch (Exception e) {
+                Log.e(TAG, methodStr + " encountered an exception: "  + e);
+            }
+            return false;
+        }
     }
 
-    /** A placeholder implementation of the corresponding HAL API call. */
+    @VisibleForTesting
+    protected IBluetoothFinder getServiceMockable() {
+        return IBluetoothFinder.Stub.asInterface(
+                ServiceManager.waitForDeclaredService(HAL_INSTANCE_NAME));
+    }
+
+    @VisibleForTesting
+    protected IBinder getServiceBinderMockable() {
+        return mBluetoothFinder.asBinder();
+    }
+
+    private class BluetoothFinderDeathRecipient implements IBinder.DeathRecipient {
+        @Override
+        public void binderDied() {
+            Log.e(TAG, "BluetoothFinder service died.");
+            synchronized (mLock) {
+                mBluetoothFinder = null;
+            }
+        }
+    }
+
+    /** See comments for {@link IBluetoothFinder#sendEids(Eid[])} */
+    public void sendEids(List<PoweredOffFindingEphemeralId> eids) {
+        final String methodStr = "sendEids";
+        if (!checkHalAndLogFailure(methodStr)) return;
+        Eid[] eidArray = eids.stream().map(
+                ephmeralId -> {
+                    Eid eid = new Eid();
+                    eid.bytes = ephmeralId.bytes;
+                    return eid;
+                }).toArray(Eid[]::new);
+        try {
+            mBluetoothFinder.sendEids(eidArray);
+        } catch (RemoteException e) {
+            handleRemoteException(e, methodStr);
+        } catch (ServiceSpecificException e) {
+            handleServiceSpecificException(e, methodStr);
+        }
+    }
+
+    /** See comments for {@link IBluetoothFinder#setPoweredOffFinderMode(boolean)} */
+    public void setPoweredOffFinderMode(boolean enable) {
+        final String methodStr = "setPoweredOffMode";
+        if (!checkHalAndLogFailure(methodStr)) return;
+        try {
+            mBluetoothFinder.setPoweredOffFinderMode(enable);
+        } catch (RemoteException e) {
+            handleRemoteException(e, methodStr);
+        } catch (ServiceSpecificException e) {
+            handleServiceSpecificException(e, methodStr);
+        }
+    }
+
+    /** See comments for {@link IBluetoothFinder#getPoweredOffFinderMode()} */
     public boolean getPoweredOffFinderMode() {
-        return mPoweredOffFindingModeEnabled;
+        final String methodStr = "getPoweredOffMode";
+        if (!checkHalAndLogFailure(methodStr)) return false;
+        try {
+            return mBluetoothFinder.getPoweredOffFinderMode();
+        } catch (RemoteException e) {
+            handleRemoteException(e, methodStr);
+        } catch (ServiceSpecificException e) {
+            handleServiceSpecificException(e, methodStr);
+        }
+        return false;
+    }
+
+    private boolean checkHalAndLogFailure(String methodStr) {
+        if ((mBluetoothFinder == null) && !initBluetoothFinderHal()) {
+            Log.e(TAG, "Unable to call " + methodStr + " because IBluetoothFinder is null.");
+            return false;
+        }
+        return true;
+    }
+
+    private void handleRemoteException(RemoteException e, String methodStr) {
+        mBluetoothFinder = null;
+        Log.e(TAG, methodStr + " failed with remote exception: " + e);
+    }
+
+    private void handleServiceSpecificException(ServiceSpecificException e, String methodStr) {
+        Log.e(TAG, methodStr + " failed with service-specific exception: " + e);
     }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
new file mode 100644
index 0000000..671b5c5
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BluetoothFinderManagerTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.nearby.managers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.bluetooth.finder.Eid;
+import android.hardware.bluetooth.finder.IBluetoothFinder;
+import android.nearby.PoweredOffFindingEphemeralId;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+public class BluetoothFinderManagerTest {
+    private BluetoothFinderManager mBluetoothFinderManager;
+    private boolean mGetServiceCalled = false;
+
+    @Mock private IBluetoothFinder mIBluetoothFinderMock;
+    @Mock private IBinder mServiceBinderMock;
+
+    private ArgumentCaptor<DeathRecipient> mDeathRecipientCaptor =
+            ArgumentCaptor.forClass(DeathRecipient.class);
+
+    private ArgumentCaptor<Eid[]> mEidArrayCaptor = ArgumentCaptor.forClass(Eid[].class);
+
+    private class BluetoothFinderManagerSpy extends BluetoothFinderManager {
+        @Override
+        protected IBluetoothFinder getServiceMockable() {
+            mGetServiceCalled = true;
+            return mIBluetoothFinderMock;
+        }
+
+        @Override
+        protected IBinder getServiceBinderMockable() {
+            return mServiceBinderMock;
+        }
+    }
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mBluetoothFinderManager = new BluetoothFinderManagerSpy();
+    }
+
+    @Test
+    public void testSendEids() throws Exception {
+        byte[] eidBytes1 = {
+                (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+                (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+                (byte) 0xe1, (byte) 0xde, (byte) 0x1d, (byte) 0xe1, (byte) 0xde, (byte) 0x1d,
+                (byte) 0xe1, (byte) 0xde
+        };
+        byte[] eidBytes2 = {
+                (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+                (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+                (byte) 0xf2, (byte) 0xef, (byte) 0x2e, (byte) 0xf2, (byte) 0xef, (byte) 0x2e,
+                (byte) 0xf2, (byte) 0xef
+        };
+        PoweredOffFindingEphemeralId ephemeralId1 = new PoweredOffFindingEphemeralId();
+        PoweredOffFindingEphemeralId ephemeralId2 = new PoweredOffFindingEphemeralId();
+        ephemeralId1.bytes = eidBytes1;
+        ephemeralId2.bytes = eidBytes2;
+
+        mBluetoothFinderManager.sendEids(List.of(ephemeralId1, ephemeralId2));
+
+        verify(mIBluetoothFinderMock).sendEids(mEidArrayCaptor.capture());
+        assertThat(mEidArrayCaptor.getValue()[0].bytes).isEqualTo(eidBytes1);
+        assertThat(mEidArrayCaptor.getValue()[1].bytes).isEqualTo(eidBytes2);
+    }
+
+    @Test
+    public void testSendEids_remoteException() throws Exception {
+        doThrow(new RemoteException())
+                .when(mIBluetoothFinderMock).sendEids(any());
+        mBluetoothFinderManager.sendEids(List.of());
+
+        // Verify that we get the service again following a RemoteException.
+        mGetServiceCalled = false;
+        mBluetoothFinderManager.sendEids(List.of());
+        assertThat(mGetServiceCalled).isTrue();
+    }
+
+    @Test
+    public void testSendEids_serviceSpecificException() throws Exception {
+        doThrow(new ServiceSpecificException(1))
+                .when(mIBluetoothFinderMock).sendEids(any());
+        mBluetoothFinderManager.sendEids(List.of());
+    }
+
+    @Test
+    public void testSetPoweredOffFinderMode() throws Exception {
+        mBluetoothFinderManager.setPoweredOffFinderMode(true);
+        verify(mIBluetoothFinderMock).setPoweredOffFinderMode(true);
+
+        mBluetoothFinderManager.setPoweredOffFinderMode(false);
+        verify(mIBluetoothFinderMock).setPoweredOffFinderMode(false);
+    }
+
+    @Test
+    public void testSetPoweredOffFinderMode_remoteException() throws Exception {
+        doThrow(new RemoteException())
+                .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+        mBluetoothFinderManager.setPoweredOffFinderMode(true);
+
+        // Verify that we get the service again following a RemoteException.
+        mGetServiceCalled = false;
+        mBluetoothFinderManager.setPoweredOffFinderMode(true);
+        assertThat(mGetServiceCalled).isTrue();
+    }
+
+    @Test
+    public void testSetPoweredOffFinderMode_serviceSpecificException() throws Exception {
+        doThrow(new ServiceSpecificException(1))
+                .when(mIBluetoothFinderMock).setPoweredOffFinderMode(anyBoolean());
+        mBluetoothFinderManager.setPoweredOffFinderMode(true);
+    }
+
+    @Test
+    public void testGetPoweredOffFinderMode() throws Exception {
+        when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenReturn(true);
+        assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isTrue();
+
+        when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenReturn(false);
+        assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+    }
+
+    @Test
+    public void testGetPoweredOffFinderMode_remoteException() throws Exception {
+        when(mIBluetoothFinderMock.getPoweredOffFinderMode()).thenThrow(new RemoteException());
+        assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+
+        // Verify that we get the service again following a RemoteException.
+        mGetServiceCalled = false;
+        assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+        assertThat(mGetServiceCalled).isTrue();
+    }
+
+    @Test
+    public void testGetPoweredOffFinderMode_serviceSpecificException() throws Exception {
+        when(mIBluetoothFinderMock.getPoweredOffFinderMode())
+                .thenThrow(new ServiceSpecificException(1));
+        assertThat(mBluetoothFinderManager.getPoweredOffFinderMode()).isFalse();
+    }
+
+    @Test
+    public void testDeathRecipient() throws Exception {
+        mBluetoothFinderManager.setPoweredOffFinderMode(true);
+        verify(mServiceBinderMock).linkToDeath(mDeathRecipientCaptor.capture(), anyInt());
+        mDeathRecipientCaptor.getValue().binderDied();
+
+        // Verify that we get the service again following a binder death.
+        mGetServiceCalled = false;
+        mBluetoothFinderManager.setPoweredOffFinderMode(true);
+        assertThat(mGetServiceCalled).isTrue();
+    }
+}
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index 9dc7cdc..ed7d048 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -369,6 +369,13 @@
         if (createSysFsBpfSubDir(location.prefix)) return 1;
     }
 
+    // Note: there's no actual src dir for fs_bpf_loader .o's,
+    // so it is not listed in 'locations[].prefix'.
+    // This is because this is primarily meant for triggering genfscon rules,
+    // and as such this will likely always be the case.
+    // Thus we need to manually create the /sys/fs/bpf/loader subdirectory.
+    if (createSysFsBpfSubDir("loader")) return 1;
+
     // Load all ELF objects, create programs and maps, and pin them
     for (const auto& location : locations) {
         if (loadAllElfObjects(location) != 0) {
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 19850fd..779f354 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -75,6 +75,7 @@
         "com.android.tethering",
     ],
     visibility: [
+        "//frameworks/base/services/tests/VpnTests",
         "//frameworks/base/tests/vcn",
         "//packages/modules/Connectivity/service",
         "//packages/modules/Connectivity/tests:__subpackages__",
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 9ba49d2..cfb1a33 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -28,6 +28,7 @@
 import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED;
 import static android.net.nsd.NsdManager.SUBTYPE_LABEL_REGEX;
 import static android.net.nsd.NsdManager.TYPE_REGEX;
+import static android.os.Process.SYSTEM_UID;
 import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
 
 import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
@@ -92,6 +93,7 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.HandlerUtils;
 import com.android.net.module.util.InetAddressUtils;
 import com.android.net.module.util.PermissionUtils;
 import com.android.net.module.util.SharedLog;
@@ -115,6 +117,7 @@
 import java.net.NetworkInterface;
 import java.net.SocketException;
 import java.net.UnknownHostException;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -738,6 +741,33 @@
                 return new ArraySet<>(subtypeMap.values());
             }
 
+            private boolean checkTtl(
+                        @Nullable Duration ttl, @NonNull ClientInfo clientInfo) {
+                if (ttl == null) {
+                    return true;
+                }
+
+                final long ttlSeconds = ttl.toSeconds();
+                final int uid = clientInfo.getUid();
+
+                // Allows Thread module in the system_server to register TTL that is smaller than
+                // 30 seconds
+                final long minTtlSeconds = uid == SYSTEM_UID ? 0 : NsdManager.TTL_SECONDS_MIN;
+
+                // Allows Thread module in the system_server to register TTL that is larger than
+                // 10 hours
+                final long maxTtlSeconds =
+                        uid == SYSTEM_UID ? 0xffffffffL : NsdManager.TTL_SECONDS_MAX;
+
+                if (ttlSeconds < minTtlSeconds || ttlSeconds > maxTtlSeconds) {
+                    mServiceLogs.e("ttlSeconds exceeds allowed range (value = "
+                            + ttlSeconds + ", allowedRange = [" + minTtlSeconds
+                            + ", " + maxTtlSeconds + " ])");
+                    return false;
+                }
+                return true;
+            }
+
             @Override
             public boolean processMessage(Message msg) {
                 final ClientInfo clientInfo;
@@ -964,11 +994,19 @@
                                 break;
                             }
 
+                            if (!checkTtl(advertisingRequest.getTtl(), clientInfo)) {
+                                clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+                                        NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
+                                break;
+                            }
+
                             serviceInfo.setSubtypes(subtypes);
                             maybeStartMonitoringSockets();
                             final MdnsAdvertisingOptions mdnsAdvertisingOptions =
-                                    MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(
-                                            isUpdateOnly).build();
+                                    MdnsAdvertisingOptions.newBuilder()
+                                            .setIsOnlyUpdate(isUpdateOnly)
+                                            .setTtl(advertisingRequest.getTtl())
+                                            .build();
                             mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
                                     mdnsAdvertisingOptions, clientInfo.mUid);
                             storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
@@ -1511,6 +1549,7 @@
                         network == null ? INetd.LOCAL_NET_ID : network.netId,
                         serviceInfo.getInterfaceIndex());
                 servInfo.setSubtypes(dedupSubtypeLabels(serviceInfo.getSubtypes()));
+                servInfo.setExpirationTime(serviceInfo.getExpirationTime());
                 return servInfo;
             }
 
@@ -2510,6 +2549,14 @@
         pw.increaseIndent();
         mServiceLogs.reverseDump(pw);
         pw.decreaseIndent();
+
+        //Dump DiscoveryManager
+        pw.println();
+        pw.println("DiscoveryManager:");
+        pw.increaseIndent();
+        HandlerUtils.runWithScissorsForDump(
+                mNsdStateMachine.getHandler(), () -> mMdnsDiscoveryManager.dump(pw), 10_000);
+        pw.decreaseIndent();
     }
 
     private abstract static class ClientRequest {
@@ -2671,6 +2718,10 @@
             return sb.toString();
         }
 
+        public int getUid() {
+            return mUid;
+        }
+
         private boolean isPreSClient() {
             return mIsPreSClient;
         }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index 0b60572..c162bcc 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -449,7 +449,8 @@
             mPendingRegistrations.put(id, registration);
             for (int i = 0; i < mAdvertisers.size(); i++) {
                 try {
-                    mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo());
+                    mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo(),
+                            registration.getAdvertisingOptions());
                 } catch (NameConflictException e) {
                     mSharedLog.wtf("Name conflict adding services that should have unique names",
                             e);
@@ -515,7 +516,7 @@
                 final Registration registration = mPendingRegistrations.valueAt(i);
                 try {
                     advertiser.addService(mPendingRegistrations.keyAt(i),
-                            registration.getServiceInfo());
+                            registration.getServiceInfo(), registration.getAdvertisingOptions());
                 } catch (NameConflictException e) {
                     mSharedLog.wtf("Name conflict adding services that should have unique names",
                             e);
@@ -587,15 +588,17 @@
         @NonNull
         private NsdServiceInfo mServiceInfo;
         final int mClientUid;
+        private final MdnsAdvertisingOptions mAdvertisingOptions;
         int mConflictDuringProbingCount;
         int mConflictAfterProbingCount;
 
-
-        private Registration(@NonNull NsdServiceInfo serviceInfo, int clientUid) {
+        private Registration(@NonNull NsdServiceInfo serviceInfo, int clientUid,
+                @NonNull MdnsAdvertisingOptions advertisingOptions) {
             this.mOriginalServiceName = serviceInfo.getServiceName();
             this.mOriginalHostname = serviceInfo.getHostname();
             this.mServiceInfo = serviceInfo;
             this.mClientUid = clientUid;
+            this.mAdvertisingOptions = advertisingOptions;
         }
 
         /** Check if the new {@link NsdServiceInfo} doesn't update any data other than subtypes. */
@@ -697,6 +700,11 @@
         public NsdServiceInfo getServiceInfo() {
             return mServiceInfo;
         }
+
+        @NonNull
+        public MdnsAdvertisingOptions getAdvertisingOptions() {
+            return mAdvertisingOptions;
+        }
     }
 
     /**
@@ -855,7 +863,7 @@
             }
             mSharedLog.i("Adding service " + service + " with ID " + id + " and subtypes "
                     + subtypes + " advertisingOptions " + advertisingOptions);
-            registration = new Registration(service, clientUid);
+            registration = new Registration(service, clientUid, advertisingOptions);
             final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
             if (network == null) {
                 // If registering on all networks, no advertiser must have conflicts
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
index e7a6ca7..a81d1e4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
@@ -16,6 +16,11 @@
 
 package com.android.server.connectivity.mdns;
 
+import android.annotation.Nullable;
+
+import java.time.Duration;
+import java.util.Objects;
+
 /**
  * API configuration parameters for advertising the mDNS service.
  *
@@ -27,13 +32,15 @@
 
     private static MdnsAdvertisingOptions sDefaultOptions;
     private final boolean mIsOnlyUpdate;
+    @Nullable
+    private final Duration mTtl;
 
     /**
      * Parcelable constructs for a {@link MdnsAdvertisingOptions}.
      */
-    MdnsAdvertisingOptions(
-            boolean isOnlyUpdate) {
+    MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl) {
         this.mIsOnlyUpdate = isOnlyUpdate;
+        this.mTtl = ttl;
     }
 
     /**
@@ -60,9 +67,36 @@
         return mIsOnlyUpdate;
     }
 
+    /**
+     * Returns the TTL for all records in a service.
+     */
+    @Nullable
+    public Duration getTtl() {
+        return mTtl;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (!(other instanceof MdnsAdvertisingOptions)) {
+            return false;
+        } else {
+            final MdnsAdvertisingOptions otherOptions = (MdnsAdvertisingOptions) other;
+            return mIsOnlyUpdate == otherOptions.mIsOnlyUpdate
+                    && Objects.equals(mTtl, otherOptions.mTtl);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mIsOnlyUpdate, mTtl);
+    }
+
     @Override
     public String toString() {
-        return "MdnsAdvertisingOptions{" + "mIsOnlyUpdate=" + mIsOnlyUpdate + '}';
+        return "MdnsAdvertisingOptions{" + "mIsOnlyUpdate=" + mIsOnlyUpdate + ", mTtl=" + mTtl
+                + '}';
     }
 
     /**
@@ -70,6 +104,8 @@
      */
     public static final class Builder {
         private boolean mIsOnlyUpdate = false;
+        @Nullable
+        private Duration mTtl;
 
         private Builder() {
         }
@@ -83,10 +119,18 @@
         }
 
         /**
+         * Sets the TTL duration for all records of the service.
+         */
+        public Builder setTtl(@Nullable Duration ttl) {
+            this.mTtl = ttl;
+            return this;
+        }
+
+        /**
          * Builds a {@link MdnsAdvertisingOptions} with the arguments supplied to this builder.
          */
         public MdnsAdvertisingOptions build() {
-            return new MdnsAdvertisingOptions(mIsOnlyUpdate);
+            return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl);
         }
     }
 }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 1d6039c..21b7069 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -34,6 +34,7 @@
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -363,4 +364,18 @@
                 executorProvider.newServiceTypeClientSchedulerExecutor(), socketKey,
                 sharedLog.forSubComponent(tag), looper, serviceCache);
     }
+
+    /**
+     * Dump DiscoveryManager state.
+     */
+    public void dump(PrintWriter pw) {
+        discoveryExecutor.checkAndRunOnHandlerThread(() -> {
+            pw.println();
+            // Dump ServiceTypeClients
+            for (MdnsServiceTypeClient serviceTypeClient
+                    : perSocketServiceTypeClients.getAllMdnsServiceTypeClient()) {
+                serviceTypeClient.dump(pw);
+            }
+        });
+    }
 }
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index aa51c41..c2363c0 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -258,8 +258,10 @@
      *
      * @throws NameConflictException There is already a service being advertised with that name.
      */
-    public void addService(int id, NsdServiceInfo service) throws NameConflictException {
-        final int replacedExitingService = mRecordRepository.addService(id, service);
+    public void addService(int id, NsdServiceInfo service,
+            @NonNull MdnsAdvertisingOptions advertisingOptions) throws NameConflictException {
+        final int replacedExitingService =
+                mRecordRepository.addService(id, service, advertisingOptions.getTtl());
         // Cancel announcements for the existing service. This only happens for exiting services
         // (so cancelling exiting announcements), as per RecordRepository.addService.
         if (replacedExitingService >= 0) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index ed0bde2..ac64c3a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -45,6 +45,7 @@
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.NetworkInterface;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -75,9 +76,9 @@
     // TTL for records with a host name as the resource record's name (e.g., A, AAAA, HINFO) or a
     // host name contained within the resource record's rdata (e.g., SRV, reverse mapping PTR
     // record)
-    private static final long NAME_RECORDS_TTL_MILLIS = TimeUnit.SECONDS.toMillis(120);
+    private static final long DEFAULT_NAME_RECORDS_TTL_MILLIS = TimeUnit.SECONDS.toMillis(120);
     // TTL for other records
-    private static final long NON_NAME_RECORDS_TTL_MILLIS = TimeUnit.MINUTES.toMillis(75);
+    private static final long DEFAULT_NON_NAME_RECORDS_TTL_MILLIS = TimeUnit.MINUTES.toMillis(75);
 
     // Top-level domain for link-local queries, as per RFC6762 3.
     private static final String LOCAL_TLD = "local";
@@ -193,6 +194,9 @@
          */
         private boolean isProbing;
 
+        @Nullable
+        private Duration ttl;
+
         /**
          * Create a ServiceRegistration with only update the subType.
          */
@@ -200,16 +204,32 @@
             NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
             newServiceInfo.setSubtypes(newSubtypes);
             return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
-                    repliedServiceCount, sentPacketCount, exiting, isProbing);
+                    repliedServiceCount, sentPacketCount, exiting, isProbing, ttl);
         }
 
         /**
          * Create a ServiceRegistration for dns-sd service registration (RFC6763).
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
-                int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing) {
+                int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing,
+                @Nullable Duration ttl) {
             this.serviceInfo = serviceInfo;
 
+            final long nonNameRecordsTtlMillis;
+            final long nameRecordsTtlMillis;
+
+            // When custom TTL is specified, all records of the service will use the custom TTL.
+            // This is typically useful for SRP (Service Registration Protocol:
+            // https://datatracker.ietf.org/doc/html/draft-ietf-dnssd-srp-24) Advertising Proxy
+            // where all records in a single SRP are required the same TTL.
+            if (ttl != null) {
+                nonNameRecordsTtlMillis = ttl.toMillis();
+                nameRecordsTtlMillis = ttl.toMillis();
+            } else {
+                nonNameRecordsTtlMillis = DEFAULT_NON_NAME_RECORDS_TTL_MILLIS;
+                nameRecordsTtlMillis = DEFAULT_NAME_RECORDS_TTL_MILLIS;
+            }
+
             final boolean hasService = !TextUtils.isEmpty(serviceInfo.getServiceType());
             final boolean hasCustomHost = !TextUtils.isEmpty(serviceInfo.getHostname());
             final String[] hostname =
@@ -229,7 +249,7 @@
                                 serviceType,
                                 0L /* receiptTimeMillis */,
                                 false /* cacheFlush */,
-                                NON_NAME_RECORDS_TTL_MILLIS,
+                                nonNameRecordsTtlMillis,
                                 serviceName),
                         true /* sharedName */));
                 for (String subtype : serviceInfo.getSubtypes()) {
@@ -239,7 +259,7 @@
                                     MdnsUtils.constructFullSubtype(serviceType, subtype),
                                     0L /* receiptTimeMillis */,
                                     false /* cacheFlush */,
-                                    NON_NAME_RECORDS_TTL_MILLIS,
+                                    nonNameRecordsTtlMillis,
                                     serviceName),
                             true /* sharedName */));
                 }
@@ -249,7 +269,7 @@
                         new MdnsServiceRecord(serviceName,
                                 0L /* receiptTimeMillis */,
                                 true /* cacheFlush */,
-                                NAME_RECORDS_TTL_MILLIS,
+                                nameRecordsTtlMillis,
                                 0 /* servicePriority */, 0 /* serviceWeight */,
                                 serviceInfo.getPort(),
                                 hostname),
@@ -261,7 +281,7 @@
                                 0L /* receiptTimeMillis */,
                                 // Service name is verified unique after probing
                                 true /* cacheFlush */,
-                                NON_NAME_RECORDS_TTL_MILLIS,
+                                nonNameRecordsTtlMillis,
                                 attrsToTextEntries(serviceInfo.getAttributes())),
                         false /* sharedName */);
 
@@ -275,7 +295,7 @@
                                 DNS_SD_SERVICE_TYPE,
                                 0L /* receiptTimeMillis */,
                                 false /* cacheFlush */,
-                                NON_NAME_RECORDS_TTL_MILLIS,
+                                nonNameRecordsTtlMillis,
                                 serviceType),
                         true /* sharedName */));
             } else {
@@ -292,7 +312,7 @@
                                     new MdnsInetAddressRecord(hostname,
                                             0L /* receiptTimeMillis */,
                                             true /* cacheFlush */,
-                                            NAME_RECORDS_TTL_MILLIS,
+                                            nameRecordsTtlMillis,
                                             address),
                                     false /* sharedName */));
                 }
@@ -315,9 +335,9 @@
          * @param serviceInfo Service to advertise
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
-                int repliedServiceCount, int sentPacketCount) {
+                int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl) {
             this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
-                    false /* exiting */, true /* isProbing */);
+                    false /* exiting */, true /* isProbing */, ttl);
         }
 
         void setProbing(boolean probing) {
@@ -339,7 +359,7 @@
                             revDnsAddr,
                             0L /* receiptTimeMillis */,
                             true /* cacheFlush */,
-                            NAME_RECORDS_TTL_MILLIS,
+                            DEFAULT_NAME_RECORDS_TTL_MILLIS,
                             mDeviceHostname),
                     false /* sharedName */));
 
@@ -349,7 +369,7 @@
                             mDeviceHostname,
                             0L /* receiptTimeMillis */,
                             true /* cacheFlush */,
-                            NAME_RECORDS_TTL_MILLIS,
+                            DEFAULT_NAME_RECORDS_TTL_MILLIS,
                             addr.getAddress()),
                     false /* sharedName */));
         }
@@ -378,11 +398,13 @@
      * This may remove/replace any existing service that used the name added but is exiting.
      * @param serviceId A unique service ID.
      * @param serviceInfo Service info to add.
+     * @param ttl the TTL duration for all records of {@code serviceInfo} or {@code null}
      * @return If the added service replaced another with a matching name (which was exiting), the
      *         ID of the replaced service.
      * @throws NameConflictException There is already a (non-exiting) service using the name.
      */
-    public int addService(int serviceId, NsdServiceInfo serviceInfo) throws NameConflictException {
+    public int addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable Duration ttl)
+            throws NameConflictException {
         if (mServices.contains(serviceId)) {
             throw new IllegalArgumentException(
                     "Service ID must not be reused across registrations: " + serviceId);
@@ -397,7 +419,7 @@
 
         final ServiceRegistration registration = new ServiceRegistration(
                 mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
-                NO_PACKET /* sentPacketCount */);
+                NO_PACKET /* sentPacketCount */, ttl);
         mServices.put(serviceId, registration);
 
         // Remove existing exiting service
@@ -776,7 +798,7 @@
                     true /* cacheFlush */,
                     // TODO: RFC6762 6.1: "In general, the TTL given for an NSEC record SHOULD
                     // be the same as the TTL that the record would have had, had it existed."
-                    NAME_RECORDS_TTL_MILLIS,
+                    DEFAULT_NAME_RECORDS_TTL_MILLIS,
                     question.getName(),
                     new int[] { question.getType() });
             additionalAnswerInfo.add(
@@ -1211,7 +1233,7 @@
         if (existing == null) return null;
 
         final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
-                existing.repliedServiceCount, existing.sentPacketCount);
+                existing.repliedServiceCount, existing.sentPacketCount, existing.ttl);
         mServices.put(serviceId, newService);
         return makeProbingInfo(serviceId, newService);
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
index 78df6df..f60a95e 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -28,6 +28,7 @@
 import com.android.net.module.util.ByteUtils;
 
 import java.nio.charset.Charset;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -62,7 +63,8 @@
                             source.createStringArrayList(),
                             source.createTypedArrayList(TextEntry.CREATOR),
                             source.readInt(),
-                            source.readParcelable(null));
+                            source.readParcelable(Network.class.getClassLoader()),
+                            Instant.ofEpochSecond(source.readLong()));
                 }
 
                 @Override
@@ -89,6 +91,9 @@
     @Nullable
     private final Network network;
 
+    @NonNull
+    private final Instant expirationTime;
+
     /** Constructs a {@link MdnsServiceInfo} object with default values. */
     public MdnsServiceInfo(
             String serviceInstanceName,
@@ -110,7 +115,8 @@
                 textStrings,
                 /* textEntries= */ null,
                 /* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
-                /* network= */ null);
+                /* network= */ null,
+                /* expirationTime= */ Instant.MAX);
     }
 
     /** Constructs a {@link MdnsServiceInfo} object with default values. */
@@ -135,7 +141,8 @@
                 textStrings,
                 textEntries,
                 /* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
-                /* network= */ null);
+                /* network= */ null,
+                /* expirationTime= */ Instant.MAX);
     }
 
     /**
@@ -165,7 +172,8 @@
                 textStrings,
                 textEntries,
                 interfaceIndex,
-                /* network= */ null);
+                /* network= */ null,
+                /* expirationTime= */ Instant.MAX);
     }
 
     /**
@@ -184,7 +192,8 @@
             @Nullable List<String> textStrings,
             @Nullable List<TextEntry> textEntries,
             int interfaceIndex,
-            @Nullable Network network) {
+            @Nullable Network network,
+            @NonNull Instant expirationTime) {
         this.serviceInstanceName = serviceInstanceName;
         this.serviceType = serviceType;
         this.subtypes = new ArrayList<>();
@@ -217,6 +226,7 @@
         this.attributes = Collections.unmodifiableMap(attributes);
         this.interfaceIndex = interfaceIndex;
         this.network = network;
+        this.expirationTime = Instant.ofEpochSecond(expirationTime.getEpochSecond());
     }
 
     private static List<TextEntry> parseTextStrings(List<String> textStrings) {
@@ -314,6 +324,17 @@
     }
 
     /**
+     * Returns the timestamp after when this service is expired or {@code null} if the expiration
+     * time is unknown.
+     *
+     * A service is considered expired if any of its DNS record is expired.
+     */
+    @NonNull
+    public Instant getExpirationTime() {
+        return expirationTime;
+    }
+
+    /**
      * Returns attribute value for {@code key} as a UTF-8 string. It's the caller who must make sure
      * that the value of {@code key} is indeed a UTF-8 string. {@code null} will be returned if no
      * attribute value exists for {@code key}.
@@ -364,6 +385,7 @@
         out.writeTypedList(textEntries);
         out.writeInt(interfaceIndex);
         out.writeParcelable(network, 0);
+        out.writeLong(expirationTime.getEpochSecond());
     }
 
     @Override
@@ -377,7 +399,8 @@
                 + ", interfaceIndex: " + interfaceIndex
                 + ", network: " + network
                 + ", textStrings: " + textStrings
-                + ", textEntries: " + textEntries;
+                + ", textEntries: " + textEntries
+                + ", expirationTime: " + expirationTime;
     }
 
 
@@ -496,4 +519,4 @@
             out.writeByteArray(value);
         }
     }
-}
\ No newline at end of file
+}
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 e222fcf..8f41b94 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -35,8 +35,10 @@
 import com.android.net.module.util.SharedLog;
 import com.android.server.connectivity.mdns.util.MdnsUtils;
 
+import java.io.PrintWriter;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -308,6 +310,7 @@
             textStrings = response.getTextRecord().getStrings();
             textEntries = response.getTextRecord().getEntries();
         }
+        Instant now = Instant.now();
         // TODO: Throw an error message if response doesn't have Inet6 or Inet4 address.
         return new MdnsServiceInfo(
                 serviceInstanceName,
@@ -320,7 +323,8 @@
                 textStrings,
                 textEntries,
                 response.getInterfaceIndex(),
-                response.getNetwork());
+                response.getNetwork(),
+                now.plusMillis(response.getMinRemainingTtl(now.toEpochMilli())));
     }
 
     /**
@@ -756,4 +760,13 @@
                 args.sessionId, timeToNextTasksWithBackoffInMs));
         return timeToNextTasksWithBackoffInMs;
     }
+
+    /**
+     * Dump ServiceTypeClient state.
+     */
+    public void dump(PrintWriter pw) {
+        ensureRunningOnHandlerThread(handler);
+        pw.println("ServiceTypeClient: Type{" + serviceType + "} " + socketKey + " with "
+                + listeners.size() + " listeners.");
+    }
 }
\ No newline at end of file
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 80c4033..9684d18 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -2231,7 +2231,7 @@
                             .setDefaultNetwork(true)
                             .setOemManaged(ident.getOemManaged())
                             .setSubId(ident.getSubId()).build();
-                    final String ifaceVt = IFACE_VT + getSubIdForMobile(snapshot);
+                    final String ifaceVt = IFACE_VT + getSubIdForCellularOrSatellite(snapshot);
                     findOrCreateNetworkIdentitySet(mActiveIfaces, ifaceVt).add(vtIdent);
                     findOrCreateNetworkIdentitySet(mActiveUidIfaces, ifaceVt).add(vtIdent);
                 }
@@ -2300,9 +2300,15 @@
         mMobileIfaces = mobileIfaces.toArray(new String[0]);
     }
 
-    private static int getSubIdForMobile(@NonNull NetworkStateSnapshot state) {
-        if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
-            throw new IllegalArgumentException("Mobile state need capability TRANSPORT_CELLULAR");
+    private static int getSubIdForCellularOrSatellite(@NonNull NetworkStateSnapshot state) {
+        if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
+                // Both cellular and satellite are 2 different network transport at Mobile using
+                // same telephony network specifier. So adding satellite transport to consider
+                // for, when satellite network is active at mobile.
+                && !state.getNetworkCapabilities().hasTransport(
+                NetworkCapabilities.TRANSPORT_SATELLITE)) {
+            throw new IllegalArgumentException(
+                    "Mobile state need capability TRANSPORT_CELLULAR or TRANSPORT_SATELLITE");
         }
 
         final NetworkSpecifier spec = state.getNetworkCapabilities().getNetworkSpecifier();
diff --git a/service/Android.bp b/service/Android.bp
index 403ba7d..c35c4f8 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -199,6 +199,7 @@
         "PlatformProperties",
         "service-connectivity-protos",
         "service-connectivity-stats-protos",
+        "net-utils-multicast-forwarding-structs",
     ],
     apex_available: [
         "com.android.tethering",
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 47e897d..6790093 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -188,6 +188,33 @@
     },
 }
 
+// The net-utils-multicast-forwarding-structs library requires the callers to
+// contain net-utils-device-common-bpf.
+java_library {
+    name: "net-utils-multicast-forwarding-structs",
+    srcs: [
+        "device/com/android/net/module/util/structs/StructMf6cctl.java",
+        "device/com/android/net/module/util/structs/StructMif6ctl.java",
+        "device/com/android/net/module/util/structs/StructMrt6Msg.java",
+    ],
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
+    libs: [
+        // Only Struct.java is needed from "net-utils-device-common-bpf"
+        "net-utils-device-common-bpf",
+    ],
+    apex_available: [
+        "com.android.tethering",
+    ],
+    lint: {
+        strict_updatability_linting: true,
+        error_checks: ["NewApi"],
+    },
+}
+
 // The net-utils-device-common-netlink library requires the callers to contain
 // net-utils-device-common-struct.
 java_library {
diff --git a/tests/integration/AndroidManifest.xml b/tests/integration/AndroidManifest.xml
index cea83c7..1821329 100644
--- a/tests/integration/AndroidManifest.xml
+++ b/tests/integration/AndroidManifest.xml
@@ -42,6 +42,9 @@
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
     <!-- Register UidFrozenStateChangedCallback -->
     <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
+    <!-- Permission required for CTS test - NetworkStatsIntegrationTest -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/>
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner"/>
 
diff --git a/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
new file mode 100644
index 0000000..765e56e
--- /dev/null
+++ b/tests/integration/src/com/android/server/net/integrationtests/NetworkStatsIntegrationTest.kt
@@ -0,0 +1,597 @@
+/*
+ * Copyright (C) 2023 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.integrationtests
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.annotation.TargetApi
+import android.app.usage.NetworkStats
+import android.app.usage.NetworkStats.Bucket
+import android.app.usage.NetworkStats.Bucket.TAG_NONE
+import android.app.usage.NetworkStatsManager
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.TYPE_TEST
+import android.net.InetAddresses
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.NetworkTemplate
+import android.net.NetworkTemplate.MATCH_TEST
+import android.net.TestNetworkSpecifier
+import android.net.TrafficStats
+import android.os.Build
+import android.os.Process
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.net.integrationtests.NetworkStatsIntegrationTest.Direction.DOWNLOAD
+import com.android.server.net.integrationtests.NetworkStatsIntegrationTest.Direction.UPLOAD
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.PacketBridge
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.TestDnsServer
+import com.android.testutils.TestHttpServer
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
+import fi.iki.elonen.NanoHTTPD
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.net.HttpURLConnection
+import java.net.HttpURLConnection.HTTP_OK
+import java.net.InetSocketAddress
+import java.net.URL
+import java.nio.charset.Charset
+import kotlin.math.ceil
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TEST_TAG = 0xF00D
+
+@RunWith(DevSdkIgnoreRunner::class)
+@TargetApi(Build.VERSION_CODES.S)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class NetworkStatsIntegrationTest {
+    private val TAG = NetworkStatsIntegrationTest::class.java.simpleName
+    private val LOCAL_V6ADDR =
+        LinkAddress(InetAddresses.parseNumericAddress("2001:db8::1234"), 64)
+
+    // Remote address, both the client and server will have a hallucination that
+    // they are talking to this address.
+    private val REMOTE_V6ADDR =
+        LinkAddress(InetAddresses.parseNumericAddress("dead:beef::808:808"), 64)
+    private val REMOTE_V4ADDR =
+        LinkAddress(InetAddresses.parseNumericAddress("8.8.8.8"), 32)
+    private val DEFAULT_MTU = 1500
+    private val DEFAULT_BUFFER_SIZE = 1500 // Any size greater than or equal to mtu
+    private val CONNECTION_TIMEOUT_MILLIS = 15000
+    private val TEST_DOWNLOAD_SIZE = 10000L
+    private val TEST_UPLOAD_SIZE = 20000L
+    private val HTTP_SERVER_NAME = "test.com"
+    private val HTTP_SERVER_PORT = 8080 // Use port > 1024 to avoid restrictions on system ports
+    private val DNS_INTERNAL_SERVER_PORT = 53
+    private val DNS_EXTERNAL_SERVER_PORT = 1053
+    private val TCP_ACK_SIZE = 72
+
+    // Packet overheads that are not part of the actual data transmission, these
+    // include DNS packets, TCP handshake/termination packets, and HTTP header
+    // packets. These overheads were gathered from real samples and may not
+    // be perfectly accurate because of DNS caches and TCP retransmissions, etc.
+    private val CONSTANT_PACKET_OVERHEAD = 8
+
+    // 130 is an observed average.
+    private val CONSTANT_BYTES_OVERHEAD = 130 * CONSTANT_PACKET_OVERHEAD
+    private val TOLERANCE = 1.3
+
+    // Set up the packet bridge with two IPv6 address only test networks.
+    private val inst = InstrumentationRegistry.getInstrumentation()
+    private val context = inst.getContext()
+    private val packetBridge = runAsShell(MANAGE_TEST_NETWORKS) {
+        PacketBridge(
+            context,
+            listOf(LOCAL_V6ADDR),
+            REMOTE_V6ADDR.address,
+            listOf(
+                Pair(DNS_INTERNAL_SERVER_PORT, DNS_EXTERNAL_SERVER_PORT)
+            )
+        )
+    }
+    private val cm = context.getSystemService(ConnectivityManager::class.java)!!
+
+    // Set up DNS server for testing server and DNS64.
+    private val fakeDns = TestDnsServer(
+        packetBridge.externalNetwork,
+        InetSocketAddress(LOCAL_V6ADDR.address, DNS_EXTERNAL_SERVER_PORT)
+    ).apply {
+        start()
+        setAnswer(
+            "ipv4only.arpa",
+            listOf(IpPrefix(REMOTE_V6ADDR.address, REMOTE_V6ADDR.prefixLength).address)
+        )
+        setAnswer(HTTP_SERVER_NAME, listOf(REMOTE_V4ADDR.address))
+    }
+
+    // Start up test http server.
+    private val httpServer = TestHttpServer(
+        LOCAL_V6ADDR.address.hostAddress,
+        HTTP_SERVER_PORT
+    ).apply {
+        start()
+    }
+
+    @Before
+    fun setUp() {
+        assumeTrue(shouldRunTests())
+        packetBridge.start()
+    }
+
+    // For networkstack tests, it is not guaranteed that the tethering module will be
+    // updated at the same time. If the tethering module is not new enough, it may not contain
+    // the necessary abilities to run these tests. For example, The tests depends on test
+    // network stats being counted, which can only be achieved when they are marked as TYPE_TEST.
+    // If the tethering module does not support TYPE_TEST stats, then these tests will need
+    // to be skipped.
+    fun shouldRunTests() = cm.getNetworkInfo(packetBridge.internalNetwork)!!.type == TYPE_TEST
+
+    @After
+    fun tearDown() {
+        packetBridge.stop()
+        fakeDns.stop()
+        httpServer.stop()
+    }
+
+    private fun waitFor464XlatReady(network: Network): String {
+        val iface = cm.getLinkProperties(network)!!.interfaceName!!
+
+        // Make a network request to listen to the specific test network.
+        val nr = NetworkRequest.Builder()
+            .clearCapabilities()
+            .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+            .setNetworkSpecifier(TestNetworkSpecifier(iface))
+            .build()
+        val testCb = TestableNetworkCallback()
+        cm.registerNetworkCallback(nr, testCb)
+
+        // Wait for the stacked address to be available.
+        testCb.eventuallyExpect<LinkPropertiesChanged> {
+            it.lp.stackedLinks.getOrNull(0)?.linkAddresses?.getOrNull(0) != null
+        }
+
+        return iface
+    }
+
+    private val Network.mtu: Int get() {
+        val lp = cm.getLinkProperties(this)!!
+        val mtuStacked = if (lp.stackedLinks[0]?.mtu != 0) lp.stackedLinks[0].mtu else DEFAULT_MTU
+        val mtuInterface = if (lp.mtu != 0) lp.mtu else DEFAULT_MTU
+        return mtuInterface.coerceAtMost(mtuStacked)
+    }
+
+    /**
+     * Verify data usage download stats with test 464xlat networks.
+     *
+     * This test starts two test networks and binds them together, the internal one is for the
+     * client to make http traffic on the test network, and the external one is for the mocked
+     * http and dns server to bind to and provide responses.
+     *
+     * After Clat setup, the client will use clat v4 address to send packets to the mocked
+     * server v4 address, which will be translated into a v6 packet by the clat daemon with
+     * NAT64 prefix learned from the mocked DNS64 response. And send to the interface.
+     *
+     * While the packets are being forwarded to the external interface, the servers will see
+     * the packets originated from the mocked v6 address, and destined to a local v6 address.
+     */
+    @Test
+    fun test464XlatTcpStats() {
+        // Wait for 464Xlat to be ready.
+        val internalInterfaceName = waitFor464XlatReady(packetBridge.internalNetwork)
+        val mtu = packetBridge.internalNetwork.mtu
+
+        val snapshotBeforeTest = StatsSnapshot(context, internalInterfaceName)
+
+        // Generate the download traffic.
+        genHttpTraffic(packetBridge.internalNetwork, uploadSize = 0L, TEST_DOWNLOAD_SIZE)
+
+        // In practice, for one way 10k download payload, the download usage is about
+        // 11222~12880 bytes, with 14~17 packets. And the upload usage is about 1279~1626 bytes
+        // with 14~17 packets, which is majorly contributed by TCP ACK packets.
+        val snapshotAfterDownload = StatsSnapshot(context, internalInterfaceName)
+        val (expectedDownloadLower, expectedDownloadUpper) = getExpectedStatsBounds(
+            TEST_DOWNLOAD_SIZE,
+            mtu,
+            DOWNLOAD
+        )
+        assertOnlyNonTaggedStatsIncreases(
+            snapshotBeforeTest,
+            snapshotAfterDownload,
+            expectedDownloadLower,
+            expectedDownloadUpper
+        )
+
+        // Generate upload traffic with tag to verify tagged data accounting as well.
+        genHttpTrafficWithTag(
+            packetBridge.internalNetwork,
+            TEST_UPLOAD_SIZE,
+            downloadSize = 0L,
+            TEST_TAG
+        )
+
+        // Verify upload data usage accounting.
+        val snapshotAfterUpload = StatsSnapshot(context, internalInterfaceName)
+        val (expectedUploadLower, expectedUploadUpper) = getExpectedStatsBounds(
+            TEST_UPLOAD_SIZE,
+            mtu,
+            UPLOAD
+        )
+        assertAllStatsIncreases(
+            snapshotAfterDownload,
+            snapshotAfterUpload,
+            expectedUploadLower,
+            expectedUploadUpper
+        )
+    }
+
+    private enum class Direction {
+        DOWNLOAD,
+        UPLOAD
+    }
+
+    private fun getExpectedStatsBounds(
+        transmittedSize: Long,
+        mtu: Int,
+        direction: Direction
+    ): Pair<BareStats, BareStats> {
+        // This is already an underestimated value since the input doesn't include TCP/IP
+        // layer overhead.
+        val txBytesLower = transmittedSize
+        // Include TCP/IP header overheads and retransmissions in the upper bound.
+        val txBytesUpper = (transmittedSize * TOLERANCE).toLong()
+        val txPacketsLower = txBytesLower / mtu + (CONSTANT_PACKET_OVERHEAD / TOLERANCE).toLong()
+        val estTransmissionPacketsUpper = ceil(txBytesUpper / mtu.toDouble()).toLong()
+        val txPacketsUpper = estTransmissionPacketsUpper +
+                (CONSTANT_PACKET_OVERHEAD * TOLERANCE).toLong()
+        // Assume ACK only sent once for the entire transmission.
+        val rxPacketsLower = 1L + (CONSTANT_PACKET_OVERHEAD / TOLERANCE).toLong()
+        // Assume ACK sent for every RX packet.
+        val rxPacketsUpper = txPacketsUpper
+        val rxBytesLower = 1L * TCP_ACK_SIZE + (CONSTANT_BYTES_OVERHEAD / TOLERANCE).toLong()
+        val rxBytesUpper = estTransmissionPacketsUpper * TCP_ACK_SIZE +
+                (CONSTANT_BYTES_OVERHEAD * TOLERANCE).toLong()
+
+        return if (direction == UPLOAD) {
+            BareStats(rxBytesLower, rxPacketsLower, txBytesLower, txPacketsLower) to
+                    BareStats(rxBytesUpper, rxPacketsUpper, txBytesUpper, txPacketsUpper)
+        } else {
+            BareStats(txBytesLower, txPacketsLower, rxBytesLower, rxPacketsLower) to
+                    BareStats(txBytesUpper, txPacketsUpper, rxBytesUpper, rxPacketsUpper)
+        }
+    }
+
+    private fun genHttpTraffic(network: Network, uploadSize: Long, downloadSize: Long) =
+        genHttpTrafficWithTag(network, uploadSize, downloadSize, NetworkStats.Bucket.TAG_NONE)
+
+    private fun genHttpTrafficWithTag(
+        network: Network,
+        uploadSize: Long,
+        downloadSize: Long,
+        tag: Int
+    ) {
+        val path = "/test_upload_download"
+        val buf = ByteArray(DEFAULT_BUFFER_SIZE)
+
+        httpServer.addResponse(
+            TestHttpServer.Request(path, NanoHTTPD.Method.POST), NanoHTTPD.Response.Status.OK,
+            content = getRandomString(downloadSize)
+        )
+        var httpConnection: HttpURLConnection? = null
+        try {
+            TrafficStats.setThreadStatsTag(tag)
+            val spec = "http://$HTTP_SERVER_NAME:${httpServer.listeningPort}$path"
+            val url = URL(spec)
+            httpConnection = network.openConnection(url) as HttpURLConnection
+            httpConnection.connectTimeout = CONNECTION_TIMEOUT_MILLIS
+            httpConnection.requestMethod = "POST"
+            httpConnection.doOutput = true
+            // Tell the server that the response should not be compressed. Otherwise, the data usage
+            // accounted will be less than expected.
+            httpConnection.setRequestProperty("Accept-Encoding", "identity")
+            // Tell the server that to close connection after this request, this is needed to
+            // prevent from reusing the same socket that has different tagging requirement.
+            httpConnection.setRequestProperty("Connection", "close")
+
+            // Send http body.
+            val outputStream = BufferedOutputStream(httpConnection.outputStream)
+            outputStream.write(getRandomString(uploadSize).toByteArray(Charset.forName("UTF-8")))
+            outputStream.close()
+            assertEquals(HTTP_OK, httpConnection.responseCode)
+
+            // Receive response from the server.
+            val inputStream = BufferedInputStream(httpConnection.getInputStream())
+            var total = 0L
+            while (true) {
+                val count = inputStream.read(buf)
+                if (count == -1) break // End-of-Stream
+                total += count
+            }
+            assertEquals(downloadSize, total)
+        } finally {
+            httpConnection?.inputStream?.close()
+            TrafficStats.clearThreadStatsTag()
+        }
+    }
+
+    // NetworkStats.Bucket cannot be written. So another class is needed to
+    // perform arithmetic operations.
+    data class BareStats(
+        val rxBytes: Long,
+        val rxPackets: Long,
+        val txBytes: Long,
+        val txPackets: Long
+    ) {
+        operator fun plus(other: BareStats): BareStats {
+            return BareStats(
+                this.rxBytes + other.rxBytes, this.rxPackets + other.rxPackets,
+                this.txBytes + other.txBytes, this.txPackets + other.txPackets
+            )
+        }
+
+        operator fun minus(other: BareStats): BareStats {
+            return BareStats(
+                this.rxBytes - other.rxBytes, this.rxPackets - other.rxPackets,
+                this.txBytes - other.txBytes, this.txPackets - other.txPackets
+            )
+        }
+
+        fun reverse(): BareStats =
+            BareStats(
+                rxBytes = txBytes,
+                rxPackets = txPackets,
+                txBytes = rxBytes,
+                txPackets = rxPackets
+            )
+
+        override fun toString(): String {
+            return "BareStats{rx/txBytes=$rxBytes/$txBytes, rx/txPackets=$rxPackets/$txPackets}"
+        }
+
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is BareStats) return false
+
+            if (rxBytes != other.rxBytes) return false
+            if (rxPackets != other.rxPackets) return false
+            if (txBytes != other.txBytes) return false
+            if (txPackets != other.txPackets) return false
+
+            return true
+        }
+
+        override fun hashCode(): Int {
+            return (rxBytes * 11 + rxPackets * 13 + txBytes * 17 + txPackets * 19).toInt()
+        }
+
+        companion object {
+            val EMPTY = BareStats(0L, 0L, 0L, 0L)
+        }
+    }
+
+    data class StatsSnapshot(val context: Context, val iface: String) {
+        val statsSummary = getNetworkSummary(iface)
+        val statsUid = getUidDetail(iface, TAG_NONE)
+        val taggedSummary = getTaggedNetworkSummary(iface, TEST_TAG)
+        val taggedUid = getUidDetail(iface, TEST_TAG)
+        val trafficStatsIface = getTrafficStatsIface(iface)
+        val trafficStatsUid = getTrafficStatsUid(Process.myUid())
+
+        private fun getUidDetail(iface: String, tag: Int): BareStats {
+            return getNetworkStatsThat(iface, tag) { nsm, template ->
+                nsm.queryDetailsForUidTagState(
+                    template, Long.MIN_VALUE, Long.MAX_VALUE,
+                    Process.myUid(), tag, Bucket.STATE_ALL
+                )
+            }
+        }
+
+        private fun getNetworkSummary(iface: String): BareStats {
+            return getNetworkStatsThat(iface, TAG_NONE) { nsm, template ->
+                nsm.querySummary(template, Long.MIN_VALUE, Long.MAX_VALUE)
+            }
+        }
+
+        private fun getTaggedNetworkSummary(iface: String, tag: Int): BareStats {
+            return getNetworkStatsThat(iface, tag) { nsm, template ->
+                nsm.queryTaggedSummary(template, Long.MIN_VALUE, Long.MAX_VALUE)
+            }
+        }
+
+        private fun getNetworkStatsThat(
+            iface: String,
+            tag: Int,
+            queryApi: (nsm: NetworkStatsManager, template: NetworkTemplate) -> NetworkStats
+        ): BareStats {
+            val nsm = context.getSystemService(NetworkStatsManager::class.java)!!
+            nsm.forceUpdate()
+            val testTemplate = NetworkTemplate.Builder(MATCH_TEST)
+                .setWifiNetworkKeys(setOf(iface)).build()
+            val stats = queryApi.invoke(nsm, testTemplate)
+            val filteredBuckets =
+                stats.buckets().filter { it.uid == Process.myUid() && it.tag == tag }
+            return filteredBuckets.fold(BareStats.EMPTY) { acc, it ->
+                acc + BareStats(
+                    it.rxBytes,
+                    it.rxPackets,
+                    it.txBytes,
+                    it.txPackets
+                )
+            }
+        }
+
+        // Helper function to iterate buckets in app.usage.NetworkStats.
+        private fun NetworkStats.buckets() = object : Iterable<NetworkStats.Bucket> {
+            override fun iterator() = object : Iterator<NetworkStats.Bucket> {
+                override operator fun hasNext() = hasNextBucket()
+                override operator fun next() =
+                    NetworkStats.Bucket().also { assertTrue(getNextBucket(it)) }
+            }
+        }
+
+        private fun getTrafficStatsIface(iface: String): BareStats = BareStats(
+            TrafficStats.getRxBytes(iface),
+            TrafficStats.getRxPackets(iface),
+            TrafficStats.getTxBytes(iface),
+            TrafficStats.getTxPackets(iface)
+        )
+
+        private fun getTrafficStatsUid(uid: Int): BareStats = BareStats(
+            TrafficStats.getUidRxBytes(uid),
+            TrafficStats.getUidRxPackets(uid),
+            TrafficStats.getUidTxBytes(uid),
+            TrafficStats.getUidTxPackets(uid)
+        )
+    }
+
+    private fun assertAllStatsIncreases(
+        before: StatsSnapshot,
+        after: StatsSnapshot,
+        lower: BareStats,
+        upper: BareStats
+    ) {
+        assertNonTaggedStatsIncreases(before, after, lower, upper)
+        assertTaggedStatsIncreases(before, after, lower, upper)
+    }
+
+    private fun assertOnlyNonTaggedStatsIncreases(
+        before: StatsSnapshot,
+        after: StatsSnapshot,
+        lower: BareStats,
+        upper: BareStats
+    ) {
+        assertNonTaggedStatsIncreases(before, after, lower, upper)
+        assertTaggedStatsEquals(before, after)
+    }
+
+    private fun assertNonTaggedStatsIncreases(
+        before: StatsSnapshot,
+        after: StatsSnapshot,
+        lower: BareStats,
+        upper: BareStats
+    ) {
+        assertInRange(
+            "Unexpected iface traffic stats",
+            after.iface,
+            before.trafficStatsIface, after.trafficStatsIface,
+            lower, upper
+        )
+        // Uid traffic stats are counted in both direction because the external network
+        // traffic is also attributed to the test uid.
+        assertInRange(
+            "Unexpected uid traffic stats",
+            after.iface,
+            before.trafficStatsUid, after.trafficStatsUid,
+            lower + lower.reverse(), upper + upper.reverse()
+        )
+        assertInRange(
+            "Unexpected non-tagged summary stats",
+            after.iface,
+            before.statsSummary, after.statsSummary,
+            lower, upper
+        )
+        assertInRange(
+            "Unexpected non-tagged uid stats",
+            after.iface,
+            before.statsUid, after.statsUid,
+            lower, upper
+        )
+    }
+
+    private fun assertTaggedStatsEquals(before: StatsSnapshot, after: StatsSnapshot) {
+        // Increment of tagged data should be zero since no tagged traffic was generated.
+        assertEquals(
+            before.taggedSummary,
+            after.taggedSummary,
+            "Unexpected tagged summary stats: ${after.iface}"
+        )
+        assertEquals(
+            before.taggedUid,
+            after.taggedUid,
+            "Unexpected tagged uid stats: ${Process.myUid()} on ${after.iface}"
+        )
+    }
+
+    private fun assertTaggedStatsIncreases(
+        before: StatsSnapshot,
+        after: StatsSnapshot,
+        lower: BareStats,
+        upper: BareStats
+    ) {
+        assertInRange(
+            "Unexpected tagged summary stats",
+            after.iface,
+            before.taggedSummary, after.taggedSummary,
+            lower,
+            upper
+        )
+        assertInRange(
+            "Unexpected tagged uid stats: ${Process.myUid()}",
+            after.iface,
+            before.taggedUid, after.taggedUid,
+            lower,
+            upper
+        )
+    }
+
+    /** Verify the given BareStats is in range [lower, upper] */
+    private fun assertInRange(
+        tag: String,
+        iface: String,
+        before: BareStats,
+        after: BareStats,
+        lower: BareStats,
+        upper: BareStats
+    ) {
+        // Passing the value after operation and the value before operation to dump the actual
+        // numbers if it fails.
+        assertTrue(checkInRange(before, after, lower, upper),
+            "$tag on $iface: $after - $before is not within range [$lower, $upper]"
+        )
+    }
+
+    private fun checkInRange(
+            before: BareStats,
+            after: BareStats,
+            lower: BareStats,
+            upper: BareStats
+    ): Boolean {
+        val value = after - before
+        return value.rxBytes in lower.rxBytes..upper.rxBytes &&
+                value.rxPackets in lower.rxPackets..upper.rxPackets &&
+                value.txBytes in lower.txBytes..upper.txBytes &&
+                value.txPackets in lower.txPackets..upper.txPackets
+    }
+
+    fun getRandomString(length: Long): String {
+        val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
+        return (1..length)
+            .map { allowedChars.random() }
+            .joinToString("")
+    }
+}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 4a1298f..20d457f 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -66,13 +66,11 @@
         "java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt",
         "java/com/android/internal/net/NetworkUtilsInternalTest.java",
         "java/com/android/internal/net/VpnProfileTest.java",
-        "java/com/android/server/VpnManagerServiceTest.java",
         "java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java",
         "java/com/android/server/connectivity/IpConnectivityMetricsTest.java",
         "java/com/android/server/connectivity/MetricsTestUtil.java",
         "java/com/android/server/connectivity/MultipathPolicyTrackerTest.java",
         "java/com/android/server/connectivity/NetdEventListenerServiceTest.java",
-        "java/com/android/server/connectivity/VpnTest.java",
         "java/com/android/server/net/ipmemorystore/*.java",
     ],
 }
diff --git a/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
new file mode 100644
index 0000000..332f2a3
--- /dev/null
+++ b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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 android.net.nsd
+
+import android.net.nsd.AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY
+import android.net.nsd.NsdManager.PROTOCOL_DNS_SD
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.parcelingRoundTrip
+import java.time.Duration
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// TODO: move this class to CTS tests when AdvertisingRequest is made public
+/** Unit tests for {@link AdvertisingRequest}. */
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@ConnectivityModuleTest
+class AdvertisingRequestTest {
+    @Test
+    fun testParcelingIsLossLess() {
+        val info = NsdServiceInfo().apply {
+            serviceType = "_ipp._tcp"
+        }
+        val beforeParcel = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+                .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+                .setTtl(Duration.ofSeconds(30L))
+                .build()
+
+        val afterParcel = parcelingRoundTrip(beforeParcel)
+
+        assertEquals(beforeParcel.serviceInfo.serviceType, afterParcel.serviceInfo.serviceType)
+        assertEquals(beforeParcel.advertisingConfig, afterParcel.advertisingConfig)
+    }
+
+@Test
+fun testBuilder_setNullTtl_success() {
+    val info = NsdServiceInfo().apply {
+        serviceType = "_ipp._tcp"
+    }
+    val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+            .setTtl(null)
+            .build()
+
+    assertNull(request.ttl)
+}
+
+    @Test
+    fun testBuilder_setPropertiesSuccess() {
+        val info = NsdServiceInfo().apply {
+            serviceType = "_ipp._tcp"
+        }
+        val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+                .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+                .setTtl(Duration.ofSeconds(100L))
+                .build()
+
+        assertEquals("_ipp._tcp", request.serviceInfo.serviceType)
+        assertEquals(PROTOCOL_DNS_SD, request.protocolType)
+        assertEquals(NSD_ADVERTISING_UPDATE_ONLY, request.advertisingConfig)
+        assertEquals(Duration.ofSeconds(100L), request.ttl)
+    }
+
+    @Test
+    fun testEquality() {
+        val info = NsdServiceInfo().apply {
+            serviceType = "_ipp._tcp"
+        }
+        val request1 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build()
+        val request2 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build()
+        val request3 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+                .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+                .setTtl(Duration.ofSeconds(120L))
+                .build()
+        val request4 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD)
+                .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY)
+                .setTtl(Duration.ofSeconds(120L))
+                .build()
+
+        assertEquals(request1, request2)
+        assertEquals(request3, request4)
+        assertNotEquals(request1, request3)
+        assertNotEquals(request2, request4)
+    }
+}
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 951675c..76a649e 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -23,6 +23,7 @@
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -54,6 +55,7 @@
 
 import java.net.InetAddress;
 import java.util.List;
+import java.time.Duration;
 
 @DevSdkIgnoreRunner.MonitorThreadLeak
 @RunWith(DevSdkIgnoreRunner.class)
@@ -224,6 +226,23 @@
         verify(listener, timeout(mTimeoutMs).times(1)).onServiceRegistered(request);
     }
 
+    @Test
+    public void testRegisterServiceWithCustomTtl() throws Exception {
+        final NsdManager manager = mManager;
+        final NsdServiceInfo info = new NsdServiceInfo("another_name2", "another_type2");
+        info.setPort(2203);
+        final AdvertisingRequest request = new AdvertisingRequest.Builder(info, PROTOCOL)
+                .setTtl(Duration.ofSeconds(30)).build();
+        final NsdManager.RegistrationListener listener = mock(
+                NsdManager.RegistrationListener.class);
+
+        manager.registerService(request, Runnable::run, listener);
+
+        AdvertisingRequest capturedRequest = getAdvertisingRequest(
+                req -> verify(mServiceConn).registerService(anyInt(), req.capture()));
+        assertEquals(request, capturedRequest);
+    }
+
     private void doTestRegisterService() throws Exception {
         NsdManager manager = mManager;
 
@@ -501,4 +520,12 @@
         verifier.accept(captor);
         return captor.getValue();
     }
+
+    AdvertisingRequest getAdvertisingRequest(
+            ThrowingConsumer<ArgumentCaptor<AdvertisingRequest>> verifier) throws Exception {
+        final ArgumentCaptor<AdvertisingRequest> captor =
+                ArgumentCaptor.forClass(AdvertisingRequest.class);
+        verifier.accept(captor);
+        return captor.getValue();
+    }
 }
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 624855e..881de56 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -83,6 +83,7 @@
 import android.net.mdns.aidl.IMDnsEventListener;
 import android.net.mdns.aidl.RegistrationInfo;
 import android.net.mdns.aidl.ResolutionInfo;
+import android.net.nsd.AdvertisingRequest;
 import android.net.nsd.INsdManagerCallback;
 import android.net.nsd.INsdServiceConnector;
 import android.net.nsd.MDnsManager;
@@ -101,6 +102,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Process;
 import android.os.RemoteException;
 import android.util.Pair;
 
@@ -110,6 +112,7 @@
 import com.android.metrics.NetworkNsdReportedMetrics;
 import com.android.server.NsdService.Dependencies;
 import com.android.server.connectivity.mdns.MdnsAdvertiser;
+import com.android.server.connectivity.mdns.MdnsAdvertisingOptions;
 import com.android.server.connectivity.mdns.MdnsDiscoveryManager;
 import com.android.server.connectivity.mdns.MdnsInterfaceSocket;
 import com.android.server.connectivity.mdns.MdnsSearchOptions;
@@ -137,6 +140,8 @@
 
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedList;
@@ -971,7 +976,8 @@
                 List.of() /* textStrings */,
                 List.of() /* textEntries */,
                 1234,
-                network);
+                network,
+                Instant.MAX /* expirationTime */);
 
         // Callbacks for query sent.
         listener.onDiscoveryQuerySent(Collections.emptyList(), 1 /* transactionId */);
@@ -1001,7 +1007,8 @@
                 List.of() /* textStrings */,
                 List.of() /* textEntries */,
                 1234,
-                network);
+                network,
+                Instant.MAX /* expirationTime */);
 
         // Verify onServiceUpdated callback.
         listener.onServiceUpdated(updatedServiceInfo);
@@ -1133,7 +1140,8 @@
                 List.of(), /* textStrings */
                 List.of(), /* textEntries */
                 1234, /* interfaceIndex */
-                network);
+                network,
+                Instant.MAX /* expirationTime */);
 
         // Verify onServiceNameDiscovered callback
         listener.onServiceNameDiscovered(foundInfo, false /* isServiceFromCache */);
@@ -1154,7 +1162,8 @@
                 null, /* textStrings */
                 null, /* textEntries */
                 1234, /* interfaceIndex */
-                network);
+                network,
+                Instant.MAX /* expirationTime */);
         // Verify onServiceNameRemoved callback
         listener.onServiceNameRemoved(removedInfo);
         verify(discListener, timeout(TIMEOUT_MS)).onServiceLost(argThat(info ->
@@ -1276,7 +1285,8 @@
                 List.of(MdnsServiceInfo.TextEntry.fromBytes(new byte[]{
                         'k', 'e', 'y', '=', (byte) 0xFF, (byte) 0xFE})) /* textEntries */,
                 1234,
-                network);
+                network,
+                Instant.ofEpochSecond(1000_000L) /* expirationTime */);
 
         // Verify onServiceFound callback
         doReturn(TEST_TIME_MS + 10L).when(mClock).elapsedRealtime();
@@ -1301,6 +1311,7 @@
         assertTrue(info.getHostAddresses().stream().anyMatch(
                 address -> address.equals(parseNumericAddress("2001:db8::2"))));
         assertEquals(network, info.getNetwork());
+        assertEquals(Instant.ofEpochSecond(1000_000L), info.getExpirationTime());
 
         // Verify the listener has been unregistered.
         verify(mDiscoveryManager, timeout(TIMEOUT_MS))
@@ -1518,6 +1529,82 @@
     }
 
     @Test
+    public void testAdvertiseCustomTtl_validTtl_success() {
+        runValidTtlAdvertisingTest(30L);
+        runValidTtlAdvertisingTest(10 * 3600L);
+    }
+
+    @Test
+    public void testAdvertiseCustomTtl_ttlSmallerThan30SecondsButClientIsSystemServer_success() {
+        when(mDeps.getCallingUid()).thenReturn(Process.SYSTEM_UID);
+
+        runValidTtlAdvertisingTest(29L);
+    }
+
+    @Test
+    public void testAdvertiseCustomTtl_ttlLargerThan10HoursButClientIsSystemServer_success() {
+        when(mDeps.getCallingUid()).thenReturn(Process.SYSTEM_UID);
+
+        runValidTtlAdvertisingTest(10 * 3600L + 1);
+        runValidTtlAdvertisingTest(0xffffffffL);
+    }
+
+    private void runValidTtlAdvertisingTest(long validTtlSeconds) {
+        setMdnsAdvertiserEnabled();
+
+        final NsdManager client = connectClient(mService);
+        final RegistrationListener regListener = mock(RegistrationListener.class);
+        final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
+                ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
+        verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
+
+        final NsdServiceInfo regInfo = new NsdServiceInfo("Service custom TTL", SERVICE_TYPE);
+        regInfo.setPort(1234);
+        final AdvertisingRequest request =
+                new AdvertisingRequest.Builder(regInfo, NsdManager.PROTOCOL_DNS_SD)
+                    .setTtl(Duration.ofSeconds(validTtlSeconds)).build();
+
+        client.registerService(request, Runnable::run, regListener);
+        waitForIdle();
+
+        final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
+        final MdnsAdvertisingOptions expectedAdverstingOptions =
+                MdnsAdvertisingOptions.newBuilder().setTtl(request.getTtl()).build();
+        verify(mAdvertiser).addOrUpdateService(idCaptor.capture(), any(),
+                eq(expectedAdverstingOptions), anyInt());
+
+        // Verify onServiceRegistered callback
+        final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
+        final int regId = idCaptor.getValue();
+        cb.onRegisterServiceSucceeded(regId, regInfo);
+
+        verify(regListener, timeout(TIMEOUT_MS)).onServiceRegistered(
+                argThat(info -> matches(info, new NsdServiceInfo(regInfo.getServiceName(), null))));
+    }
+
+    @Test
+    public void testAdvertiseCustomTtl_invalidTtl_FailsWithBadParameters() {
+        setMdnsAdvertiserEnabled();
+        final long invalidTtlSeconds = 29L;
+        final NsdManager client = connectClient(mService);
+        final RegistrationListener regListener = mock(RegistrationListener.class);
+        final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
+                ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
+        verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any(), any(), any());
+
+        final NsdServiceInfo regInfo = new NsdServiceInfo("Service custom TTL", SERVICE_TYPE);
+        regInfo.setPort(1234);
+        final AdvertisingRequest request =
+                new AdvertisingRequest.Builder(regInfo, NsdManager.PROTOCOL_DNS_SD)
+                    .setTtl(Duration.ofSeconds(invalidTtlSeconds)).build();
+        client.registerService(request, Runnable::run, regListener);
+        waitForIdle();
+
+        verify(regListener, timeout(TIMEOUT_MS))
+                .onRegistrationFailed(any(), eq(FAILURE_BAD_PARAMETERS));
+    }
+
+    @Test
     public void testStopServiceResolutionWithMdnsDiscoveryManager() {
         setMdnsDiscoveryManagerEnabled();
 
diff --git a/tests/unit/java/com/android/server/VpnManagerServiceTest.java b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
deleted file mode 100644
index bf23cd1..0000000
--- a/tests/unit/java/com/android/server/VpnManagerServiceTest.java
+++ /dev/null
@@ -1,409 +0,0 @@
-/*
- * Copyright (C) 2022 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;
-
-import static android.os.Build.VERSION_CODES.R;
-
-import static com.android.testutils.ContextUtils.mockService;
-import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import static com.android.testutils.MiscAsserts.assertThrows;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.annotation.UserIdInt;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.net.ConnectivityManager;
-import android.net.INetd;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.INetworkManagementService;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.security.Credentials;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.net.VpnProfile;
-import com.android.server.connectivity.Vpn;
-import com.android.server.connectivity.VpnProfileStore;
-import com.android.server.net.LockdownVpnTracker;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-import com.android.testutils.HandlerUtils;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-
-@RunWith(DevSdkIgnoreRunner.class)
-@IgnoreUpTo(R) // VpnManagerService is not available before R
-@SmallTest
-public class VpnManagerServiceTest extends VpnTestBase {
-    private static final String CONTEXT_ATTRIBUTION_TAG = "VPN_MANAGER";
-
-    @Rule
-    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
-
-    private static final int TIMEOUT_MS = 2_000;
-
-    @Mock Context mContext;
-    @Mock Context mContextWithoutAttributionTag;
-    @Mock Context mSystemContext;
-    @Mock Context mUserAllContext;
-    private HandlerThread mHandlerThread;
-    @Mock private Vpn mVpn;
-    @Mock private INetworkManagementService mNms;
-    @Mock private ConnectivityManager mCm;
-    @Mock private UserManager mUserManager;
-    @Mock private INetd mNetd;
-    @Mock private PackageManager mPackageManager;
-    @Mock private VpnProfileStore mVpnProfileStore;
-    @Mock private LockdownVpnTracker mLockdownVpnTracker;
-
-    private VpnManagerServiceDependencies mDeps;
-    private VpnManagerService mService;
-    private BroadcastReceiver mUserPresentReceiver;
-    private BroadcastReceiver mIntentReceiver;
-    private final String mNotMyVpnPkg = "com.not.my.vpn";
-
-    class VpnManagerServiceDependencies extends VpnManagerService.Dependencies {
-        @Override
-        public HandlerThread makeHandlerThread() {
-            return mHandlerThread;
-        }
-
-        @Override
-        public INetworkManagementService getINetworkManagementService() {
-            return mNms;
-        }
-
-        @Override
-        public INetd getNetd() {
-            return mNetd;
-        }
-
-        @Override
-        public Vpn createVpn(Looper looper, Context context, INetworkManagementService nms,
-                INetd netd, @UserIdInt int userId) {
-            return mVpn;
-        }
-
-        @Override
-        public VpnProfileStore getVpnProfileStore() {
-            return mVpnProfileStore;
-        }
-
-        @Override
-        public LockdownVpnTracker createLockDownVpnTracker(Context context, Handler handler,
-                Vpn vpn, VpnProfile profile) {
-            return mLockdownVpnTracker;
-        }
-
-        @Override
-        public @UserIdInt int getMainUserId() {
-            return UserHandle.USER_SYSTEM;
-        }
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mHandlerThread = new HandlerThread("TestVpnManagerService");
-        mDeps = new VpnManagerServiceDependencies();
-
-        // The attribution tag is a dependency for IKE library to collect VPN metrics correctly
-        // and thus should not be changed without updating the IKE code.
-        doReturn(mContext)
-                .when(mContextWithoutAttributionTag)
-                .createAttributionContext(CONTEXT_ATTRIBUTION_TAG);
-
-        doReturn(mUserAllContext).when(mContext).createContextAsUser(UserHandle.ALL, 0);
-        doReturn(mSystemContext).when(mContext).createContextAsUser(UserHandle.SYSTEM, 0);
-        doReturn(mPackageManager).when(mContext).getPackageManager();
-        setMockedPackages(mPackageManager, sPackages);
-
-        mockService(mContext, ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mCm);
-        mockService(mContext, UserManager.class, Context.USER_SERVICE, mUserManager);
-        doReturn(SYSTEM_USER).when(mUserManager).getUserInfo(eq(SYSTEM_USER_ID));
-
-        mService = new VpnManagerService(mContextWithoutAttributionTag, mDeps);
-        mService.systemReady();
-
-        final ArgumentCaptor<BroadcastReceiver> intentReceiverCaptor =
-                ArgumentCaptor.forClass(BroadcastReceiver.class);
-        final ArgumentCaptor<BroadcastReceiver> userPresentReceiverCaptor =
-                ArgumentCaptor.forClass(BroadcastReceiver.class);
-        verify(mSystemContext).registerReceiver(
-                userPresentReceiverCaptor.capture(), any(), any(), any());
-        verify(mUserAllContext, times(2)).registerReceiver(
-                intentReceiverCaptor.capture(), any(), any(), any());
-        mUserPresentReceiver = userPresentReceiverCaptor.getValue();
-        mIntentReceiver = intentReceiverCaptor.getValue();
-
-        // Add user to create vpn in mVpn
-        onUserStarted(SYSTEM_USER_ID);
-        assertNotNull(mService.mVpns.get(SYSTEM_USER_ID));
-    }
-
-    @Test
-    public void testUpdateAppExclusionList() {
-        // Start vpn
-        mService.startVpnProfile(TEST_VPN_PKG);
-        verify(mVpn).startVpnProfile(eq(TEST_VPN_PKG));
-
-        // Remove package due to package replaced.
-        onPackageRemoved(PKGS[0], PKG_UIDS[0], true /* isReplacing */);
-        verify(mVpn, never()).refreshPlatformVpnAppExclusionList();
-
-        // Add package due to package replaced.
-        onPackageAdded(PKGS[0], PKG_UIDS[0], true /* isReplacing */);
-        verify(mVpn, never()).refreshPlatformVpnAppExclusionList();
-
-        // Remove package
-        onPackageRemoved(PKGS[0], PKG_UIDS[0], false /* isReplacing */);
-        verify(mVpn).refreshPlatformVpnAppExclusionList();
-
-        // Add the package back
-        onPackageAdded(PKGS[0], PKG_UIDS[0], false /* isReplacing */);
-        verify(mVpn, times(2)).refreshPlatformVpnAppExclusionList();
-    }
-
-    @Test
-    public void testStartVpnProfileFromDiffPackage() {
-        assertThrows(
-                SecurityException.class, () -> mService.startVpnProfile(mNotMyVpnPkg));
-    }
-
-    @Test
-    public void testStopVpnProfileFromDiffPackage() {
-        assertThrows(SecurityException.class, () -> mService.stopVpnProfile(mNotMyVpnPkg));
-    }
-
-    @Test
-    public void testGetProvisionedVpnProfileStateFromDiffPackage() {
-        assertThrows(SecurityException.class, () ->
-                mService.getProvisionedVpnProfileState(mNotMyVpnPkg));
-    }
-
-    @Test
-    public void testGetProvisionedVpnProfileState() {
-        mService.getProvisionedVpnProfileState(TEST_VPN_PKG);
-        verify(mVpn).getProvisionedVpnProfileState(TEST_VPN_PKG);
-    }
-
-    private Intent buildIntent(String action, String packageName, int userId, int uid,
-            boolean isReplacing) {
-        final Intent intent = new Intent(action);
-        intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
-        intent.putExtra(Intent.EXTRA_UID, uid);
-        intent.putExtra(Intent.EXTRA_REPLACING, isReplacing);
-        if (packageName != null) {
-            intent.setData(Uri.fromParts("package" /* scheme */, packageName, null /* fragment */));
-        }
-
-        return intent;
-    }
-
-    private void sendIntent(Intent intent) {
-        sendIntent(mIntentReceiver, mContext, intent);
-    }
-
-    private void sendIntent(BroadcastReceiver receiver, Context context, Intent intent) {
-        final Handler h = mHandlerThread.getThreadHandler();
-
-        // Send in handler thread.
-        h.post(() -> receiver.onReceive(context, intent));
-        HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
-    }
-
-    private void onUserStarted(int userId) {
-        sendIntent(buildIntent(Intent.ACTION_USER_STARTED,
-                null /* packageName */, userId, -1 /* uid */, false /* isReplacing */));
-    }
-
-    private void onUserUnlocked(int userId) {
-        sendIntent(buildIntent(Intent.ACTION_USER_UNLOCKED,
-                null /* packageName */, userId, -1 /* uid */, false /* isReplacing */));
-    }
-
-    private void onUserStopped(int userId) {
-        sendIntent(buildIntent(Intent.ACTION_USER_STOPPED,
-                null /* packageName */, userId, -1 /* uid */, false /* isReplacing */));
-    }
-
-    private void onLockDownReset() {
-        sendIntent(buildIntent(LockdownVpnTracker.ACTION_LOCKDOWN_RESET, null /* packageName */,
-                UserHandle.USER_SYSTEM, -1 /* uid */, false /* isReplacing */));
-    }
-
-    private void onPackageAdded(String packageName, int userId, int uid, boolean isReplacing) {
-        sendIntent(buildIntent(Intent.ACTION_PACKAGE_ADDED, packageName, userId, uid, isReplacing));
-    }
-
-    private void onPackageAdded(String packageName, int uid, boolean isReplacing) {
-        onPackageAdded(packageName, UserHandle.USER_SYSTEM, uid, isReplacing);
-    }
-
-    private void onPackageRemoved(String packageName, int userId, int uid, boolean isReplacing) {
-        sendIntent(buildIntent(Intent.ACTION_PACKAGE_REMOVED, packageName, userId, uid,
-                isReplacing));
-    }
-
-    private void onPackageRemoved(String packageName, int uid, boolean isReplacing) {
-        onPackageRemoved(packageName, UserHandle.USER_SYSTEM, uid, isReplacing);
-    }
-
-    @Test
-    public void testReceiveIntentFromNonHandlerThread() {
-        assertThrows(IllegalStateException.class, () ->
-                mIntentReceiver.onReceive(mContext, buildIntent(Intent.ACTION_PACKAGE_REMOVED,
-                        PKGS[0], UserHandle.USER_SYSTEM, PKG_UIDS[0], true /* isReplacing */)));
-
-        assertThrows(IllegalStateException.class, () ->
-                mUserPresentReceiver.onReceive(mContext, new Intent(Intent.ACTION_USER_PRESENT)));
-    }
-
-    private void setupLockdownVpn(String packageName) {
-        final byte[] profileTag = packageName.getBytes(StandardCharsets.UTF_8);
-        doReturn(profileTag).when(mVpnProfileStore).get(Credentials.LOCKDOWN_VPN);
-    }
-
-    private void setupVpnProfile(String profileName) {
-        final VpnProfile profile = new VpnProfile(profileName);
-        profile.name = profileName;
-        profile.server = "192.0.2.1";
-        profile.dnsServers = "8.8.8.8";
-        profile.type = VpnProfile.TYPE_IPSEC_XAUTH_PSK;
-        final byte[] encodedProfile = profile.encode();
-        doReturn(encodedProfile).when(mVpnProfileStore).get(Credentials.VPN + profileName);
-    }
-
-    @Test
-    public void testUserPresent() {
-        // Verify that LockDownVpnTracker is not created.
-        verify(mLockdownVpnTracker, never()).init();
-
-        setupLockdownVpn(TEST_VPN_PKG);
-        setupVpnProfile(TEST_VPN_PKG);
-
-        // mUserPresentReceiver only registers ACTION_USER_PRESENT intent and does no verification
-        // on action, so an empty intent is enough.
-        sendIntent(mUserPresentReceiver, mSystemContext, new Intent());
-
-        verify(mLockdownVpnTracker).init();
-        verify(mSystemContext).unregisterReceiver(mUserPresentReceiver);
-        verify(mUserAllContext, never()).unregisterReceiver(any());
-    }
-
-    @Test
-    public void testUpdateLockdownVpn() {
-        setupLockdownVpn(TEST_VPN_PKG);
-        onUserUnlocked(SYSTEM_USER_ID);
-
-        // Will not create lockDownVpnTracker w/o valid profile configured in the keystore
-        verify(mLockdownVpnTracker, never()).init();
-
-        setupVpnProfile(TEST_VPN_PKG);
-
-        // Remove the user from mVpns
-        onUserStopped(SYSTEM_USER_ID);
-        onUserUnlocked(SYSTEM_USER_ID);
-        verify(mLockdownVpnTracker, never()).init();
-
-        // Add user back
-        onUserStarted(SYSTEM_USER_ID);
-        verify(mLockdownVpnTracker).init();
-
-        // Trigger another update. The existing LockDownVpnTracker should be shut down and
-        // initialize another one.
-        onUserUnlocked(SYSTEM_USER_ID);
-        verify(mLockdownVpnTracker).shutdown();
-        verify(mLockdownVpnTracker, times(2)).init();
-    }
-
-    @Test
-    public void testLockdownReset() {
-        // Init LockdownVpnTracker
-        setupLockdownVpn(TEST_VPN_PKG);
-        setupVpnProfile(TEST_VPN_PKG);
-        onUserUnlocked(SYSTEM_USER_ID);
-        verify(mLockdownVpnTracker).init();
-
-        onLockDownReset();
-        verify(mLockdownVpnTracker).reset();
-    }
-
-    @Test
-    public void testLockdownResetWhenLockdownVpnTrackerIsNotInit() {
-        setupLockdownVpn(TEST_VPN_PKG);
-        setupVpnProfile(TEST_VPN_PKG);
-
-        onLockDownReset();
-
-        // LockDownVpnTracker is not created. Lockdown reset will not take effect.
-        verify(mLockdownVpnTracker, never()).reset();
-    }
-
-    @Test
-    public void testIsVpnLockdownEnabled() {
-        // Vpn is created but the VPN lockdown is not enabled.
-        assertFalse(mService.isVpnLockdownEnabled(SYSTEM_USER_ID));
-
-        // Set lockdown for the SYSTEM_USER_ID VPN.
-        doReturn(true).when(mVpn).getLockdown();
-        assertTrue(mService.isVpnLockdownEnabled(SYSTEM_USER_ID));
-
-        // Even lockdown is enabled but no Vpn is created for SECONDARY_USER.
-        assertFalse(mService.isVpnLockdownEnabled(SECONDARY_USER.id));
-    }
-
-    @Test
-    public void testGetVpnLockdownAllowlist() {
-        doReturn(null).when(mVpn).getLockdownAllowlist();
-        assertNull(mService.getVpnLockdownAllowlist(SYSTEM_USER_ID));
-
-        final List<String> expected = List.of(PKGS);
-        doReturn(expected).when(mVpn).getLockdownAllowlist();
-        assertEquals(expected, mService.getVpnLockdownAllowlist(SYSTEM_USER_ID));
-
-        // Even lockdown is enabled but no Vpn is created for SECONDARY_USER.
-        assertNull(mService.getVpnLockdownAllowlist(SECONDARY_USER.id));
-    }
-}
diff --git a/tests/unit/java/com/android/server/VpnTestBase.java b/tests/unit/java/com/android/server/VpnTestBase.java
deleted file mode 100644
index 6113872..0000000
--- a/tests/unit/java/com/android/server/VpnTestBase.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2022 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;
-
-import static android.content.pm.UserInfo.FLAG_ADMIN;
-import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
-import static android.content.pm.UserInfo.FLAG_PRIMARY;
-import static android.content.pm.UserInfo.FLAG_RESTRICTED;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doAnswer;
-
-import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
-import android.os.Process;
-import android.os.UserHandle;
-import android.util.ArrayMap;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-/** Common variables or methods shared between VpnTest and VpnManagerServiceTest. */
-public class VpnTestBase {
-    protected static final String TEST_VPN_PKG = "com.testvpn.vpn";
-    /**
-     * Names and UIDs for some fake packages. Important points:
-     *  - UID is ordered increasing.
-     *  - One pair of packages have consecutive UIDs.
-     */
-    protected static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"};
-    protected static final int[] PKG_UIDS = {10066, 10077, 10078, 10400};
-    // Mock packages
-    protected static final Map<String, Integer> sPackages = new ArrayMap<>();
-    static {
-        for (int i = 0; i < PKGS.length; i++) {
-            sPackages.put(PKGS[i], PKG_UIDS[i]);
-        }
-        sPackages.put(TEST_VPN_PKG, Process.myUid());
-    }
-
-    // Mock users
-    protected static final int SYSTEM_USER_ID = 0;
-    protected static final UserInfo SYSTEM_USER = new UserInfo(0, "system", UserInfo.FLAG_PRIMARY);
-    protected static final UserInfo PRIMARY_USER = new UserInfo(27, "Primary",
-            FLAG_ADMIN | FLAG_PRIMARY);
-    protected static final UserInfo SECONDARY_USER = new UserInfo(15, "Secondary", FLAG_ADMIN);
-    protected static final UserInfo RESTRICTED_PROFILE_A = new UserInfo(40, "RestrictedA",
-            FLAG_RESTRICTED);
-    protected static final UserInfo RESTRICTED_PROFILE_B = new UserInfo(42, "RestrictedB",
-            FLAG_RESTRICTED);
-    protected static final UserInfo MANAGED_PROFILE_A = new UserInfo(45, "ManagedA",
-            FLAG_MANAGED_PROFILE);
-    static {
-        RESTRICTED_PROFILE_A.restrictedProfileParentId = PRIMARY_USER.id;
-        RESTRICTED_PROFILE_B.restrictedProfileParentId = SECONDARY_USER.id;
-        MANAGED_PROFILE_A.profileGroupId = PRIMARY_USER.id;
-    }
-
-    // Populate a fake packageName-to-UID mapping.
-    protected void setMockedPackages(PackageManager mockPm, final Map<String, Integer> packages) {
-        try {
-            doAnswer(invocation -> {
-                final String appName = (String) invocation.getArguments()[0];
-                final int userId = (int) invocation.getArguments()[1];
-
-                final Integer appId = packages.get(appName);
-                if (appId == null) {
-                    throw new PackageManager.NameNotFoundException(appName);
-                }
-
-                return UserHandle.getUid(userId, appId);
-            }).when(mockPm).getPackageUidAsUser(anyString(), anyInt());
-        } catch (Exception e) {
-        }
-    }
-
-    protected List<Integer> toList(int[] arr) {
-        return Arrays.stream(arr).boxed().collect(Collectors.toList());
-    }
-}
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
deleted file mode 100644
index c9cece0..0000000
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ /dev/null
@@ -1,3298 +0,0 @@
-/*
- * Copyright (C) 2016 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.connectivity;
-
-import static android.Manifest.permission.BIND_VPN_SERVICE;
-import static android.Manifest.permission.CONTROL_VPN;
-import static android.content.pm.PackageManager.PERMISSION_DENIED;
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback;
-import static android.net.ConnectivityDiagnosticsManager.DataStallReport;
-import static android.net.ConnectivityManager.NetworkCallback;
-import static android.net.INetd.IF_STATE_DOWN;
-import static android.net.INetd.IF_STATE_UP;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
-import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
-import static android.net.RouteInfo.RTN_UNREACHABLE;
-import static android.net.VpnManager.TYPE_VPN_PLATFORM;
-import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
-import static android.net.cts.util.IkeSessionTestUtils.TEST_IDENTITY;
-import static android.net.cts.util.IkeSessionTestUtils.TEST_KEEPALIVE_TIMEOUT_UNSET;
-import static android.net.cts.util.IkeSessionTestUtils.getTestIkeSessionParams;
-import static android.net.ipsec.ike.IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_AUTO;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_NONE;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_UDP;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_AUTO;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_IPV4;
-import static android.net.ipsec.ike.IkeSessionParams.ESP_IP_VERSION_IPV6;
-import static android.os.UserHandle.PER_USER_RANGE;
-import static android.telephony.CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL;
-import static android.telephony.CarrierConfigManager.KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT;
-import static android.telephony.CarrierConfigManager.KEY_PREFERRED_IKE_PROTOCOL_INT;
-
-import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
-import static com.android.server.connectivity.Vpn.AUTOMATIC_KEEPALIVE_DELAY_SECONDS;
-import static com.android.server.connectivity.Vpn.DEFAULT_LONG_LIVED_TCP_CONNS_EXPENSIVE_TIMEOUT_SEC;
-import static com.android.server.connectivity.Vpn.DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT;
-import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_AUTO;
-import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV4_UDP;
-import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_ESP;
-import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_UDP;
-import static com.android.testutils.HandlerUtils.waitForIdleSerialExecutor;
-import static com.android.testutils.MiscAsserts.assertThrows;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.longThat;
-import static org.mockito.Mockito.after;
-import static org.mockito.Mockito.atLeast;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doCallRealMethod;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.annotation.NonNull;
-import android.annotation.UserIdInt;
-import android.app.AppOpsManager;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.content.pm.UserInfo;
-import android.content.res.Resources;
-import android.net.ConnectivityDiagnosticsManager;
-import android.net.ConnectivityManager;
-import android.net.INetd;
-import android.net.Ikev2VpnProfile;
-import android.net.InetAddresses;
-import android.net.InterfaceConfigurationParcel;
-import android.net.IpPrefix;
-import android.net.IpSecConfig;
-import android.net.IpSecManager;
-import android.net.IpSecTransform;
-import android.net.IpSecTunnelInterfaceResponse;
-import android.net.LinkAddress;
-import android.net.LinkProperties;
-import android.net.Network;
-import android.net.NetworkAgent;
-import android.net.NetworkAgentConfig;
-import android.net.NetworkCapabilities;
-import android.net.NetworkInfo.DetailedState;
-import android.net.RouteInfo;
-import android.net.TelephonyNetworkSpecifier;
-import android.net.UidRangeParcel;
-import android.net.VpnManager;
-import android.net.VpnProfileState;
-import android.net.VpnService;
-import android.net.VpnTransportInfo;
-import android.net.ipsec.ike.ChildSessionCallback;
-import android.net.ipsec.ike.ChildSessionConfiguration;
-import android.net.ipsec.ike.IkeFqdnIdentification;
-import android.net.ipsec.ike.IkeSessionCallback;
-import android.net.ipsec.ike.IkeSessionConfiguration;
-import android.net.ipsec.ike.IkeSessionConnectionInfo;
-import android.net.ipsec.ike.IkeSessionParams;
-import android.net.ipsec.ike.IkeTrafficSelector;
-import android.net.ipsec.ike.IkeTunnelConnectionParams;
-import android.net.ipsec.ike.exceptions.IkeException;
-import android.net.ipsec.ike.exceptions.IkeNetworkLostException;
-import android.net.ipsec.ike.exceptions.IkeNonProtocolException;
-import android.net.ipsec.ike.exceptions.IkeProtocolException;
-import android.net.ipsec.ike.exceptions.IkeTimeoutException;
-import android.net.vcn.VcnTransportInfo;
-import android.net.wifi.WifiInfo;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.os.INetworkManagementService;
-import android.os.ParcelFileDescriptor;
-import android.os.PersistableBundle;
-import android.os.PowerWhitelistManager;
-import android.os.Process;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.os.test.TestLooper;
-import android.provider.Settings;
-import android.security.Credentials;
-import android.telephony.CarrierConfigManager;
-import android.telephony.SubscriptionInfo;
-import android.telephony.SubscriptionManager;
-import android.telephony.TelephonyManager;
-import android.util.ArrayMap;
-import android.util.ArraySet;
-import android.util.Pair;
-import android.util.Range;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.R;
-import com.android.internal.net.LegacyVpnInfo;
-import com.android.internal.net.VpnConfig;
-import com.android.internal.net.VpnProfile;
-import com.android.internal.util.HexDump;
-import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.DeviceIdleInternal;
-import com.android.server.IpSecService;
-import com.android.server.VpnTestBase;
-import com.android.server.vcn.util.PersistableBundleUtils;
-import com.android.testutils.DevSdkIgnoreRule;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.AdditionalAnswers;
-import org.mockito.Answers;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.net.Inet4Address;
-import java.net.Inet6Address;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Tests for {@link Vpn}.
- *
- * Build, install and run with:
- *  runtest frameworks-net -c com.android.server.connectivity.VpnTest
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class VpnTest extends VpnTestBase {
-    private static final String TAG = "VpnTest";
-
-    @Rule
-    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
-
-    static final Network EGRESS_NETWORK = new Network(101);
-    static final String EGRESS_IFACE = "wlan0";
-    private static final String TEST_VPN_CLIENT = "2.4.6.8";
-    private static final String TEST_VPN_SERVER = "1.2.3.4";
-    private static final String TEST_VPN_IDENTITY = "identity";
-    private static final byte[] TEST_VPN_PSK = "psk".getBytes();
-
-    private static final int IP4_PREFIX_LEN = 32;
-    private static final int IP6_PREFIX_LEN = 64;
-    private static final int MIN_PORT = 0;
-    private static final int MAX_PORT = 65535;
-
-    private static final InetAddress TEST_VPN_CLIENT_IP =
-            InetAddresses.parseNumericAddress(TEST_VPN_CLIENT);
-    private static final InetAddress TEST_VPN_SERVER_IP =
-            InetAddresses.parseNumericAddress(TEST_VPN_SERVER);
-    private static final InetAddress TEST_VPN_CLIENT_IP_2 =
-            InetAddresses.parseNumericAddress("192.0.2.200");
-    private static final InetAddress TEST_VPN_SERVER_IP_2 =
-            InetAddresses.parseNumericAddress("192.0.2.201");
-    private static final InetAddress TEST_VPN_INTERNAL_IP =
-            InetAddresses.parseNumericAddress("198.51.100.10");
-    private static final InetAddress TEST_VPN_INTERNAL_IP6 =
-            InetAddresses.parseNumericAddress("2001:db8::1");
-    private static final InetAddress TEST_VPN_INTERNAL_DNS =
-            InetAddresses.parseNumericAddress("8.8.8.8");
-    private static final InetAddress TEST_VPN_INTERNAL_DNS6 =
-            InetAddresses.parseNumericAddress("2001:4860:4860::8888");
-
-    private static final IkeTrafficSelector IN_TS =
-            new IkeTrafficSelector(MIN_PORT, MAX_PORT, TEST_VPN_INTERNAL_IP, TEST_VPN_INTERNAL_IP);
-    private static final IkeTrafficSelector IN_TS6 =
-            new IkeTrafficSelector(
-                    MIN_PORT, MAX_PORT, TEST_VPN_INTERNAL_IP6, TEST_VPN_INTERNAL_IP6);
-    private static final IkeTrafficSelector OUT_TS =
-            new IkeTrafficSelector(MIN_PORT, MAX_PORT,
-                    InetAddresses.parseNumericAddress("0.0.0.0"),
-                    InetAddresses.parseNumericAddress("255.255.255.255"));
-    private static final IkeTrafficSelector OUT_TS6 =
-            new IkeTrafficSelector(
-                    MIN_PORT,
-                    MAX_PORT,
-                    InetAddresses.parseNumericAddress("::"),
-                    InetAddresses.parseNumericAddress("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
-
-    private static final Network TEST_NETWORK = new Network(Integer.MAX_VALUE);
-    private static final Network TEST_NETWORK_2 = new Network(Integer.MAX_VALUE - 1);
-    private static final String TEST_IFACE_NAME = "TEST_IFACE";
-    private static final int TEST_TUNNEL_RESOURCE_ID = 0x2345;
-    private static final long TEST_TIMEOUT_MS = 500L;
-    private static final long TIMEOUT_CROSSTHREAD_MS = 20_000L;
-    private static final String PRIMARY_USER_APP_EXCLUDE_KEY =
-            "VPNAPPEXCLUDED_27_com.testvpn.vpn";
-    static final String PKGS_BYTES = getPackageByteString(List.of(PKGS));
-    private static final Range<Integer> PRIMARY_USER_RANGE = uidRangeForUser(PRIMARY_USER.id);
-    private static final int TEST_KEEPALIVE_TIMER = 800;
-    private static final int TEST_SUB_ID = 1234;
-    private static final String TEST_MCCMNC = "12345";
-
-    @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext;
-    @Mock private UserManager mUserManager;
-    @Mock private PackageManager mPackageManager;
-    @Mock private INetworkManagementService mNetService;
-    @Mock private INetd mNetd;
-    @Mock private AppOpsManager mAppOps;
-    @Mock private NotificationManager mNotificationManager;
-    @Mock private Vpn.SystemServices mSystemServices;
-    @Mock private Vpn.IkeSessionWrapper mIkeSessionWrapper;
-    @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator;
-    @Mock private Vpn.VpnNetworkAgentWrapper mMockNetworkAgent;
-    @Mock private ConnectivityManager mConnectivityManager;
-    @Mock private ConnectivityDiagnosticsManager mCdm;
-    @Mock private TelephonyManager mTelephonyManager;
-    @Mock private TelephonyManager mTmPerSub;
-    @Mock private CarrierConfigManager mConfigManager;
-    @Mock private SubscriptionManager mSubscriptionManager;
-    @Mock private IpSecService mIpSecService;
-    @Mock private VpnProfileStore mVpnProfileStore;
-    private final TestExecutor mExecutor;
-    @Mock DeviceIdleInternal mDeviceIdleInternal;
-    private final VpnProfile mVpnProfile;
-
-    @Captor private ArgumentCaptor<Collection<Range<Integer>>> mUidRangesCaptor;
-
-    private IpSecManager mIpSecManager;
-    private TestDeps mTestDeps;
-
-    public static class TestExecutor extends ScheduledThreadPoolExecutor {
-        public static final long REAL_DELAY = -1;
-
-        // For the purposes of the test, run all scheduled tasks after 10ms to save
-        // execution time, unless overridden by the specific test. Set to REAL_DELAY
-        // to actually wait for the delay specified by the real call to schedule().
-        public long delayMs = 10;
-        // If this is true, execute() will call the runnable inline. This is useful because
-        // super.execute() calls schedule(), which messes with checks that scheduled() is
-        // called a given number of times.
-        public boolean executeDirect = false;
-
-        public TestExecutor() {
-            super(1);
-        }
-
-        @Override
-        public void execute(final Runnable command) {
-            // See |executeDirect| for why this is necessary.
-            if (executeDirect) {
-                command.run();
-            } else {
-                super.execute(command);
-            }
-        }
-
-        @Override
-        public ScheduledFuture<?> schedule(final Runnable command, final long delay,
-                TimeUnit unit) {
-            if (0 == delay || delayMs == REAL_DELAY) {
-                // super.execute() calls schedule() with 0, so use the real delay if it's 0.
-                return super.schedule(command, delay, unit);
-            } else {
-                return super.schedule(command, delayMs, TimeUnit.MILLISECONDS);
-            }
-        }
-    }
-
-    public VpnTest() throws Exception {
-        // Build an actual VPN profile that is capable of being converted to and from an
-        // Ikev2VpnProfile
-        final Ikev2VpnProfile.Builder builder =
-                new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY);
-        builder.setAuthPsk(TEST_VPN_PSK);
-        builder.setBypassable(true /* isBypassable */);
-        mExecutor = spy(new TestExecutor());
-        mVpnProfile = builder.build().toVpnProfile();
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-
-        mIpSecManager = new IpSecManager(mContext, mIpSecService);
-        mTestDeps = spy(new TestDeps());
-        doReturn(IPV6_MIN_MTU)
-                .when(mTestDeps)
-                .calculateVpnMtu(any(), anyInt(), anyInt(), anyBoolean());
-        doReturn(1500).when(mTestDeps).getJavaNetworkInterfaceMtu(any(), anyInt());
-
-        when(mContext.getPackageManager()).thenReturn(mPackageManager);
-        setMockedPackages(sPackages);
-
-        when(mContext.getPackageName()).thenReturn(TEST_VPN_PKG);
-        when(mContext.getOpPackageName()).thenReturn(TEST_VPN_PKG);
-        mockService(UserManager.class, Context.USER_SERVICE, mUserManager);
-        mockService(AppOpsManager.class, Context.APP_OPS_SERVICE, mAppOps);
-        mockService(NotificationManager.class, Context.NOTIFICATION_SERVICE, mNotificationManager);
-        mockService(ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mConnectivityManager);
-        mockService(IpSecManager.class, Context.IPSEC_SERVICE, mIpSecManager);
-        mockService(ConnectivityDiagnosticsManager.class, Context.CONNECTIVITY_DIAGNOSTICS_SERVICE,
-                mCdm);
-        mockService(TelephonyManager.class, Context.TELEPHONY_SERVICE, mTelephonyManager);
-        mockService(CarrierConfigManager.class, Context.CARRIER_CONFIG_SERVICE, mConfigManager);
-        mockService(SubscriptionManager.class, Context.TELEPHONY_SUBSCRIPTION_SERVICE,
-                mSubscriptionManager);
-        doReturn(mTmPerSub).when(mTelephonyManager).createForSubscriptionId(anyInt());
-        when(mContext.getString(R.string.config_customVpnAlwaysOnDisconnectedDialogComponent))
-                .thenReturn(Resources.getSystem().getString(
-                        R.string.config_customVpnAlwaysOnDisconnectedDialogComponent));
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS))
-                .thenReturn(true);
-
-        // Used by {@link Notification.Builder}
-        ApplicationInfo applicationInfo = new ApplicationInfo();
-        applicationInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT;
-        when(mContext.getApplicationInfo()).thenReturn(applicationInfo);
-        when(mPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
-                .thenReturn(applicationInfo);
-
-        doNothing().when(mNetService).registerObserver(any());
-
-        // Deny all appops by default.
-        when(mAppOps.noteOpNoThrow(anyString(), anyInt(), anyString(), any(), any()))
-                .thenReturn(AppOpsManager.MODE_IGNORED);
-
-        // Setup IpSecService
-        final IpSecTunnelInterfaceResponse tunnelResp =
-                new IpSecTunnelInterfaceResponse(
-                        IpSecManager.Status.OK, TEST_TUNNEL_RESOURCE_ID, TEST_IFACE_NAME);
-        when(mIpSecService.createTunnelInterface(any(), any(), any(), any(), any()))
-                .thenReturn(tunnelResp);
-        doReturn(new LinkProperties()).when(mConnectivityManager).getLinkProperties(any());
-
-        // The unit test should know what kind of permission it needs and set the permission by
-        // itself, so set the default value of Context#checkCallingOrSelfPermission to
-        // PERMISSION_DENIED.
-        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(any());
-
-        // Set up mIkev2SessionCreator and mExecutor
-        resetIkev2SessionCreator(mIkeSessionWrapper);
-    }
-
-    private void resetIkev2SessionCreator(Vpn.IkeSessionWrapper ikeSession) {
-        reset(mIkev2SessionCreator);
-        when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
-                .thenReturn(ikeSession);
-    }
-
-    private <T> void mockService(Class<T> clazz, String name, T service) {
-        doReturn(service).when(mContext).getSystemService(name);
-        doReturn(name).when(mContext).getSystemServiceName(clazz);
-        if (mContext.getSystemService(clazz).getClass().equals(Object.class)) {
-            // Test is using mockito-extended (mContext uses Answers.RETURNS_DEEP_STUBS and returned
-            // a mock object on a final method)
-            doCallRealMethod().when(mContext).getSystemService(clazz);
-        }
-    }
-
-    private Set<Range<Integer>> rangeSet(Range<Integer> ... ranges) {
-        final Set<Range<Integer>> range = new ArraySet<>();
-        for (Range<Integer> r : ranges) range.add(r);
-
-        return range;
-    }
-
-    private static Range<Integer> uidRangeForUser(int userId) {
-        return new Range<Integer>(userId * PER_USER_RANGE, (userId + 1) * PER_USER_RANGE - 1);
-    }
-
-    private Range<Integer> uidRange(int start, int stop) {
-        return new Range<Integer>(start, stop);
-    }
-
-    private static String getPackageByteString(List<String> packages) {
-        try {
-            return HexDump.toHexString(
-                    PersistableBundleUtils.toDiskStableBytes(PersistableBundleUtils.fromList(
-                            packages, PersistableBundleUtils.STRING_SERIALIZER)),
-                        true /* upperCase */);
-        } catch (IOException e) {
-            return null;
-        }
-    }
-
-    @Test
-    public void testRestrictedProfilesAreAddedToVpn() {
-        setMockedUsers(PRIMARY_USER, SECONDARY_USER, RESTRICTED_PROFILE_A, RESTRICTED_PROFILE_B);
-
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-
-        // Assume the user can have restricted profiles.
-        doReturn(true).when(mUserManager).canHaveRestrictedProfile();
-        final Set<Range<Integer>> ranges =
-                vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id, null, null);
-
-        assertEquals(rangeSet(PRIMARY_USER_RANGE, uidRangeForUser(RESTRICTED_PROFILE_A.id)),
-                 ranges);
-    }
-
-    @Test
-    public void testManagedProfilesAreNotAddedToVpn() {
-        setMockedUsers(PRIMARY_USER, MANAGED_PROFILE_A);
-
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final Set<Range<Integer>> ranges = vpn.createUserAndRestrictedProfilesRanges(
-                PRIMARY_USER.id, null, null);
-
-        assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges);
-    }
-
-    @Test
-    public void testAddUserToVpnOnlyAddsOneUser() {
-        setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A, MANAGED_PROFILE_A);
-
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final Set<Range<Integer>> ranges = new ArraySet<>();
-        vpn.addUserToRanges(ranges, PRIMARY_USER.id, null, null);
-
-        assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges);
-    }
-
-    @Test
-    public void testUidAllowAndDenylist() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final Range<Integer> user = PRIMARY_USER_RANGE;
-        final int userStart = user.getLower();
-        final int userStop = user.getUpper();
-        final String[] packages = {PKGS[0], PKGS[1], PKGS[2]};
-
-        // Allowed list
-        final Set<Range<Integer>> allow = vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id,
-                Arrays.asList(packages), null /* disallowedApplications */);
-        assertEquals(rangeSet(
-                uidRange(userStart + PKG_UIDS[0], userStart + PKG_UIDS[0]),
-                uidRange(userStart + PKG_UIDS[1], userStart + PKG_UIDS[2]),
-                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0]),
-                         Process.toSdkSandboxUid(userStart + PKG_UIDS[0])),
-                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[1]),
-                         Process.toSdkSandboxUid(userStart + PKG_UIDS[2]))),
-                allow);
-
-        // Denied list
-        final Set<Range<Integer>> disallow =
-                vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id,
-                        null /* allowedApplications */, Arrays.asList(packages));
-        assertEquals(rangeSet(
-                uidRange(userStart, userStart + PKG_UIDS[0] - 1),
-                uidRange(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
-                /* Empty range between UIDS[1] and UIDS[2], should be excluded, */
-                uidRange(userStart + PKG_UIDS[2] + 1,
-                         Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
-                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
-                         Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
-                uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)),
-                disallow);
-    }
-
-    private void verifyPowerSaveTempWhitelistApp(String packageName) {
-        verify(mDeviceIdleInternal, timeout(TEST_TIMEOUT_MS)).addPowerSaveTempWhitelistApp(
-                anyInt(), eq(packageName), anyLong(), anyInt(), eq(false),
-                eq(PowerWhitelistManager.REASON_VPN), eq("VpnManager event"));
-    }
-
-    @Test
-    public void testGetAlwaysAndOnGetLockDown() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-
-        // Default state.
-        assertFalse(vpn.getAlwaysOn());
-        assertFalse(vpn.getLockdown());
-
-        // Set always-on without lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, Collections.emptyList()));
-        assertTrue(vpn.getAlwaysOn());
-        assertFalse(vpn.getLockdown());
-
-        // Set always-on with lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, Collections.emptyList()));
-        assertTrue(vpn.getAlwaysOn());
-        assertTrue(vpn.getLockdown());
-
-        // Remove always-on configuration.
-        assertTrue(vpn.setAlwaysOnPackage(null, false, Collections.emptyList()));
-        assertFalse(vpn.getAlwaysOn());
-        assertFalse(vpn.getLockdown());
-    }
-
-    @Test
-    public void testAlwaysOnWithoutLockdown() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[1], false /* lockdown */, null /* lockdownAllowlist */));
-        verify(mConnectivityManager, never()).setRequireVpnForUids(anyBoolean(), any());
-
-        assertTrue(vpn.setAlwaysOnPackage(
-                null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
-        verify(mConnectivityManager, never()).setRequireVpnForUids(anyBoolean(), any());
-    }
-
-    @Test
-    public void testLockdownChangingPackage() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final Range<Integer> user = PRIMARY_USER_RANGE;
-        final int userStart = user.getLower();
-        final int userStop = user.getUpper();
-        // Set always-on without lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, null));
-
-        // Set always-on with lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
-        }));
-
-        // Switch to another app.
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null));
-        verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
-        }));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
-        }));
-    }
-
-    @Test
-    public void testLockdownAllowlist() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final Range<Integer> user = PRIMARY_USER_RANGE;
-        final int userStart = user.getLower();
-        final int userStop = user.getUpper();
-        // Set always-on with lockdown and allow app PKGS[2] from lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[1], true, Collections.singletonList(PKGS[2])));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[]  {
-                new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1]) - 1),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
-        }));
-        // Change allowed app list to PKGS[3].
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[1], true, Collections.singletonList(PKGS[3])));
-        verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
-        }));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1),
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
-        }));
-
-        // Change the VPN app.
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[0], true, Collections.singletonList(PKGS[3])));
-        verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1),
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1))
-        }));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart, userStart + PKG_UIDS[0] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1))
-        }));
-
-        // Remove the list of allowed packages.
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null));
-        verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
-        }));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop),
-        }));
-
-        // Add the list of allowed packages.
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[0], true, Collections.singletonList(PKGS[1])));
-        verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop),
-        }));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
-        }));
-
-        // Try allowing a package with a comma, should be rejected.
-        assertFalse(vpn.setAlwaysOnPackage(
-                PKGS[0], true, Collections.singletonList("a.b,c.d")));
-
-        // Pass a non-existent packages in the allowlist, they (and only they) should be ignored.
-        // allowed package should change from PGKS[1] to PKGS[2].
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[0], true, Arrays.asList("com.foo.app", PKGS[2], "com.bar.app")));
-        verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
-        }));
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
-                new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[2] - 1),
-                new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
-                                   Process.toSdkSandboxUid(userStart + PKG_UIDS[2] - 1)),
-                new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
-        }));
-    }
-
-    @Test
-    public void testLockdownSystemUser() throws Exception {
-        final Vpn vpn = createVpn(SYSTEM_USER_ID);
-
-        // Uid 0 is always excluded and PKG_UIDS[1] is the uid of the VPN.
-        final List<Integer> excludedUids = new ArrayList<>(List.of(0, PKG_UIDS[1]));
-        final List<Range<Integer>> ranges = makeVpnUidRange(SYSTEM_USER_ID, excludedUids);
-
-        // Set always-on with lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[1], true /* lockdown */, null /* lockdownAllowlist */));
-        verify(mConnectivityManager).setRequireVpnForUids(true, ranges);
-
-        // Disable always-on with lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(
-                null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
-        verify(mConnectivityManager).setRequireVpnForUids(false, ranges);
-
-        // Set always-on with lockdown and allow the app PKGS[2].
-        excludedUids.add(PKG_UIDS[2]);
-        final List<Range<Integer>> ranges2 = makeVpnUidRange(SYSTEM_USER_ID, excludedUids);
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[1], true /* lockdown */, Collections.singletonList(PKGS[2])));
-        verify(mConnectivityManager).setRequireVpnForUids(true, ranges2);
-
-        // Disable always-on with lockdown.
-        assertTrue(vpn.setAlwaysOnPackage(
-                null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
-        verify(mConnectivityManager).setRequireVpnForUids(false, ranges2);
-    }
-
-    @Test
-    public void testLockdownRuleRepeatability() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
-                new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())};
-        // Given legacy lockdown is already enabled,
-        vpn.setLockdown(true);
-        verify(mConnectivityManager, times(1)).setRequireVpnForUids(true,
-                toRanges(primaryUserRangeParcel));
-
-        // Enabling legacy lockdown twice should do nothing.
-        vpn.setLockdown(true);
-        verify(mConnectivityManager, times(1)).setRequireVpnForUids(anyBoolean(), any());
-
-        // And disabling should remove the rules exactly once.
-        vpn.setLockdown(false);
-        verify(mConnectivityManager, times(1)).setRequireVpnForUids(false,
-                toRanges(primaryUserRangeParcel));
-
-        // Removing the lockdown again should have no effect.
-        vpn.setLockdown(false);
-        verify(mConnectivityManager, times(2)).setRequireVpnForUids(anyBoolean(), any());
-    }
-
-    private ArrayList<Range<Integer>> toRanges(UidRangeParcel[] ranges) {
-        ArrayList<Range<Integer>> rangesArray = new ArrayList<>(ranges.length);
-        for (int i = 0; i < ranges.length; i++) {
-            rangesArray.add(new Range<>(ranges[i].start, ranges[i].stop));
-        }
-        return rangesArray;
-    }
-
-    @Test
-    public void testLockdownRuleReversibility() throws Exception {
-        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final UidRangeParcel[] entireUser = {
-            new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())
-        };
-        final UidRangeParcel[] exceptPkg0 = {
-            new UidRangeParcel(entireUser[0].start, entireUser[0].start + PKG_UIDS[0] - 1),
-            new UidRangeParcel(entireUser[0].start + PKG_UIDS[0] + 1,
-                               Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] - 1)),
-            new UidRangeParcel(Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] + 1),
-                               entireUser[0].stop),
-        };
-
-        final InOrder order = inOrder(mConnectivityManager);
-
-        // Given lockdown is enabled with no package (legacy VPN),
-        vpn.setLockdown(true);
-        order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser));
-
-        // When a new VPN package is set the rules should change to cover that package.
-        vpn.prepare(null, PKGS[0], VpnManager.TYPE_VPN_SERVICE);
-        order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(entireUser));
-        order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(exceptPkg0));
-
-        // When that VPN package is unset, everything should be undone again in reverse.
-        vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE);
-        order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(exceptPkg0));
-        order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser));
-    }
-
-    @Test
-    public void testOnUserAddedAndRemoved_restrictedUser() throws Exception {
-        final InOrder order = inOrder(mMockNetworkAgent);
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final Set<Range<Integer>> initialRange = rangeSet(PRIMARY_USER_RANGE);
-        // Note since mVpnProfile is a Ikev2VpnProfile, this starts an IkeV2VpnRunner.
-        startLegacyVpn(vpn, mVpnProfile);
-        // Set an initial Uid range and mock the network agent
-        vpn.mNetworkCapabilities.setUids(initialRange);
-        vpn.mNetworkAgent = mMockNetworkAgent;
-
-        // Add the restricted user
-        setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
-        vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
-        // Expect restricted user range to be added to the NetworkCapabilities.
-        final Set<Range<Integer>> expectRestrictedRange =
-                rangeSet(PRIMARY_USER_RANGE, uidRangeForUser(RESTRICTED_PROFILE_A.id));
-        assertEquals(expectRestrictedRange, vpn.mNetworkCapabilities.getUids());
-        order.verify(mMockNetworkAgent).doSendNetworkCapabilities(
-                argThat(nc -> expectRestrictedRange.equals(nc.getUids())));
-
-        // Remove the restricted user
-        vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
-        // Expect restricted user range to be removed from the NetworkCapabilities.
-        assertEquals(initialRange, vpn.mNetworkCapabilities.getUids());
-        order.verify(mMockNetworkAgent).doSendNetworkCapabilities(
-                argThat(nc -> initialRange.equals(nc.getUids())));
-    }
-
-    @Test
-    public void testOnUserAddedAndRemoved_restrictedUserLockdown() throws Exception {
-        final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
-                new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())};
-        final Range<Integer> restrictedUserRange = uidRangeForUser(RESTRICTED_PROFILE_A.id);
-        final UidRangeParcel[] restrictedUserRangeParcel = new UidRangeParcel[] {
-                new UidRangeParcel(restrictedUserRange.getLower(), restrictedUserRange.getUpper())};
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-
-        // Set lockdown calls setRequireVpnForUids
-        vpn.setLockdown(true);
-        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(primaryUserRangeParcel));
-
-        // Add the restricted user
-        doReturn(true).when(mUserManager).canHaveRestrictedProfile();
-        setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
-        vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
-
-        // Expect restricted user range to be added.
-        verify(mConnectivityManager).setRequireVpnForUids(true,
-                toRanges(restrictedUserRangeParcel));
-
-        // Mark as partial indicates that the user is removed, mUserManager.getAliveUsers() does not
-        // return the restricted user but it is still returned in mUserManager.getUserInfo().
-        RESTRICTED_PROFILE_A.partial = true;
-        // Remove the restricted user
-        vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
-        verify(mConnectivityManager).setRequireVpnForUids(false,
-                toRanges(restrictedUserRangeParcel));
-        // reset to avoid affecting other tests since RESTRICTED_PROFILE_A is static.
-        RESTRICTED_PROFILE_A.partial = false;
-    }
-
-    @Test
-    public void testOnUserAddedAndRemoved_restrictedUserAlwaysOn() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-
-        // setAlwaysOnPackage() calls setRequireVpnForUids()
-        assertTrue(vpn.setAlwaysOnPackage(
-                PKGS[0], true /* lockdown */, null /* lockdownAllowlist */));
-        final List<Integer> excludedUids = List.of(PKG_UIDS[0]);
-        final List<Range<Integer>> primaryRanges =
-                makeVpnUidRange(PRIMARY_USER.id, excludedUids);
-        verify(mConnectivityManager).setRequireVpnForUids(true, primaryRanges);
-
-        // Add the restricted user
-        doReturn(true).when(mUserManager).canHaveRestrictedProfile();
-        setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
-        vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
-
-        final List<Range<Integer>> restrictedRanges =
-                makeVpnUidRange(RESTRICTED_PROFILE_A.id, excludedUids);
-        // Expect restricted user range to be added.
-        verify(mConnectivityManager).setRequireVpnForUids(true, restrictedRanges);
-
-        // Mark as partial indicates that the user is removed, mUserManager.getAliveUsers() does not
-        // return the restricted user but it is still returned in mUserManager.getUserInfo().
-        RESTRICTED_PROFILE_A.partial = true;
-        // Remove the restricted user
-        vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
-        verify(mConnectivityManager).setRequireVpnForUids(false, restrictedRanges);
-
-        // reset to avoid affecting other tests since RESTRICTED_PROFILE_A is static.
-        RESTRICTED_PROFILE_A.partial = false;
-    }
-
-    @Test
-    public void testPrepare_throwSecurityExceptionWhenGivenPackageDoesNotBelongToTheCaller()
-            throws Exception {
-        mTestDeps.mIgnoreCallingUidChecks = false;
-        final Vpn vpn = createVpn();
-        assertThrows(SecurityException.class,
-                () -> vpn.prepare("com.not.vpn.owner", null, VpnManager.TYPE_VPN_SERVICE));
-        assertThrows(SecurityException.class,
-                () -> vpn.prepare(null, "com.not.vpn.owner", VpnManager.TYPE_VPN_SERVICE));
-        assertThrows(SecurityException.class,
-                () -> vpn.prepare("com.not.vpn.owner1", "com.not.vpn.owner2",
-                        VpnManager.TYPE_VPN_SERVICE));
-    }
-
-    @Test
-    public void testPrepare_bothOldPackageAndNewPackageAreNull() throws Exception {
-        final Vpn vpn = createVpn();
-        assertTrue(vpn.prepare(null, null, VpnManager.TYPE_VPN_SERVICE));
-
-    }
-
-    @Test
-    public void testPrepare_legacyVpnWithoutControlVpn()
-            throws Exception {
-        doThrow(new SecurityException("no CONTROL_VPN")).when(mContext)
-                .enforceCallingOrSelfPermission(eq(CONTROL_VPN), any());
-        final Vpn vpn = createVpn();
-        assertThrows(SecurityException.class,
-                () -> vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE));
-
-        // CONTROL_VPN can be held by the caller or another system server process - both are
-        // allowed. Just checking for `enforceCallingPermission` may not be sufficient.
-        verify(mContext, never()).enforceCallingPermission(eq(CONTROL_VPN), any());
-    }
-
-    @Test
-    public void testPrepare_legacyVpnWithControlVpn()
-            throws Exception {
-        doNothing().when(mContext).enforceCallingOrSelfPermission(eq(CONTROL_VPN), any());
-        final Vpn vpn = createVpn();
-        assertTrue(vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE));
-
-        // CONTROL_VPN can be held by the caller or another system server process - both are
-        // allowed. Just checking for `enforceCallingPermission` may not be sufficient.
-        verify(mContext, never()).enforceCallingPermission(eq(CONTROL_VPN), any());
-    }
-
-    @Test
-    public void testIsAlwaysOnPackageSupported() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-
-        ApplicationInfo appInfo = new ApplicationInfo();
-        when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(PRIMARY_USER.id)))
-                .thenReturn(appInfo);
-
-        ServiceInfo svcInfo = new ServiceInfo();
-        ResolveInfo resInfo = new ResolveInfo();
-        resInfo.serviceInfo = svcInfo;
-        when(mPackageManager.queryIntentServicesAsUser(any(), eq(PackageManager.GET_META_DATA),
-                eq(PRIMARY_USER.id)))
-                .thenReturn(Collections.singletonList(resInfo));
-
-        // null package name should return false
-        assertFalse(vpn.isAlwaysOnPackageSupported(null));
-
-        // Pre-N apps are not supported
-        appInfo.targetSdkVersion = VERSION_CODES.M;
-        assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0]));
-
-        // N+ apps are supported by default
-        appInfo.targetSdkVersion = VERSION_CODES.N;
-        assertTrue(vpn.isAlwaysOnPackageSupported(PKGS[0]));
-
-        // Apps that opt out explicitly are not supported
-        appInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT;
-        Bundle metaData = new Bundle();
-        metaData.putBoolean(VpnService.SERVICE_META_DATA_SUPPORTS_ALWAYS_ON, false);
-        svcInfo.metaData = metaData;
-        assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0]));
-    }
-
-    @Test
-    public void testNotificationShownForAlwaysOnApp() throws Exception {
-        final UserHandle userHandle = UserHandle.of(PRIMARY_USER.id);
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        setMockedUsers(PRIMARY_USER);
-
-        final InOrder order = inOrder(mNotificationManager);
-
-        // Don't show a notification for regular disconnected states.
-        vpn.updateState(DetailedState.DISCONNECTED, TAG);
-        order.verify(mNotificationManager, atLeastOnce()).cancel(anyString(), anyInt());
-
-        // Start showing a notification for disconnected once always-on.
-        vpn.setAlwaysOnPackage(PKGS[0], false, null);
-        order.verify(mNotificationManager).notify(anyString(), anyInt(), any());
-
-        // Stop showing the notification once connected.
-        vpn.updateState(DetailedState.CONNECTED, TAG);
-        order.verify(mNotificationManager).cancel(anyString(), anyInt());
-
-        // Show the notification if we disconnect again.
-        vpn.updateState(DetailedState.DISCONNECTED, TAG);
-        order.verify(mNotificationManager).notify(anyString(), anyInt(), any());
-
-        // Notification should be cleared after unsetting always-on package.
-        vpn.setAlwaysOnPackage(null, false, null);
-        order.verify(mNotificationManager).cancel(anyString(), anyInt());
-    }
-
-    /**
-     * The profile name should NOT change between releases for backwards compatibility
-     *
-     * <p>If this is changed between releases, the {@link Vpn#getVpnProfilePrivileged()} method MUST
-     * be updated to ensure backward compatibility.
-     */
-    @Test
-    public void testGetProfileNameForPackage() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        setMockedUsers(PRIMARY_USER);
-
-        final String expected = Credentials.PLATFORM_VPN + PRIMARY_USER.id + "_" + TEST_VPN_PKG;
-        assertEquals(expected, vpn.getProfileNameForPackage(TEST_VPN_PKG));
-    }
-
-    private Vpn createVpn(String... grantedOps) throws Exception {
-        return createVpn(PRIMARY_USER, grantedOps);
-    }
-
-    private Vpn createVpn(UserInfo user, String... grantedOps) throws Exception {
-        final Vpn vpn = createVpn(user.id);
-        setMockedUsers(user);
-
-        for (final String opStr : grantedOps) {
-            when(mAppOps.noteOpNoThrow(opStr, Process.myUid(), TEST_VPN_PKG,
-                    null /* attributionTag */, null /* message */))
-                    .thenReturn(AppOpsManager.MODE_ALLOWED);
-        }
-
-        return vpn;
-    }
-
-    private void checkProvisionVpnProfile(Vpn vpn, boolean expectedResult, String... checkedOps) {
-        assertEquals(expectedResult, vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile));
-
-        // The profile should always be stored, whether or not consent has been previously granted.
-        verify(mVpnProfileStore)
-                .put(
-                        eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)),
-                        eq(mVpnProfile.encode()));
-
-        for (final String checkedOpStr : checkedOps) {
-            verify(mAppOps).noteOpNoThrow(checkedOpStr, Process.myUid(), TEST_VPN_PKG,
-                    null /* attributionTag */, null /* message */);
-        }
-    }
-
-    @Test
-    public void testProvisionVpnProfileNoIpsecTunnels() throws Exception {
-        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS))
-                .thenReturn(false);
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        try {
-            checkProvisionVpnProfile(
-                    vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-            fail("Expected exception due to missing feature");
-        } catch (UnsupportedOperationException expected) {
-        }
-    }
-
-    private String startVpnForVerifyAppExclusionList(Vpn vpn) throws Exception {
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-        when(mVpnProfileStore.get(PRIMARY_USER_APP_EXCLUDE_KEY))
-                .thenReturn(HexDump.hexStringToByteArray(PKGS_BYTES));
-        final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
-        final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
-                PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
-        verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
-        clearInvocations(mConnectivityManager);
-        verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
-        vpn.mNetworkAgent = mMockNetworkAgent;
-
-        return sessionKey;
-    }
-
-    private Vpn prepareVpnForVerifyAppExclusionList() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        startVpnForVerifyAppExclusionList(vpn);
-
-        return vpn;
-    }
-
-    @Test
-    public void testSetAndGetAppExclusionList() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        final String sessionKey = startVpnForVerifyAppExclusionList(vpn);
-        verify(mVpnProfileStore, never()).put(eq(PRIMARY_USER_APP_EXCLUDE_KEY), any());
-        vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
-        verify(mVpnProfileStore)
-                .put(eq(PRIMARY_USER_APP_EXCLUDE_KEY),
-                     eq(HexDump.hexStringToByteArray(PKGS_BYTES)));
-        final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
-                PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
-        verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
-        assertEquals(uidRanges, vpn.mNetworkCapabilities.getUids());
-        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
-    }
-
-    @Test
-    public void testRefreshPlatformVpnAppExclusionList_updatesExcludedUids() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        final String sessionKey = startVpnForVerifyAppExclusionList(vpn);
-        vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
-        final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
-                PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
-        verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
-        verify(mMockNetworkAgent).doSendNetworkCapabilities(any());
-        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
-
-        reset(mMockNetworkAgent);
-
-        // Remove one of the package
-        List<Integer> newExcludedUids = toList(PKG_UIDS);
-        newExcludedUids.remove((Integer) PKG_UIDS[0]);
-        Set<Range<Integer>> newUidRanges = makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids);
-        sPackages.remove(PKGS[0]);
-        vpn.refreshPlatformVpnAppExclusionList();
-
-        // List in keystore is not changed, but UID for the removed packages is no longer exempted.
-        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
-        assertEquals(newUidRanges, vpn.mNetworkCapabilities.getUids());
-        ArgumentCaptor<NetworkCapabilities> ncCaptor =
-                ArgumentCaptor.forClass(NetworkCapabilities.class);
-        verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
-        assertEquals(newUidRanges, ncCaptor.getValue().getUids());
-        verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(newUidRanges));
-
-        reset(mMockNetworkAgent);
-
-        // Add the package back
-        newExcludedUids.add(PKG_UIDS[0]);
-        newUidRanges = makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids);
-        sPackages.put(PKGS[0], PKG_UIDS[0]);
-        vpn.refreshPlatformVpnAppExclusionList();
-
-        // List in keystore is not changed and the uid list should be updated in the net cap.
-        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
-        assertEquals(newUidRanges, vpn.mNetworkCapabilities.getUids());
-        verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
-        assertEquals(newUidRanges, ncCaptor.getValue().getUids());
-
-        // The uidRange is the same as the original setAppExclusionList so this is the second call
-        verify(mConnectivityManager, times(2))
-                .setVpnDefaultForUids(eq(sessionKey), eq(newUidRanges));
-    }
-
-    private List<Range<Integer>> makeVpnUidRange(int userId, List<Integer> excludedAppIdList) {
-        final SortedSet<Integer> list = new TreeSet<>();
-
-        final int userBase = userId * UserHandle.PER_USER_RANGE;
-        for (int appId : excludedAppIdList) {
-            final int uid = UserHandle.getUid(userId, appId);
-            list.add(uid);
-            if (Process.isApplicationUid(uid)) {
-                list.add(Process.toSdkSandboxUid(uid)); // Add Sdk Sandbox UID
-            }
-        }
-
-        final int minUid = userBase;
-        final int maxUid = userBase + UserHandle.PER_USER_RANGE - 1;
-        final List<Range<Integer>> ranges = new ArrayList<>();
-
-        // Iterate the list to create the ranges between each uid.
-        int start = minUid;
-        for (int uid : list) {
-            if (uid == start) {
-                start++;
-            } else {
-                ranges.add(new Range<>(start, uid - 1));
-                start = uid + 1;
-            }
-        }
-
-        // Create the range between last uid and max uid.
-        if (start <= maxUid) {
-            ranges.add(new Range<>(start, maxUid));
-        }
-
-        return ranges;
-    }
-
-    private Set<Range<Integer>> makeVpnUidRangeSet(int userId, List<Integer> excludedAppIdList) {
-        return new ArraySet<>(makeVpnUidRange(userId, excludedAppIdList));
-    }
-
-    @Test
-    public void testSetAndGetAppExclusionListRestrictedUser() throws Exception {
-        final Vpn vpn = prepareVpnForVerifyAppExclusionList();
-
-        // Mock it to restricted profile
-        when(mUserManager.getUserInfo(anyInt())).thenReturn(RESTRICTED_PROFILE_A);
-
-        // Restricted users cannot configure VPNs
-        assertThrows(SecurityException.class,
-                () -> vpn.setAppExclusionList(TEST_VPN_PKG, new ArrayList<>()));
-
-        assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
-    }
-
-    @Test
-    public void testProvisionVpnProfilePreconsented() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        checkProvisionVpnProfile(
-                vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-    }
-
-    @Test
-    public void testProvisionVpnProfileNotPreconsented() throws Exception {
-        final Vpn vpn = createVpn();
-
-        // Expect that both the ACTIVATE_VPN and ACTIVATE_PLATFORM_VPN were tried, but the caller
-        // had neither.
-        checkProvisionVpnProfile(vpn, false /* expectedResult */,
-                AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN, AppOpsManager.OPSTR_ACTIVATE_VPN);
-    }
-
-    @Test
-    public void testProvisionVpnProfileVpnServicePreconsented() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_VPN);
-
-        checkProvisionVpnProfile(vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_VPN);
-    }
-
-    @Test
-    public void testProvisionVpnProfileTooLarge() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        final VpnProfile bigProfile = new VpnProfile("");
-        bigProfile.name = new String(new byte[Vpn.MAX_VPN_PROFILE_SIZE_BYTES + 1]);
-
-        try {
-            vpn.provisionVpnProfile(TEST_VPN_PKG, bigProfile);
-            fail("Expected IAE due to profile size");
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test
-    public void testProvisionVpnProfileRestrictedUser() throws Exception {
-        final Vpn vpn =
-                createVpn(
-                        RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        try {
-            vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile);
-            fail("Expected SecurityException due to restricted user");
-        } catch (SecurityException expected) {
-        }
-    }
-
-    @Test
-    public void testDeleteVpnProfile() throws Exception {
-        final Vpn vpn = createVpn();
-
-        vpn.deleteVpnProfile(TEST_VPN_PKG);
-
-        verify(mVpnProfileStore)
-                .remove(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
-    }
-
-    @Test
-    public void testDeleteVpnProfileRestrictedUser() throws Exception {
-        final Vpn vpn =
-                createVpn(
-                        RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        try {
-            vpn.deleteVpnProfile(TEST_VPN_PKG);
-            fail("Expected SecurityException due to restricted user");
-        } catch (SecurityException expected) {
-        }
-    }
-
-    @Test
-    public void testGetVpnProfilePrivileged() throws Exception {
-        final Vpn vpn = createVpn();
-
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(new VpnProfile("").encode());
-
-        vpn.getVpnProfilePrivileged(TEST_VPN_PKG);
-
-        verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
-    }
-
-    private void verifyPlatformVpnIsActivated(String packageName) {
-        verify(mAppOps).noteOpNoThrow(
-                eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
-                eq(Process.myUid()),
-                eq(packageName),
-                eq(null) /* attributionTag */,
-                eq(null) /* message */);
-        verify(mAppOps).startOp(
-                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
-                eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
-                eq(packageName),
-                eq(null) /* attributionTag */,
-                eq(null) /* message */);
-    }
-
-    private void verifyPlatformVpnIsDeactivated(String packageName) {
-        // Add a small delay to double confirm that finishOp is only called once.
-        verify(mAppOps, after(100)).finishOp(
-                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
-                eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
-                eq(packageName),
-                eq(null) /* attributionTag */);
-    }
-
-    @Test
-    public void testStartVpnProfile() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-
-        vpn.startVpnProfile(TEST_VPN_PKG);
-
-        verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
-        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
-    }
-
-    @Test
-    public void testStartVpnProfileVpnServicePreconsented() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_VPN);
-
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-
-        vpn.startVpnProfile(TEST_VPN_PKG);
-
-        // Verify that the ACTIVATE_VPN appop was checked, but no error was thrown.
-        verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(),
-                TEST_VPN_PKG, null /* attributionTag */, null /* message */);
-    }
-
-    @Test
-    public void testStartVpnProfileNotConsented() throws Exception {
-        final Vpn vpn = createVpn();
-
-        try {
-            vpn.startVpnProfile(TEST_VPN_PKG);
-            fail("Expected failure due to no user consent");
-        } catch (SecurityException expected) {
-        }
-
-        // Verify both appops were checked.
-        verify(mAppOps)
-                .noteOpNoThrow(
-                        eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
-                        eq(Process.myUid()),
-                        eq(TEST_VPN_PKG),
-                        eq(null) /* attributionTag */,
-                        eq(null) /* message */);
-        verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(),
-                TEST_VPN_PKG, null /* attributionTag */, null /* message */);
-
-        // Keystore should never have been accessed.
-        verify(mVpnProfileStore, never()).get(any());
-    }
-
-    @Test
-    public void testStartVpnProfileMissingProfile() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))).thenReturn(null);
-
-        try {
-            vpn.startVpnProfile(TEST_VPN_PKG);
-            fail("Expected failure due to missing profile");
-        } catch (IllegalArgumentException expected) {
-        }
-
-        verify(mVpnProfileStore).get(vpn.getProfileNameForPackage(TEST_VPN_PKG));
-        verify(mAppOps)
-                .noteOpNoThrow(
-                        eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
-                        eq(Process.myUid()),
-                        eq(TEST_VPN_PKG),
-                        eq(null) /* attributionTag */,
-                        eq(null) /* message */);
-    }
-
-    @Test
-    public void testStartVpnProfileRestrictedUser() throws Exception {
-        final Vpn vpn = createVpn(RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        try {
-            vpn.startVpnProfile(TEST_VPN_PKG);
-            fail("Expected SecurityException due to restricted user");
-        } catch (SecurityException expected) {
-        }
-    }
-
-    @Test
-    public void testStopVpnProfileRestrictedUser() throws Exception {
-        final Vpn vpn = createVpn(RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-
-        try {
-            vpn.stopVpnProfile(TEST_VPN_PKG);
-            fail("Expected SecurityException due to restricted user");
-        } catch (SecurityException expected) {
-        }
-    }
-
-    @Test
-    public void testStartOpAndFinishOpWillBeCalledWhenPlatformVpnIsOnAndOff() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-        vpn.startVpnProfile(TEST_VPN_PKG);
-        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
-        // Add a small delay to make sure that startOp is only called once.
-        verify(mAppOps, after(100).times(1)).startOp(
-                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
-                eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
-                eq(TEST_VPN_PKG),
-                eq(null) /* attributionTag */,
-                eq(null) /* message */);
-        // Check that the startOp is not called with OPSTR_ESTABLISH_VPN_SERVICE.
-        verify(mAppOps, never()).startOp(
-                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE),
-                eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
-                eq(TEST_VPN_PKG),
-                eq(null) /* attributionTag */,
-                eq(null) /* message */);
-        vpn.stopVpnProfile(TEST_VPN_PKG);
-        verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
-    }
-
-    @Test
-    public void testStartOpWithSeamlessHandover() throws Exception {
-        // Create with SYSTEM_USER so that establish() will match the user ID when checking
-        // against Binder.getCallerUid
-        final Vpn vpn = createVpn(SYSTEM_USER, AppOpsManager.OPSTR_ACTIVATE_VPN);
-        assertTrue(vpn.prepare(TEST_VPN_PKG, null, VpnManager.TYPE_VPN_SERVICE));
-        final VpnConfig config = new VpnConfig();
-        config.user = "VpnTest";
-        config.addresses.add(new LinkAddress("192.0.2.2/32"));
-        config.mtu = 1450;
-        final ResolveInfo resolveInfo = new ResolveInfo();
-        final ServiceInfo serviceInfo = new ServiceInfo();
-        serviceInfo.permission = BIND_VPN_SERVICE;
-        resolveInfo.serviceInfo = serviceInfo;
-        when(mPackageManager.resolveService(any(), anyInt())).thenReturn(resolveInfo);
-        when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true);
-        vpn.establish(config);
-        verify(mAppOps, times(1)).startOp(
-                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE),
-                eq(Process.myUid()),
-                eq(TEST_VPN_PKG),
-                eq(null) /* attributionTag */,
-                eq(null) /* message */);
-        // Call establish() twice with the same config, it should match seamless handover case and
-        // startOp() shouldn't be called again.
-        vpn.establish(config);
-        verify(mAppOps, times(1)).startOp(
-                eq(AppOpsManager.OPSTR_ESTABLISH_VPN_SERVICE),
-                eq(Process.myUid()),
-                eq(TEST_VPN_PKG),
-                eq(null) /* attributionTag */,
-                eq(null) /* message */);
-    }
-
-    private void verifyVpnManagerEvent(String sessionKey, String category, int errorClass,
-            int errorCode, String[] packageName, @NonNull VpnProfileState... profileState) {
-        final Context userContext =
-                mContext.createContextAsUser(UserHandle.of(PRIMARY_USER.id), 0 /* flags */);
-        final ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
-
-        final int verifyTimes = profileState.length;
-        verify(userContext, timeout(TEST_TIMEOUT_MS).times(verifyTimes))
-                .startService(intentArgumentCaptor.capture());
-
-        for (int i = 0; i < verifyTimes; i++) {
-            final Intent intent = intentArgumentCaptor.getAllValues().get(i);
-            assertEquals(packageName[i], intent.getPackage());
-            assertEquals(sessionKey, intent.getStringExtra(VpnManager.EXTRA_SESSION_KEY));
-            final Set<String> categories = intent.getCategories();
-            assertTrue(categories.contains(category));
-            assertEquals(1, categories.size());
-            assertEquals(errorClass,
-                    intent.getIntExtra(VpnManager.EXTRA_ERROR_CLASS, -1 /* defaultValue */));
-            assertEquals(errorCode,
-                    intent.getIntExtra(VpnManager.EXTRA_ERROR_CODE, -1 /* defaultValue */));
-            // CATEGORY_EVENT_DEACTIVATED_BY_USER & CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED won't
-            // send NetworkCapabilities & LinkProperties to VPN app.
-            // For ERROR_CODE_NETWORK_LOST, the NetworkCapabilities & LinkProperties of underlying
-            // network will be cleared. So the VPN app will receive null for those 2 extra values.
-            if (category.equals(VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER)
-                    || category.equals(VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED)
-                    || errorCode == VpnManager.ERROR_CODE_NETWORK_LOST) {
-                assertNull(intent.getParcelableExtra(
-                        VpnManager.EXTRA_UNDERLYING_NETWORK_CAPABILITIES));
-                assertNull(intent.getParcelableExtra(VpnManager.EXTRA_UNDERLYING_LINK_PROPERTIES));
-            } else {
-                assertNotNull(intent.getParcelableExtra(
-                        VpnManager.EXTRA_UNDERLYING_NETWORK_CAPABILITIES));
-                assertNotNull(intent.getParcelableExtra(
-                        VpnManager.EXTRA_UNDERLYING_LINK_PROPERTIES));
-            }
-
-            assertEquals(profileState[i], intent.getParcelableExtra(
-                    VpnManager.EXTRA_VPN_PROFILE_STATE, VpnProfileState.class));
-        }
-        reset(userContext);
-    }
-
-    private void verifyDeactivatedByUser(String sessionKey, String[] packageName) {
-        // CATEGORY_EVENT_DEACTIVATED_BY_USER is not an error event, so both of errorClass and
-        // errorCode won't be set.
-        verifyVpnManagerEvent(sessionKey, VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
-                -1 /* errorClass */, -1 /* errorCode */, packageName,
-                // VPN NetworkAgnet does not switch to CONNECTED in the test, and the state is not
-                // important here. Verify that the state as it is, i.e. CONNECTING state.
-                new VpnProfileState(VpnProfileState.STATE_CONNECTING,
-                        sessionKey, false /* alwaysOn */, false /* lockdown */));
-    }
-
-    private void verifyAlwaysOnStateChanged(String[] packageName, VpnProfileState... profileState) {
-        verifyVpnManagerEvent(null /* sessionKey */,
-                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
-                -1 /* errorCode */, packageName, profileState);
-    }
-
-    @Test
-    public void testVpnManagerEventForUserDeactivated() throws Exception {
-        // For security reasons, Vpn#prepare() will check that oldPackage and newPackage are either
-        // null or the package of the caller. This test will call Vpn#prepare() to pretend the old
-        // VPN is replaced by a new one. But only Settings can change to some other packages, and
-        // this is checked with CONTROL_VPN so simulate holding CONTROL_VPN in order to pass the
-        // security checks.
-        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-
-        // Test the case that the user deactivates the vpn in vpn app.
-        final String sessionKey1 = vpn.startVpnProfile(TEST_VPN_PKG);
-        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
-        vpn.stopVpnProfile(TEST_VPN_PKG);
-        verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
-        verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
-        reset(mDeviceIdleInternal);
-        verifyDeactivatedByUser(sessionKey1, new String[] {TEST_VPN_PKG});
-        reset(mAppOps);
-
-        // Test the case that the user chooses another vpn and the original one is replaced.
-        final String sessionKey2 = vpn.startVpnProfile(TEST_VPN_PKG);
-        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
-        vpn.prepare(TEST_VPN_PKG, "com.new.vpn" /* newPackage */, TYPE_VPN_PLATFORM);
-        verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
-        verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
-        reset(mDeviceIdleInternal);
-        verifyDeactivatedByUser(sessionKey2, new String[] {TEST_VPN_PKG});
-    }
-
-    @Test
-    public void testVpnManagerEventForAlwaysOnChanged() throws Exception {
-        // Calling setAlwaysOnPackage() needs to hold CONTROL_VPN.
-        doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        // Enable VPN always-on for PKGS[1].
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
-                null /* lockdownAllowlist */));
-        verifyPowerSaveTempWhitelistApp(PKGS[1]);
-        reset(mDeviceIdleInternal);
-        verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
-                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
-                        null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
-
-        // Enable VPN lockdown for PKGS[1].
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true /* lockdown */,
-                null /* lockdownAllowlist */));
-        verifyPowerSaveTempWhitelistApp(PKGS[1]);
-        reset(mDeviceIdleInternal);
-        verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
-                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
-                        null /* sessionKey */, true /* alwaysOn */, true /* lockdown */));
-
-        // Disable VPN lockdown for PKGS[1].
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
-                null /* lockdownAllowlist */));
-        verifyPowerSaveTempWhitelistApp(PKGS[1]);
-        reset(mDeviceIdleInternal);
-        verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
-                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
-                        null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
-
-        // Disable VPN always-on.
-        assertTrue(vpn.setAlwaysOnPackage(null, false /* lockdown */,
-                null /* lockdownAllowlist */));
-        verifyPowerSaveTempWhitelistApp(PKGS[1]);
-        reset(mDeviceIdleInternal);
-        verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
-                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
-                        null /* sessionKey */, false /* alwaysOn */, false /* lockdown */));
-
-        // Enable VPN always-on for PKGS[1] again.
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
-                null /* lockdownAllowlist */));
-        verifyPowerSaveTempWhitelistApp(PKGS[1]);
-        reset(mDeviceIdleInternal);
-        verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
-                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
-                        null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
-
-        // Enable VPN always-on for PKGS[2].
-        assertTrue(vpn.setAlwaysOnPackage(PKGS[2], false /* lockdown */,
-                null /* lockdownAllowlist */));
-        verifyPowerSaveTempWhitelistApp(PKGS[2]);
-        reset(mDeviceIdleInternal);
-        // PKGS[1] is replaced with PKGS[2].
-        // Pass 2 VpnProfileState objects to verifyVpnManagerEvent(), the first one is sent to
-        // PKGS[1] to notify PKGS[1] that the VPN always-on is disabled, the second one is sent to
-        // PKGS[2] to notify PKGS[2] that the VPN always-on is enabled.
-        verifyAlwaysOnStateChanged(new String[] {PKGS[1], PKGS[2]},
-                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
-                        null /* sessionKey */, false /* alwaysOn */, false /* lockdown */),
-                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
-                        null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
-    }
-
-    @Test
-    public void testReconnectVpnManagerVpnWithAlwaysOnEnabled() throws Exception {
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-        vpn.startVpnProfile(TEST_VPN_PKG);
-        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
-
-        // Enable VPN always-on for TEST_VPN_PKG.
-        assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, false /* lockdown */,
-                null /* lockdownAllowlist */));
-
-        // Reset to verify next startVpnProfile.
-        reset(mAppOps);
-
-        vpn.stopVpnProfile(TEST_VPN_PKG);
-
-        // Reconnect the vpn with different package will cause exception.
-        assertThrows(SecurityException.class, () -> vpn.startVpnProfile(PKGS[0]));
-
-        // Reconnect the vpn again with the vpn always on package w/o exception.
-        vpn.startVpnProfile(TEST_VPN_PKG);
-        verifyPlatformVpnIsActivated(TEST_VPN_PKG);
-    }
-
-    @Test
-    public void testLockdown_enableDisableWhileConnected() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
-        final InOrder order = inOrder(mTestDeps);
-        order.verify(mTestDeps, timeout(TIMEOUT_CROSSTHREAD_MS))
-                .newNetworkAgent(any(), any(), any(), any(), any(), any(),
-                        argThat(config -> config.allowBypass), any(), any());
-
-        // Make VPN lockdown.
-        assertTrue(vpnSnapShot.vpn.setAlwaysOnPackage(TEST_VPN_PKG, true /* lockdown */,
-                null /* lockdownAllowlist */));
-
-        order.verify(mTestDeps, timeout(TIMEOUT_CROSSTHREAD_MS))
-                .newNetworkAgent(any(), any(), any(), any(), any(), any(),
-                argThat(config -> !config.allowBypass), any(), any());
-
-        // Disable lockdown.
-        assertTrue(vpnSnapShot.vpn.setAlwaysOnPackage(TEST_VPN_PKG, false /* lockdown */,
-                null /* lockdownAllowlist */));
-
-        order.verify(mTestDeps, timeout(TIMEOUT_CROSSTHREAD_MS))
-                .newNetworkAgent(any(), any(), any(), any(), any(), any(),
-                        argThat(config -> config.allowBypass), any(), any());
-    }
-
-    @Test
-    public void testSetPackageAuthorizationVpnService() throws Exception {
-        final Vpn vpn = createVpn();
-
-        assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_SERVICE));
-        verify(mAppOps)
-                .setMode(
-                        eq(AppOpsManager.OPSTR_ACTIVATE_VPN),
-                        eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
-                        eq(TEST_VPN_PKG),
-                        eq(AppOpsManager.MODE_ALLOWED));
-    }
-
-    @Test
-    public void testSetPackageAuthorizationPlatformVpn() throws Exception {
-        final Vpn vpn = createVpn();
-
-        assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, TYPE_VPN_PLATFORM));
-        verify(mAppOps)
-                .setMode(
-                        eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
-                        eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
-                        eq(TEST_VPN_PKG),
-                        eq(AppOpsManager.MODE_ALLOWED));
-    }
-
-    @Test
-    public void testSetPackageAuthorizationRevokeAuthorization() throws Exception {
-        final Vpn vpn = createVpn();
-
-        assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_NONE));
-        verify(mAppOps)
-                .setMode(
-                        eq(AppOpsManager.OPSTR_ACTIVATE_VPN),
-                        eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
-                        eq(TEST_VPN_PKG),
-                        eq(AppOpsManager.MODE_IGNORED));
-        verify(mAppOps)
-                .setMode(
-                        eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
-                        eq(UserHandle.getUid(PRIMARY_USER.id, Process.myUid())),
-                        eq(TEST_VPN_PKG),
-                        eq(AppOpsManager.MODE_IGNORED));
-    }
-
-    private NetworkCallback triggerOnAvailableAndGetCallback() throws Exception {
-        return triggerOnAvailableAndGetCallback(new NetworkCapabilities.Builder().build());
-    }
-
-    private NetworkCallback triggerOnAvailableAndGetCallback(
-            @NonNull final NetworkCapabilities caps) throws Exception {
-        final ArgumentCaptor<NetworkCallback> networkCallbackCaptor =
-                ArgumentCaptor.forClass(NetworkCallback.class);
-        verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
-                .registerSystemDefaultNetworkCallback(networkCallbackCaptor.capture(), any());
-
-        // onAvailable() will trigger onDefaultNetworkChanged(), so NetdUtils#setInterfaceUp will be
-        // invoked. Set the return value of INetd#interfaceGetCfg to prevent NullPointerException.
-        final InterfaceConfigurationParcel config = new InterfaceConfigurationParcel();
-        config.flags = new String[] {IF_STATE_DOWN};
-        when(mNetd.interfaceGetCfg(anyString())).thenReturn(config);
-        final NetworkCallback cb = networkCallbackCaptor.getValue();
-        cb.onAvailable(TEST_NETWORK);
-        // Trigger onCapabilitiesChanged() and onLinkPropertiesChanged() so the test can verify that
-        // if NetworkCapabilities and LinkProperties of underlying network will be sent/cleared or
-        // not.
-        // See verifyVpnManagerEvent().
-        cb.onCapabilitiesChanged(TEST_NETWORK, caps);
-        cb.onLinkPropertiesChanged(TEST_NETWORK, new LinkProperties());
-        return cb;
-    }
-
-    private void verifyInterfaceSetCfgWithFlags(String flag) throws Exception {
-        // Add a timeout for waiting for interfaceSetCfg to be called.
-        verify(mNetd, timeout(TEST_TIMEOUT_MS)).interfaceSetCfg(argThat(
-                config -> Arrays.asList(config.flags).contains(flag)));
-    }
-
-    private void doTestPlatformVpnWithException(IkeException exception,
-            String category, int errorType, int errorCode) throws Exception {
-        final ArgumentCaptor<IkeSessionCallback> captor =
-                ArgumentCaptor.forClass(IkeSessionCallback.class);
-
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-
-        doReturn(new NetworkCapabilities()).when(mConnectivityManager)
-                .getRedactedNetworkCapabilitiesForPackage(any(), anyInt(), anyString());
-        doReturn(new LinkProperties()).when(mConnectivityManager)
-                .getRedactedLinkPropertiesForPackage(any(), anyInt(), anyString());
-
-        final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
-        final Set<Range<Integer>> uidRanges = rangeSet(PRIMARY_USER_RANGE);
-        // This is triggered by Ikev2VpnRunner constructor.
-        verify(mConnectivityManager, times(1)).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
-        final NetworkCallback cb = triggerOnAvailableAndGetCallback();
-
-        verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
-
-        // Wait for createIkeSession() to be called before proceeding in order to ensure consistent
-        // state
-        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
-                .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
-        // This is triggered by Vpn#startOrMigrateIkeSession().
-        verify(mConnectivityManager, times(2)).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
-        reset(mIkev2SessionCreator);
-        // For network lost case, the process should be triggered by calling onLost(), which is the
-        // same process with the real case.
-        if (errorCode == VpnManager.ERROR_CODE_NETWORK_LOST) {
-            cb.onLost(TEST_NETWORK);
-            verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any());
-        } else {
-            final IkeSessionCallback ikeCb = captor.getValue();
-            mExecutor.execute(() -> ikeCb.onClosedWithException(exception));
-        }
-
-        verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
-        reset(mDeviceIdleInternal);
-        verifyVpnManagerEvent(sessionKey, category, errorType, errorCode,
-                // VPN NetworkAgnet does not switch to CONNECTED in the test, and the state is not
-                // important here. Verify that the state as it is, i.e. CONNECTING state.
-                new String[] {TEST_VPN_PKG}, new VpnProfileState(VpnProfileState.STATE_CONNECTING,
-                        sessionKey, false /* alwaysOn */, false /* lockdown */));
-        if (errorType == VpnManager.ERROR_CLASS_NOT_RECOVERABLE) {
-            verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey),
-                    eq(Collections.EMPTY_LIST));
-            verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
-                    .unregisterNetworkCallback(eq(cb));
-        } else if (errorType == VpnManager.ERROR_CLASS_RECOVERABLE
-                // Vpn won't retry when there is no usable underlying network.
-                && errorCode != VpnManager.ERROR_CODE_NETWORK_LOST) {
-            int retryIndex = 0;
-            // First failure occurred above.
-            final IkeSessionCallback retryCb = verifyRetryAndGetNewIkeCb(retryIndex++);
-            // Trigger 2 more failures to let the retry delay increase to 5s.
-            mExecutor.execute(() -> retryCb.onClosedWithException(exception));
-            final IkeSessionCallback retryCb2 = verifyRetryAndGetNewIkeCb(retryIndex++);
-            mExecutor.execute(() -> retryCb2.onClosedWithException(exception));
-            final IkeSessionCallback retryCb3 = verifyRetryAndGetNewIkeCb(retryIndex++);
-
-            // setVpnDefaultForUids may be called again but the uidRanges should not change.
-            verify(mConnectivityManager, atLeast(2)).setVpnDefaultForUids(eq(sessionKey),
-                    mUidRangesCaptor.capture());
-            final List<Collection<Range<Integer>>> capturedUidRanges =
-                    mUidRangesCaptor.getAllValues();
-            for (int i = 2; i < capturedUidRanges.size(); i++) {
-                // Assert equals no order.
-                assertTrue(
-                        "uid ranges should not be modified. Expected: " + uidRanges
-                                + ", actual: " + capturedUidRanges.get(i),
-                        capturedUidRanges.get(i).containsAll(uidRanges)
-                                && capturedUidRanges.get(i).size() == uidRanges.size());
-            }
-
-            // A fourth failure will cause the retry delay to be greater than 5s.
-            mExecutor.execute(() -> retryCb3.onClosedWithException(exception));
-            verifyRetryAndGetNewIkeCb(retryIndex++);
-
-            // The VPN network preference will be cleared when the retry delay is greater than 5s.
-            verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey),
-                    eq(Collections.EMPTY_LIST));
-        }
-    }
-
-    private IkeSessionCallback verifyRetryAndGetNewIkeCb(int retryIndex) {
-        final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
-                ArgumentCaptor.forClass(IkeSessionCallback.class);
-
-        // Verify retry is scheduled
-        final long expectedDelayMs = mTestDeps.getNextRetryDelayMs(retryIndex);
-        verify(mExecutor, timeout(TEST_TIMEOUT_MS)).schedule(any(Runnable.class),
-                eq(expectedDelayMs), eq(TimeUnit.MILLISECONDS));
-
-        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS + expectedDelayMs))
-                .createIkeSession(any(), any(), any(), any(), ikeCbCaptor.capture(), any());
-
-        // Forget the mIkev2SessionCreator#createIkeSession call and mExecutor#schedule call
-        // for the next retry verification
-        resetIkev2SessionCreator(mIkeSessionWrapper);
-
-        return ikeCbCaptor.getValue();
-    }
-
-    @Test
-    public void testStartPlatformVpnAuthenticationFailed() throws Exception {
-        final IkeProtocolException exception = mock(IkeProtocolException.class);
-        final int errorCode = IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED;
-        when(exception.getErrorType()).thenReturn(errorCode);
-        doTestPlatformVpnWithException(exception,
-                VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_NOT_RECOVERABLE,
-                errorCode);
-    }
-
-    @Test
-    public void testStartPlatformVpnFailedWithRecoverableError() throws Exception {
-        final IkeProtocolException exception = mock(IkeProtocolException.class);
-        final int errorCode = IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE;
-        when(exception.getErrorType()).thenReturn(errorCode);
-        doTestPlatformVpnWithException(exception,
-                VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, errorCode);
-    }
-
-    @Test
-    public void testStartPlatformVpnFailedWithUnknownHostException() throws Exception {
-        final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
-        final UnknownHostException unknownHostException = new UnknownHostException();
-        final int errorCode = VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST;
-        when(exception.getCause()).thenReturn(unknownHostException);
-        doTestPlatformVpnWithException(exception,
-                VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
-                errorCode);
-    }
-
-    @Test
-    public void testStartPlatformVpnFailedWithIkeTimeoutException() throws Exception {
-        final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
-        final IkeTimeoutException ikeTimeoutException =
-                new IkeTimeoutException("IkeTimeoutException");
-        final int errorCode = VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT;
-        when(exception.getCause()).thenReturn(ikeTimeoutException);
-        doTestPlatformVpnWithException(exception,
-                VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
-                errorCode);
-    }
-
-    @Test
-    public void testStartPlatformVpnFailedWithIkeNetworkLostException() throws Exception {
-        final IkeNetworkLostException exception = new IkeNetworkLostException(
-                new Network(100));
-        doTestPlatformVpnWithException(exception,
-                VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
-                VpnManager.ERROR_CODE_NETWORK_LOST);
-    }
-
-    @Test
-    public void testStartPlatformVpnFailedWithIOException() throws Exception {
-        final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
-        final IOException ioException = new IOException();
-        final int errorCode = VpnManager.ERROR_CODE_NETWORK_IO;
-        when(exception.getCause()).thenReturn(ioException);
-        doTestPlatformVpnWithException(exception,
-                VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
-                errorCode);
-    }
-
-    @Test
-    public void testStartPlatformVpnIllegalArgumentExceptionInSetup() throws Exception {
-        when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
-                .thenThrow(new IllegalArgumentException());
-        final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), mVpnProfile);
-        final NetworkCallback cb = triggerOnAvailableAndGetCallback();
-
-        verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
-
-        // Wait for createIkeSession() to be called before proceeding in order to ensure consistent
-        // state
-        verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)).unregisterNetworkCallback(eq(cb));
-        assertEquals(LegacyVpnInfo.STATE_FAILED, vpn.getLegacyVpnInfo().state);
-    }
-
-    @Test
-    public void testVpnManagerEventWillNotBeSentToSettingsVpn() throws Exception {
-        startLegacyVpn(createVpn(PRIMARY_USER.id), mVpnProfile);
-        triggerOnAvailableAndGetCallback();
-
-        verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
-
-        final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
-        final IkeTimeoutException ikeTimeoutException =
-                new IkeTimeoutException("IkeTimeoutException");
-        when(exception.getCause()).thenReturn(ikeTimeoutException);
-
-        final ArgumentCaptor<IkeSessionCallback> captor =
-                ArgumentCaptor.forClass(IkeSessionCallback.class);
-        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
-                .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
-        final IkeSessionCallback ikeCb = captor.getValue();
-        ikeCb.onClosedWithException(exception);
-
-        final Context userContext =
-                mContext.createContextAsUser(UserHandle.of(PRIMARY_USER.id), 0 /* flags */);
-        verify(userContext, never()).startService(any());
-    }
-
-    private void setAndVerifyAlwaysOnPackage(Vpn vpn, int uid, boolean lockdownEnabled) {
-        assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, lockdownEnabled, null));
-
-        verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
-        verify(mAppOps).setMode(
-                eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), eq(uid), eq(TEST_VPN_PKG),
-                eq(AppOpsManager.MODE_ALLOWED));
-
-        verify(mSystemServices).settingsSecurePutStringForUser(
-                eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(PRIMARY_USER.id));
-        verify(mSystemServices).settingsSecurePutIntForUser(
-                eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN), eq(lockdownEnabled ? 1 : 0),
-                eq(PRIMARY_USER.id));
-        verify(mSystemServices).settingsSecurePutStringForUser(
-                eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(PRIMARY_USER.id));
-    }
-
-    @Test
-    public void testSetAndStartAlwaysOnVpn() throws Exception {
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        setMockedUsers(PRIMARY_USER);
-
-        // UID checks must return a different UID; otherwise it'll be treated as already prepared.
-        final int uid = Process.myUid() + 1;
-        when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt()))
-                .thenReturn(uid);
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(mVpnProfile.encode());
-
-        setAndVerifyAlwaysOnPackage(vpn, uid, false);
-        assertTrue(vpn.startAlwaysOnVpn());
-
-        // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in
-        // a subsequent CL.
-    }
-
-    private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
-        setMockedUsers(PRIMARY_USER);
-        vpn.startLegacyVpn(vpnProfile);
-        return vpn;
-    }
-
-    private IkeSessionConnectionInfo createIkeConnectInfo() {
-        return new IkeSessionConnectionInfo(TEST_VPN_CLIENT_IP, TEST_VPN_SERVER_IP, TEST_NETWORK);
-    }
-
-    private IkeSessionConnectionInfo createIkeConnectInfo_2() {
-        return new IkeSessionConnectionInfo(
-                TEST_VPN_CLIENT_IP_2, TEST_VPN_SERVER_IP_2, TEST_NETWORK_2);
-    }
-
-    private IkeSessionConfiguration createIkeConfig(
-            IkeSessionConnectionInfo ikeConnectInfo, boolean isMobikeEnabled) {
-        final IkeSessionConfiguration.Builder builder =
-                new IkeSessionConfiguration.Builder(ikeConnectInfo);
-
-        if (isMobikeEnabled) {
-            builder.addIkeExtension(EXTENSION_TYPE_MOBIKE);
-        }
-
-        return builder.build();
-    }
-
-    private ChildSessionConfiguration createChildConfig() {
-        return new ChildSessionConfiguration.Builder(
-                        Arrays.asList(IN_TS, IN_TS6), Arrays.asList(OUT_TS, OUT_TS6))
-                .addInternalAddress(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN))
-                .addInternalAddress(new LinkAddress(TEST_VPN_INTERNAL_IP6, IP6_PREFIX_LEN))
-                .addInternalDnsServer(TEST_VPN_INTERNAL_DNS)
-                .addInternalDnsServer(TEST_VPN_INTERNAL_DNS6)
-                .build();
-    }
-
-    private IpSecTransform createIpSecTransform() {
-        return new IpSecTransform(mContext, new IpSecConfig());
-    }
-
-    private void verifyApplyTunnelModeTransforms(int expectedTimes) throws Exception {
-        verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform(
-                eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_IN),
-                anyInt(), anyString());
-        verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform(
-                eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_OUT),
-                anyInt(), anyString());
-    }
-
-    private Pair<IkeSessionCallback, ChildSessionCallback> verifyCreateIkeAndCaptureCbs()
-            throws Exception {
-        final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
-                ArgumentCaptor.forClass(IkeSessionCallback.class);
-        final ArgumentCaptor<ChildSessionCallback> childCbCaptor =
-                ArgumentCaptor.forClass(ChildSessionCallback.class);
-
-        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS)).createIkeSession(
-                any(), any(), any(), any(), ikeCbCaptor.capture(), childCbCaptor.capture());
-
-        return new Pair<>(ikeCbCaptor.getValue(), childCbCaptor.getValue());
-    }
-
-    private static class PlatformVpnSnapshot {
-        public final Vpn vpn;
-        public final NetworkCallback nwCb;
-        public final IkeSessionCallback ikeCb;
-        public final ChildSessionCallback childCb;
-
-        PlatformVpnSnapshot(Vpn vpn, NetworkCallback nwCb,
-                IkeSessionCallback ikeCb, ChildSessionCallback childCb) {
-            this.vpn = vpn;
-            this.nwCb = nwCb;
-            this.ikeCb = ikeCb;
-            this.childCb = childCb;
-        }
-    }
-
-    private PlatformVpnSnapshot verifySetupPlatformVpn(IkeSessionConfiguration ikeConfig)
-            throws Exception {
-        return verifySetupPlatformVpn(ikeConfig, true);
-    }
-
-    private PlatformVpnSnapshot verifySetupPlatformVpn(
-            IkeSessionConfiguration ikeConfig, boolean mtuSupportsIpv6) throws Exception {
-        return verifySetupPlatformVpn(mVpnProfile, ikeConfig, mtuSupportsIpv6);
-    }
-
-    private PlatformVpnSnapshot verifySetupPlatformVpn(VpnProfile vpnProfile,
-            IkeSessionConfiguration ikeConfig, boolean mtuSupportsIpv6) throws Exception {
-        return verifySetupPlatformVpn(vpnProfile, ikeConfig,
-                new NetworkCapabilities.Builder().build() /* underlying network caps */,
-                mtuSupportsIpv6, false /* areLongLivedTcpConnectionsExpensive */);
-    }
-
-    private PlatformVpnSnapshot verifySetupPlatformVpn(VpnProfile vpnProfile,
-            IkeSessionConfiguration ikeConfig,
-            @NonNull final NetworkCapabilities underlyingNetworkCaps,
-            boolean mtuSupportsIpv6,
-            boolean areLongLivedTcpConnectionsExpensive) throws Exception {
-        if (!mtuSupportsIpv6) {
-            doReturn(IPV6_MIN_MTU - 1).when(mTestDeps).calculateVpnMtu(any(), anyInt(), anyInt(),
-                    anyBoolean());
-        }
-
-        doReturn(mMockNetworkAgent).when(mTestDeps)
-                .newNetworkAgent(
-                        any(), any(), anyString(), any(), any(), any(), any(), any(), any());
-        doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
-
-        final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
-        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
-                .thenReturn(vpnProfile.encode());
-
-        final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
-        final Set<Range<Integer>> uidRanges = Collections.singleton(PRIMARY_USER_RANGE);
-        verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
-        final NetworkCallback nwCb = triggerOnAvailableAndGetCallback(underlyingNetworkCaps);
-        // There are 4 interactions with the executor.
-        // - Network available
-        // - LP change
-        // - NC change
-        // - schedule() calls in scheduleStartIkeSession()
-        // The first 3 calls are triggered from Executor.execute(). The execute() will also call to
-        // schedule() with 0 delay. Verify the exact interaction here so that it won't cause flakes
-        // in the follow-up flow.
-        verify(mExecutor, timeout(TEST_TIMEOUT_MS).times(4))
-                .schedule(any(Runnable.class), anyLong(), any());
-        reset(mExecutor);
-
-        // Mock the setup procedure by firing callbacks
-        final Pair<IkeSessionCallback, ChildSessionCallback> cbPair =
-                verifyCreateIkeAndCaptureCbs();
-        final IkeSessionCallback ikeCb = cbPair.first;
-        final ChildSessionCallback childCb = cbPair.second;
-
-        ikeCb.onOpened(ikeConfig);
-        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN);
-        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT);
-        childCb.onOpened(createChildConfig());
-
-        // Verification VPN setup
-        verifyApplyTunnelModeTransforms(1);
-
-        ArgumentCaptor<LinkProperties> lpCaptor = ArgumentCaptor.forClass(LinkProperties.class);
-        ArgumentCaptor<NetworkCapabilities> ncCaptor =
-                ArgumentCaptor.forClass(NetworkCapabilities.class);
-        ArgumentCaptor<NetworkAgentConfig> nacCaptor =
-                ArgumentCaptor.forClass(NetworkAgentConfig.class);
-        verify(mTestDeps).newNetworkAgent(
-                any(), any(), anyString(), ncCaptor.capture(), lpCaptor.capture(),
-                any(), nacCaptor.capture(), any(), any());
-        verify(mIkeSessionWrapper).setUnderpinnedNetwork(TEST_NETWORK);
-        // Check LinkProperties
-        final LinkProperties lp = lpCaptor.getValue();
-        final List<RouteInfo> expectedRoutes =
-                new ArrayList<>(
-                        Arrays.asList(
-                                new RouteInfo(
-                                        new IpPrefix(Inet4Address.ANY, 0),
-                                        null /* gateway */,
-                                        TEST_IFACE_NAME,
-                                        RouteInfo.RTN_UNICAST)));
-        final List<LinkAddress> expectedAddresses =
-                new ArrayList<>(
-                        Arrays.asList(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN)));
-        final List<InetAddress> expectedDns = new ArrayList<>(Arrays.asList(TEST_VPN_INTERNAL_DNS));
-
-        if (mtuSupportsIpv6) {
-            expectedRoutes.add(
-                    new RouteInfo(
-                            new IpPrefix(Inet6Address.ANY, 0),
-                            null /* gateway */,
-                            TEST_IFACE_NAME,
-                            RouteInfo.RTN_UNICAST));
-            expectedAddresses.add(new LinkAddress(TEST_VPN_INTERNAL_IP6, IP6_PREFIX_LEN));
-            expectedDns.add(TEST_VPN_INTERNAL_DNS6);
-        } else {
-            expectedRoutes.add(
-                    new RouteInfo(
-                            new IpPrefix(Inet6Address.ANY, 0),
-                            null /* gateway */,
-                            TEST_IFACE_NAME,
-                            RTN_UNREACHABLE));
-        }
-
-        assertEquals(expectedRoutes, lp.getRoutes());
-        assertEquals(expectedAddresses, lp.getLinkAddresses());
-        assertEquals(expectedDns, lp.getDnsServers());
-
-        // Check NetworkCapabilities
-        assertEquals(Arrays.asList(TEST_NETWORK), ncCaptor.getValue().getUnderlyingNetworks());
-
-        // Check if allowBypass is set or not.
-        assertTrue(nacCaptor.getValue().isBypassableVpn());
-        // Check if extra info for VPN is set.
-        assertTrue(nacCaptor.getValue().getLegacyExtraInfo().contains(TEST_VPN_PKG));
-        final VpnTransportInfo info = (VpnTransportInfo) ncCaptor.getValue().getTransportInfo();
-        assertTrue(info.isBypassable());
-        assertEquals(areLongLivedTcpConnectionsExpensive,
-                info.areLongLivedTcpConnectionsExpensive());
-        return new PlatformVpnSnapshot(vpn, nwCb, ikeCb, childCb);
-    }
-
-    @Test
-    public void testStartPlatformVpn() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
-        verify(mConnectivityManager).setVpnDefaultForUids(anyString(), eq(Collections.EMPTY_LIST));
-    }
-
-    @Test
-    public void testMigrateIkeSession_FromIkeTunnConnParams_AutoTimerNoTimer() throws Exception {
-        doTestMigrateIkeSession_FromIkeTunnConnParams(
-                false /* isAutomaticIpVersionSelectionEnabled */,
-                true /* isAutomaticNattKeepaliveTimerEnabled */,
-                TEST_KEEPALIVE_TIMEOUT_UNSET /* keepaliveInProfile */,
-                ESP_IP_VERSION_AUTO /* ipVersionInProfile */,
-                ESP_ENCAP_TYPE_AUTO /* encapTypeInProfile */);
-    }
-
-    @Test
-    public void testMigrateIkeSession_FromIkeTunnConnParams_AutoTimerTimerSet() throws Exception {
-        doTestMigrateIkeSession_FromIkeTunnConnParams(
-                false /* isAutomaticIpVersionSelectionEnabled */,
-                true /* isAutomaticNattKeepaliveTimerEnabled */,
-                TEST_KEEPALIVE_TIMER /* keepaliveInProfile */,
-                ESP_IP_VERSION_AUTO /* ipVersionInProfile */,
-                ESP_ENCAP_TYPE_AUTO /* encapTypeInProfile */);
-    }
-
-    @Test
-    public void testMigrateIkeSession_FromIkeTunnConnParams_AutoIp() throws Exception {
-        doTestMigrateIkeSession_FromIkeTunnConnParams(
-                true /* isAutomaticIpVersionSelectionEnabled */,
-                false /* isAutomaticNattKeepaliveTimerEnabled */,
-                TEST_KEEPALIVE_TIMEOUT_UNSET /* keepaliveInProfile */,
-                ESP_IP_VERSION_AUTO /* ipVersionInProfile */,
-                ESP_ENCAP_TYPE_AUTO /* encapTypeInProfile */);
-    }
-
-    @Test
-    public void testMigrateIkeSession_FromIkeTunnConnParams_AssignedIpProtocol() throws Exception {
-        doTestMigrateIkeSession_FromIkeTunnConnParams(
-                false /* isAutomaticIpVersionSelectionEnabled */,
-                false /* isAutomaticNattKeepaliveTimerEnabled */,
-                TEST_KEEPALIVE_TIMEOUT_UNSET /* keepaliveInProfile */,
-                ESP_IP_VERSION_IPV4 /* ipVersionInProfile */,
-                ESP_ENCAP_TYPE_UDP /* encapTypeInProfile */);
-    }
-
-    @Test
-    public void testMigrateIkeSession_FromNotIkeTunnConnParams_AutoTimer() throws Exception {
-        doTestMigrateIkeSession_FromNotIkeTunnConnParams(
-                false /* isAutomaticIpVersionSelectionEnabled */,
-                true /* isAutomaticNattKeepaliveTimerEnabled */);
-    }
-
-    @Test
-    public void testMigrateIkeSession_FromNotIkeTunnConnParams_AutoIp() throws Exception {
-        doTestMigrateIkeSession_FromNotIkeTunnConnParams(
-                true /* isAutomaticIpVersionSelectionEnabled */,
-                false /* isAutomaticNattKeepaliveTimerEnabled */);
-    }
-
-    private void doTestMigrateIkeSession_FromNotIkeTunnConnParams(
-            boolean isAutomaticIpVersionSelectionEnabled,
-            boolean isAutomaticNattKeepaliveTimerEnabled) throws Exception {
-        final Ikev2VpnProfile ikeProfile =
-                new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
-                        .setAuthPsk(TEST_VPN_PSK)
-                        .setBypassable(true /* isBypassable */)
-                        .setAutomaticNattKeepaliveTimerEnabled(isAutomaticNattKeepaliveTimerEnabled)
-                        .setAutomaticIpVersionSelectionEnabled(isAutomaticIpVersionSelectionEnabled)
-                        .build();
-
-        final int expectedKeepalive = isAutomaticNattKeepaliveTimerEnabled
-                ? AUTOMATIC_KEEPALIVE_DELAY_SECONDS
-                : DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT;
-        doTestMigrateIkeSession(ikeProfile.toVpnProfile(),
-                expectedKeepalive,
-                ESP_IP_VERSION_AUTO /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
-                new NetworkCapabilities.Builder().build());
-    }
-
-    private Ikev2VpnProfile makeIkeV2VpnProfile(
-            boolean isAutomaticIpVersionSelectionEnabled,
-            boolean isAutomaticNattKeepaliveTimerEnabled,
-            int keepaliveInProfile,
-            int ipVersionInProfile,
-            int encapTypeInProfile) {
-        // TODO: Update helper function in IkeSessionTestUtils to support building IkeSessionParams
-        // with IP version and encap type when mainline-prod branch support these two APIs.
-        final IkeSessionParams params = getTestIkeSessionParams(true /* testIpv6 */,
-                new IkeFqdnIdentification(TEST_IDENTITY), keepaliveInProfile);
-        final IkeSessionParams ikeSessionParams = new IkeSessionParams.Builder(params)
-                .setIpVersion(ipVersionInProfile)
-                .setEncapType(encapTypeInProfile)
-                .build();
-
-        final IkeTunnelConnectionParams tunnelParams =
-                new IkeTunnelConnectionParams(ikeSessionParams, CHILD_PARAMS);
-        return new Ikev2VpnProfile.Builder(tunnelParams)
-                .setBypassable(true)
-                .setAutomaticNattKeepaliveTimerEnabled(isAutomaticNattKeepaliveTimerEnabled)
-                .setAutomaticIpVersionSelectionEnabled(isAutomaticIpVersionSelectionEnabled)
-                .build();
-    }
-
-    private void doTestMigrateIkeSession_FromIkeTunnConnParams(
-            boolean isAutomaticIpVersionSelectionEnabled,
-            boolean isAutomaticNattKeepaliveTimerEnabled,
-            int keepaliveInProfile,
-            int ipVersionInProfile,
-            int encapTypeInProfile) throws Exception {
-        doTestMigrateIkeSession_FromIkeTunnConnParams(isAutomaticIpVersionSelectionEnabled,
-                isAutomaticNattKeepaliveTimerEnabled, keepaliveInProfile, ipVersionInProfile,
-                encapTypeInProfile, new NetworkCapabilities.Builder().build());
-    }
-
-    private void doTestMigrateIkeSession_FromIkeTunnConnParams(
-            boolean isAutomaticIpVersionSelectionEnabled,
-            boolean isAutomaticNattKeepaliveTimerEnabled,
-            int keepaliveInProfile,
-            int ipVersionInProfile,
-            int encapTypeInProfile,
-            @NonNull final NetworkCapabilities nc) throws Exception {
-        final Ikev2VpnProfile ikeProfile = makeIkeV2VpnProfile(
-                isAutomaticIpVersionSelectionEnabled,
-                isAutomaticNattKeepaliveTimerEnabled,
-                keepaliveInProfile,
-                ipVersionInProfile,
-                encapTypeInProfile);
-
-        final IkeSessionParams ikeSessionParams =
-                ikeProfile.getIkeTunnelConnectionParams().getIkeSessionParams();
-        final int expectedKeepalive = isAutomaticNattKeepaliveTimerEnabled
-                ? AUTOMATIC_KEEPALIVE_DELAY_SECONDS
-                : ikeSessionParams.getNattKeepAliveDelaySeconds();
-        final int expectedIpVersion = isAutomaticIpVersionSelectionEnabled
-                ? ESP_IP_VERSION_AUTO
-                : ikeSessionParams.getIpVersion();
-        final int expectedEncapType = isAutomaticIpVersionSelectionEnabled
-                ? ESP_ENCAP_TYPE_AUTO
-                : ikeSessionParams.getEncapType();
-        doTestMigrateIkeSession(ikeProfile.toVpnProfile(), expectedKeepalive,
-                expectedIpVersion, expectedEncapType, nc);
-    }
-
-    @Test
-    public void doTestMigrateIkeSession_Vcn() throws Exception {
-        final int expectedKeepalive = 2097; // Any unlikely number will do
-        final NetworkCapabilities vcnNc = new NetworkCapabilities.Builder()
-                .addTransportType(TRANSPORT_CELLULAR)
-                .setTransportInfo(new VcnTransportInfo(TEST_SUB_ID, expectedKeepalive))
-                .build();
-        final Ikev2VpnProfile ikev2VpnProfile = makeIkeV2VpnProfile(
-                true /* isAutomaticIpVersionSelectionEnabled */,
-                true /* isAutomaticNattKeepaliveTimerEnabled */,
-                234 /* keepaliveInProfile */, // Should be ignored, any value will do
-                ESP_IP_VERSION_IPV4, // Should be ignored
-                ESP_ENCAP_TYPE_UDP // Should be ignored
-        );
-        doTestMigrateIkeSession(
-                ikev2VpnProfile.toVpnProfile(),
-                expectedKeepalive,
-                ESP_IP_VERSION_AUTO /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
-                vcnNc);
-    }
-
-    private void doTestMigrateIkeSession(
-            @NonNull final VpnProfile profile,
-            final int expectedKeepalive,
-            final int expectedIpVersion,
-            final int expectedEncapType,
-            @NonNull final NetworkCapabilities caps) throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot =
-                verifySetupPlatformVpn(profile,
-                        createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
-                        caps /* underlying network capabilities */,
-                        false /* mtuSupportsIpv6 */,
-                        expectedKeepalive < DEFAULT_LONG_LIVED_TCP_CONNS_EXPENSIVE_TIMEOUT_SEC);
-        // Simulate a new network coming up
-        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
-        verify(mIkeSessionWrapper, never()).setNetwork(any(), anyInt(), anyInt(), anyInt());
-
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2, caps);
-        // Verify MOBIKE is triggered
-        verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(TEST_NETWORK_2,
-                expectedIpVersion, expectedEncapType, expectedKeepalive);
-
-        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
-    }
-
-    @Test
-    public void testLinkPropertiesUpdateTriggerReevaluation() throws Exception {
-        final boolean hasV6 = true;
-
-        mockCarrierConfig(TEST_SUB_ID, TelephonyManager.SIM_STATE_LOADED, TEST_KEEPALIVE_TIMER,
-                PREFERRED_IKE_PROTOCOL_IPV6_ESP);
-        final IkeSessionParams params = getTestIkeSessionParams(hasV6,
-                new IkeFqdnIdentification(TEST_IDENTITY), TEST_KEEPALIVE_TIMER);
-        final IkeTunnelConnectionParams tunnelParams =
-                new IkeTunnelConnectionParams(params, CHILD_PARAMS);
-        final Ikev2VpnProfile ikeProfile = new Ikev2VpnProfile.Builder(tunnelParams)
-                .setBypassable(true)
-                .setAutomaticNattKeepaliveTimerEnabled(false)
-                .setAutomaticIpVersionSelectionEnabled(true)
-                .build();
-        final PlatformVpnSnapshot vpnSnapShot =
-                verifySetupPlatformVpn(ikeProfile.toVpnProfile(),
-                        createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
-                        new NetworkCapabilities.Builder().build() /* underlying network caps */,
-                        hasV6 /* mtuSupportsIpv6 */,
-                        false /* areLongLivedTcpConnectionsExpensive */);
-        reset(mExecutor);
-
-        // Simulate a new network coming up
-        final LinkProperties lp = new LinkProperties();
-        lp.addLinkAddress(new LinkAddress("192.0.2.2/32"));
-
-        // Have the executor use the real delay to make sure schedule() was called only
-        // once for all calls. Also, arrange for execute() not to call schedule() to avoid
-        // messing with the checks for schedule().
-        mExecutor.delayMs = TestExecutor.REAL_DELAY;
-        mExecutor.executeDirect = true;
-        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
-        vpnSnapShot.nwCb.onCapabilitiesChanged(
-                TEST_NETWORK_2, new NetworkCapabilities.Builder().build());
-        vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp));
-        verify(mExecutor).schedule(any(Runnable.class), longThat(it -> it > 0), any());
-        reset(mExecutor);
-
-        final InOrder order = inOrder(mIkeSessionWrapper);
-
-        // Verify the network is started
-        order.verify(mIkeSessionWrapper, timeout(TIMEOUT_CROSSTHREAD_MS)).setNetwork(TEST_NETWORK_2,
-                ESP_IP_VERSION_AUTO, ESP_ENCAP_TYPE_AUTO, TEST_KEEPALIVE_TIMER);
-
-        // Send the same properties, check that no migration is scheduled
-        vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp));
-        verify(mExecutor, never()).schedule(any(Runnable.class), anyLong(), any());
-
-        // Add v6 address, verify MOBIKE is triggered
-        lp.addLinkAddress(new LinkAddress("2001:db8::1/64"));
-        vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp));
-        order.verify(mIkeSessionWrapper, timeout(TIMEOUT_CROSSTHREAD_MS)).setNetwork(TEST_NETWORK_2,
-                ESP_IP_VERSION_AUTO, ESP_ENCAP_TYPE_AUTO, TEST_KEEPALIVE_TIMER);
-
-        // Add another v4 address, verify MOBIKE is triggered
-        final LinkProperties stacked = new LinkProperties();
-        stacked.setInterfaceName("v4-" + lp.getInterfaceName());
-        stacked.addLinkAddress(new LinkAddress("192.168.0.1/32"));
-        lp.addStackedLink(stacked);
-        vpnSnapShot.nwCb.onLinkPropertiesChanged(TEST_NETWORK_2, new LinkProperties(lp));
-        order.verify(mIkeSessionWrapper, timeout(TIMEOUT_CROSSTHREAD_MS)).setNetwork(TEST_NETWORK_2,
-                ESP_IP_VERSION_AUTO, ESP_ENCAP_TYPE_AUTO, TEST_KEEPALIVE_TIMER);
-
-        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
-    }
-
-    private void mockCarrierConfig(int subId, int simStatus, int keepaliveTimer, int ikeProtocol) {
-        final SubscriptionInfo subscriptionInfo = mock(SubscriptionInfo.class);
-        doReturn(subId).when(subscriptionInfo).getSubscriptionId();
-        doReturn(List.of(subscriptionInfo)).when(mSubscriptionManager)
-                .getActiveSubscriptionInfoList();
-
-        doReturn(simStatus).when(mTmPerSub).getSimApplicationState();
-        doReturn(TEST_MCCMNC).when(mTmPerSub).getSimOperator(subId);
-
-        final PersistableBundle persistableBundle = new PersistableBundle();
-        persistableBundle.putInt(KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT, keepaliveTimer);
-        persistableBundle.putInt(KEY_PREFERRED_IKE_PROTOCOL_INT, ikeProtocol);
-        // For CarrierConfigManager.isConfigForIdentifiedCarrier check
-        persistableBundle.putBoolean(KEY_CARRIER_CONFIG_APPLIED_BOOL, true);
-        doReturn(persistableBundle).when(mConfigManager).getConfigForSubId(subId);
-    }
-
-    private CarrierConfigManager.CarrierConfigChangeListener getCarrierConfigListener() {
-        final ArgumentCaptor<CarrierConfigManager.CarrierConfigChangeListener> listenerCaptor =
-                ArgumentCaptor.forClass(CarrierConfigManager.CarrierConfigChangeListener.class);
-
-        verify(mConfigManager).registerCarrierConfigChangeListener(any(), listenerCaptor.capture());
-
-        return listenerCaptor.getValue();
-    }
-
-    @Test
-    public void testNattKeepaliveTimerFromCarrierConfig_noSubId() throws Exception {
-        doTestReadCarrierConfig(new NetworkCapabilities(),
-                TelephonyManager.SIM_STATE_LOADED,
-                PREFERRED_IKE_PROTOCOL_IPV4_UDP,
-                AUTOMATIC_KEEPALIVE_DELAY_SECONDS /* expectedKeepaliveTimer */,
-                ESP_IP_VERSION_AUTO /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
-                false /* expectedReadFromCarrierConfig*/,
-                true /* areLongLivedTcpConnectionsExpensive */);
-    }
-
-    @Test
-    public void testNattKeepaliveTimerFromCarrierConfig_simAbsent() throws Exception {
-        doTestReadCarrierConfig(new NetworkCapabilities.Builder().build(),
-                TelephonyManager.SIM_STATE_ABSENT,
-                PREFERRED_IKE_PROTOCOL_IPV4_UDP,
-                AUTOMATIC_KEEPALIVE_DELAY_SECONDS /* expectedKeepaliveTimer */,
-                ESP_IP_VERSION_AUTO /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
-                false /* expectedReadFromCarrierConfig*/,
-                true /* areLongLivedTcpConnectionsExpensive */);
-    }
-
-    @Test
-    public void testNattKeepaliveTimerFromCarrierConfig() throws Exception {
-        doTestReadCarrierConfig(createTestCellNc(),
-                TelephonyManager.SIM_STATE_LOADED,
-                PREFERRED_IKE_PROTOCOL_AUTO,
-                TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */,
-                ESP_IP_VERSION_AUTO /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
-                true /* expectedReadFromCarrierConfig*/,
-                false /* areLongLivedTcpConnectionsExpensive */);
-    }
-
-    @Test
-    public void testNattKeepaliveTimerFromCarrierConfig_NotCell() throws Exception {
-        final NetworkCapabilities nc = new NetworkCapabilities.Builder()
-                .addTransportType(TRANSPORT_WIFI)
-                .setTransportInfo(new WifiInfo.Builder().build())
-                .build();
-        doTestReadCarrierConfig(nc,
-                TelephonyManager.SIM_STATE_LOADED,
-                PREFERRED_IKE_PROTOCOL_IPV4_UDP,
-                AUTOMATIC_KEEPALIVE_DELAY_SECONDS /* expectedKeepaliveTimer */,
-                ESP_IP_VERSION_AUTO /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
-                false /* expectedReadFromCarrierConfig*/,
-                true /* areLongLivedTcpConnectionsExpensive */);
-    }
-
-    @Test
-    public void testPreferredIpProtocolFromCarrierConfig_v4UDP() throws Exception {
-        doTestReadCarrierConfig(createTestCellNc(),
-                TelephonyManager.SIM_STATE_LOADED,
-                PREFERRED_IKE_PROTOCOL_IPV4_UDP,
-                TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */,
-                ESP_IP_VERSION_IPV4 /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_UDP /* expectedEncapType */,
-                true /* expectedReadFromCarrierConfig*/,
-                false /* areLongLivedTcpConnectionsExpensive */);
-    }
-
-    @Test
-    public void testPreferredIpProtocolFromCarrierConfig_v6ESP() throws Exception {
-        doTestReadCarrierConfig(createTestCellNc(),
-                TelephonyManager.SIM_STATE_LOADED,
-                PREFERRED_IKE_PROTOCOL_IPV6_ESP,
-                TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */,
-                ESP_IP_VERSION_IPV6 /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_NONE /* expectedEncapType */,
-                true /* expectedReadFromCarrierConfig*/,
-                false /* areLongLivedTcpConnectionsExpensive */);
-    }
-
-    @Test
-    public void testPreferredIpProtocolFromCarrierConfig_v6UDP() throws Exception {
-        doTestReadCarrierConfig(createTestCellNc(),
-                TelephonyManager.SIM_STATE_LOADED,
-                PREFERRED_IKE_PROTOCOL_IPV6_UDP,
-                TEST_KEEPALIVE_TIMER /* expectedKeepaliveTimer */,
-                ESP_IP_VERSION_IPV6 /* expectedIpVersion */,
-                ESP_ENCAP_TYPE_UDP /* expectedEncapType */,
-                true /* expectedReadFromCarrierConfig*/,
-                false /* areLongLivedTcpConnectionsExpensive */);
-    }
-
-    private NetworkCapabilities createTestCellNc() {
-        return new NetworkCapabilities.Builder()
-                .addTransportType(TRANSPORT_CELLULAR)
-                .setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder()
-                        .setSubscriptionId(TEST_SUB_ID)
-                        .build())
-                .build();
-    }
-
-    private void doTestReadCarrierConfig(NetworkCapabilities nc, int simState, int preferredIpProto,
-            int expectedKeepaliveTimer, int expectedIpVersion, int expectedEncapType,
-            boolean expectedReadFromCarrierConfig,
-            boolean areLongLivedTcpConnectionsExpensive)
-            throws Exception {
-        final Ikev2VpnProfile ikeProfile =
-                new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
-                        .setAuthPsk(TEST_VPN_PSK)
-                        .setBypassable(true /* isBypassable */)
-                        .setAutomaticNattKeepaliveTimerEnabled(true)
-                        .setAutomaticIpVersionSelectionEnabled(true)
-                        .build();
-
-        final PlatformVpnSnapshot vpnSnapShot =
-                verifySetupPlatformVpn(ikeProfile.toVpnProfile(),
-                        createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
-                        new NetworkCapabilities.Builder().build() /* underlying network caps */,
-                        false /* mtuSupportsIpv6 */,
-                        true /* areLongLivedTcpConnectionsExpensive */);
-
-        final CarrierConfigManager.CarrierConfigChangeListener listener =
-                getCarrierConfigListener();
-
-        // Simulate a new network coming up
-        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
-        // Migration will not be started until receiving network capabilities change.
-        verify(mIkeSessionWrapper, never()).setNetwork(any(), anyInt(), anyInt(), anyInt());
-
-        reset(mIkeSessionWrapper);
-        mockCarrierConfig(TEST_SUB_ID, simState, TEST_KEEPALIVE_TIMER, preferredIpProto);
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2, nc);
-        verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(TEST_NETWORK_2,
-                expectedIpVersion, expectedEncapType, expectedKeepaliveTimer);
-        if (expectedReadFromCarrierConfig) {
-            final ArgumentCaptor<NetworkCapabilities> ncCaptor =
-                    ArgumentCaptor.forClass(NetworkCapabilities.class);
-            verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS))
-                    .doSendNetworkCapabilities(ncCaptor.capture());
-
-            final VpnTransportInfo info =
-                    (VpnTransportInfo) ncCaptor.getValue().getTransportInfo();
-            assertEquals(areLongLivedTcpConnectionsExpensive,
-                    info.areLongLivedTcpConnectionsExpensive());
-        } else {
-            verify(mMockNetworkAgent, never()).doSendNetworkCapabilities(any());
-        }
-
-        reset(mExecutor);
-        reset(mIkeSessionWrapper);
-        reset(mMockNetworkAgent);
-
-        // Trigger carrier config change
-        listener.onCarrierConfigChanged(1 /* logicalSlotIndex */, TEST_SUB_ID,
-                -1 /* carrierId */, -1 /* specificCarrierId */);
-        verify(mIkeSessionWrapper).setNetwork(TEST_NETWORK_2,
-                expectedIpVersion, expectedEncapType, expectedKeepaliveTimer);
-        // Expect no NetworkCapabilities change.
-        // Call to doSendNetworkCapabilities() will not be triggered.
-        verify(mMockNetworkAgent, never()).doSendNetworkCapabilities(any());
-    }
-
-    @Test
-    public void testStartPlatformVpn_mtuDoesNotSupportIpv6() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot =
-                verifySetupPlatformVpn(
-                        createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
-                        false /* mtuSupportsIpv6 */);
-        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
-    }
-
-    @Test
-    public void testStartPlatformVpn_underlyingNetworkNotChange() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-        // Trigger update on the same network should not cause underlying network change in NC of
-        // the VPN network
-        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK);
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK,
-                new NetworkCapabilities.Builder()
-                        .setSubscriptionIds(Set.of(TEST_SUB_ID))
-                        .build());
-        // Verify setNetwork() called but no underlying network update
-        verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(eq(TEST_NETWORK),
-                eq(ESP_IP_VERSION_AUTO) /* ipVersion */,
-                eq(ESP_ENCAP_TYPE_AUTO) /* encapType */,
-                eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */);
-        verify(mMockNetworkAgent, never())
-                .doSetUnderlyingNetworks(any());
-
-        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2,
-                new NetworkCapabilities.Builder().build());
-
-        // A new network should trigger both setNetwork() and a underlying network update.
-        verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(eq(TEST_NETWORK_2),
-                eq(ESP_IP_VERSION_AUTO) /* ipVersion */,
-                eq(ESP_ENCAP_TYPE_AUTO) /* encapType */,
-                eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */);
-        verify(mMockNetworkAgent).doSetUnderlyingNetworks(
-                Collections.singletonList(TEST_NETWORK_2));
-
-        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
-    }
-
-    @Test
-    public void testStartPlatformVpnMobility_mobikeEnabled() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
-        // Set new MTU on a different network
-        final int newMtu = IPV6_MIN_MTU + 1;
-        doReturn(newMtu).when(mTestDeps).calculateVpnMtu(any(), anyInt(), anyInt(), anyBoolean());
-
-        // Mock network loss and verify a cleanup task is scheduled
-        vpnSnapShot.nwCb.onLost(TEST_NETWORK);
-        verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any());
-
-        // Mock new network comes up and the cleanup task is cancelled
-        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
-        verify(mIkeSessionWrapper, never()).setNetwork(any(), anyInt(), anyInt(), anyInt());
-
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2,
-                new NetworkCapabilities.Builder().build());
-        // Verify MOBIKE is triggered
-        verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(eq(TEST_NETWORK_2),
-                eq(ESP_IP_VERSION_AUTO) /* ipVersion */,
-                eq(ESP_ENCAP_TYPE_AUTO) /* encapType */,
-                eq(DEFAULT_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT) /* keepaliveDelay */);
-        // Verify mNetworkCapabilities is updated
-        assertEquals(
-                Collections.singletonList(TEST_NETWORK_2),
-                vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
-        verify(mMockNetworkAgent)
-                .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
-
-        // Mock the MOBIKE procedure
-        vpnSnapShot.ikeCb.onIkeSessionConnectionInfoChanged(createIkeConnectInfo_2());
-        vpnSnapShot.childCb.onIpSecTransformsMigrated(
-                createIpSecTransform(), createIpSecTransform());
-
-        verify(mIpSecService).setNetworkForTunnelInterface(
-                eq(TEST_TUNNEL_RESOURCE_ID), eq(TEST_NETWORK_2), anyString());
-
-        // Expect 2 times: one for initial setup and one for MOBIKE
-        verifyApplyTunnelModeTransforms(2);
-
-        // Verify mNetworkAgent is updated
-        verify(mMockNetworkAgent).doSendLinkProperties(argThat(lp -> lp.getMtu() == newMtu));
-        verify(mMockNetworkAgent, never()).unregister();
-        // No further doSetUnderlyingNetworks interaction. The interaction count should stay one.
-        verify(mMockNetworkAgent, times(1)).doSetUnderlyingNetworks(any());
-        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
-    }
-
-    @Test
-    public void testStartPlatformVpnMobility_mobikeEnabledMtuDoesNotSupportIpv6() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot =
-                verifySetupPlatformVpn(
-                        createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
-        // Set MTU below 1280
-        final int newMtu = IPV6_MIN_MTU - 1;
-        doReturn(newMtu).when(mTestDeps).calculateVpnMtu(any(), anyInt(), anyInt(), anyBoolean());
-
-        // Mock new network available & MOBIKE procedures
-        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2,
-                new NetworkCapabilities.Builder().build());
-        // Verify mNetworkCapabilities is updated
-        verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS))
-                .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
-        assertEquals(
-                Collections.singletonList(TEST_NETWORK_2),
-                vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
-
-        vpnSnapShot.ikeCb.onIkeSessionConnectionInfoChanged(createIkeConnectInfo_2());
-        vpnSnapShot.childCb.onIpSecTransformsMigrated(
-                createIpSecTransform(), createIpSecTransform());
-
-        // Verify removal of IPv6 addresses and routes triggers a network agent restart
-        final ArgumentCaptor<LinkProperties> lpCaptor =
-                ArgumentCaptor.forClass(LinkProperties.class);
-        verify(mTestDeps, times(2))
-                .newNetworkAgent(any(), any(), anyString(), any(), lpCaptor.capture(), any(), any(),
-                        any(), any());
-        verify(mMockNetworkAgent).unregister();
-        // mMockNetworkAgent is an old NetworkAgent, so it won't update LinkProperties after
-        // unregistering.
-        verify(mMockNetworkAgent, never()).doSendLinkProperties(any());
-
-        final LinkProperties lp = lpCaptor.getValue();
-
-        for (LinkAddress addr : lp.getLinkAddresses()) {
-            if (addr.isIpv6()) {
-                fail("IPv6 address found on VPN with MTU < IPv6 minimum MTU");
-            }
-        }
-
-        for (InetAddress dnsAddr : lp.getDnsServers()) {
-            if (dnsAddr instanceof Inet6Address) {
-                fail("IPv6 DNS server found on VPN with MTU < IPv6 minimum MTU");
-            }
-        }
-
-        for (RouteInfo routeInfo : lp.getRoutes()) {
-            if (routeInfo.getDestinationLinkAddress().isIpv6()
-                    && !routeInfo.isIPv6UnreachableDefault()) {
-                fail("IPv6 route found on VPN with MTU < IPv6 minimum MTU");
-            }
-        }
-
-        assertEquals(newMtu, lp.getMtu());
-
-        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
-    }
-
-    @Test
-    public void testStartPlatformVpnReestablishes_mobikeDisabled() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
-
-        // Forget the first IKE creation to be prepared to capture callbacks of the second
-        // IKE session
-        resetIkev2SessionCreator(mock(Vpn.IkeSessionWrapper.class));
-
-        // Mock network switch
-        vpnSnapShot.nwCb.onLost(TEST_NETWORK);
-        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
-        // The old IKE Session will not be killed until receiving network capabilities change.
-        verify(mIkeSessionWrapper, never()).kill();
-
-        vpnSnapShot.nwCb.onCapabilitiesChanged(
-                TEST_NETWORK_2, new NetworkCapabilities.Builder().build());
-        // Verify the old IKE Session is killed
-        verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).kill();
-
-        // Capture callbacks of the new IKE Session
-        final Pair<IkeSessionCallback, ChildSessionCallback> cbPair =
-                verifyCreateIkeAndCaptureCbs();
-        final IkeSessionCallback ikeCb = cbPair.first;
-        final ChildSessionCallback childCb = cbPair.second;
-
-        // Mock the IKE Session setup
-        ikeCb.onOpened(createIkeConfig(createIkeConnectInfo_2(), false /* isMobikeEnabled */));
-
-        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN);
-        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT);
-        childCb.onOpened(createChildConfig());
-
-        // Expect 2 times since there have been two Session setups
-        verifyApplyTunnelModeTransforms(2);
-
-        // Verify mNetworkCapabilities and mNetworkAgent are updated
-        assertEquals(
-                Collections.singletonList(TEST_NETWORK_2),
-                vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
-        verify(mMockNetworkAgent)
-                .doSetUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
-
-        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
-    }
-
-    private String getDump(@NonNull final Vpn vpn) {
-        final StringWriter sw = new StringWriter();
-        final IndentingPrintWriter writer = new IndentingPrintWriter(sw, "");
-        vpn.dump(writer);
-        writer.flush();
-        return sw.toString();
-    }
-
-    private int countMatches(@NonNull final Pattern regexp, @NonNull final String string) {
-        final Matcher m = regexp.matcher(string);
-        int i = 0;
-        while (m.find()) ++i;
-        return i;
-    }
-
-    @Test
-    public void testNCEventChanges() throws Exception {
-        final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder()
-                .addTransportType(TRANSPORT_CELLULAR)
-                .addCapability(NET_CAPABILITY_INTERNET)
-                .addCapability(NET_CAPABILITY_NOT_RESTRICTED)
-                .setLinkDownstreamBandwidthKbps(1000)
-                .setLinkUpstreamBandwidthKbps(500);
-
-        final Ikev2VpnProfile ikeProfile =
-                new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
-                        .setAuthPsk(TEST_VPN_PSK)
-                        .setBypassable(true /* isBypassable */)
-                        .setAutomaticNattKeepaliveTimerEnabled(true)
-                        .setAutomaticIpVersionSelectionEnabled(true)
-                        .build();
-
-        final PlatformVpnSnapshot vpnSnapShot =
-                verifySetupPlatformVpn(ikeProfile.toVpnProfile(),
-                        createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
-                        ncBuilder.build(), false /* mtuSupportsIpv6 */,
-                        true /* areLongLivedTcpConnectionsExpensive */);
-
-        // Calls to onCapabilitiesChanged will be thrown to the executor for execution ; by
-        // default this will incur a 10ms delay before it's executed, messing with the timing
-        // of the log and having the checks for counts in equals() below flake.
-        mExecutor.executeDirect = true;
-
-        // First nc changed triggered by verifySetupPlatformVpn
-        final Pattern pattern = Pattern.compile("Cap changed from", Pattern.MULTILINE);
-        final String stage1 = getDump(vpnSnapShot.vpn);
-        assertEquals(1, countMatches(pattern, stage1));
-
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
-        final String stage2 = getDump(vpnSnapShot.vpn);
-        // Was the same caps, there should still be only 1 match
-        assertEquals(1, countMatches(pattern, stage2));
-
-        ncBuilder.setLinkDownstreamBandwidthKbps(1200)
-                .setLinkUpstreamBandwidthKbps(300);
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
-        final String stage3 = getDump(vpnSnapShot.vpn);
-        // Was not an important change, should not be logged, still only 1 match
-        assertEquals(1, countMatches(pattern, stage3));
-
-        ncBuilder.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
-        final String stage4 = getDump(vpnSnapShot.vpn);
-        // Change to caps is important, should cause a new match
-        assertEquals(2, countMatches(pattern, stage4));
-
-        ncBuilder.removeCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
-        ncBuilder.setLinkDownstreamBandwidthKbps(600);
-        vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
-        final String stage5 = getDump(vpnSnapShot.vpn);
-        // Change to caps is important, should cause a new match even with the unimportant change
-        assertEquals(3, countMatches(pattern, stage5));
-    }
-    // TODO : beef up event logs tests
-
-    private void verifyHandlingNetworkLoss(PlatformVpnSnapshot vpnSnapShot) throws Exception {
-        // Forget the #sendLinkProperties during first setup.
-        reset(mMockNetworkAgent);
-
-        // Mock network loss
-        vpnSnapShot.nwCb.onLost(TEST_NETWORK);
-
-        // Mock the grace period expires
-        verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any());
-
-        final ArgumentCaptor<LinkProperties> lpCaptor =
-                ArgumentCaptor.forClass(LinkProperties.class);
-        verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS))
-                .doSendLinkProperties(lpCaptor.capture());
-        final LinkProperties lp = lpCaptor.getValue();
-
-        assertNull(lp.getInterfaceName());
-        final List<RouteInfo> expectedRoutes = Arrays.asList(
-                new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null /* gateway */,
-                        null /* iface */, RTN_UNREACHABLE),
-                new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null /* gateway */,
-                        null /* iface */, RTN_UNREACHABLE));
-        assertEquals(expectedRoutes, lp.getRoutes());
-
-        verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS)).unregister();
-    }
-
-    @Test
-    public void testStartPlatformVpnHandlesNetworkLoss_mobikeEnabled() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-        verifyHandlingNetworkLoss(vpnSnapShot);
-    }
-
-    @Test
-    public void testStartPlatformVpnHandlesNetworkLoss_mobikeDisabled() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
-        verifyHandlingNetworkLoss(vpnSnapShot);
-    }
-
-    private ConnectivityDiagnosticsCallback getConnectivityDiagCallback() {
-        final ArgumentCaptor<ConnectivityDiagnosticsCallback> cdcCaptor =
-                ArgumentCaptor.forClass(ConnectivityDiagnosticsCallback.class);
-        verify(mCdm).registerConnectivityDiagnosticsCallback(
-                any(), any(), cdcCaptor.capture());
-        return cdcCaptor.getValue();
-    }
-
-    private DataStallReport createDataStallReport() {
-        return new DataStallReport(TEST_NETWORK, 1234 /* reportTimestamp */,
-                1 /* detectionMethod */, new LinkProperties(), new NetworkCapabilities(),
-                new PersistableBundle());
-    }
-
-    private void verifyMobikeTriggered(List<Network> expected, int retryIndex) {
-        // Verify retry is scheduled
-        final long expectedDelayMs = mTestDeps.getValidationFailRecoveryMs(retryIndex);
-        final ArgumentCaptor<Long> delayCaptor = ArgumentCaptor.forClass(Long.class);
-        verify(mExecutor, times(retryIndex + 1)).schedule(
-                any(Runnable.class), delayCaptor.capture(), eq(TimeUnit.MILLISECONDS));
-        final List<Long> delays = delayCaptor.getAllValues();
-        assertEquals(expectedDelayMs, (long) delays.get(delays.size() - 1));
-
-        final ArgumentCaptor<Network> networkCaptor = ArgumentCaptor.forClass(Network.class);
-        verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS + expectedDelayMs))
-                .setNetwork(networkCaptor.capture(), anyInt() /* ipVersion */,
-                        anyInt() /* encapType */, anyInt() /* keepaliveDelay */);
-        assertEquals(expected, Collections.singletonList(networkCaptor.getValue()));
-    }
-
-    @Test
-    public void testDataStallInIkev2VpnMobikeDisabled() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
-
-        doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-
-        // Should not trigger MOBIKE if MOBIKE is not enabled
-        verify(mIkeSessionWrapper, never()).setNetwork(any() /* network */,
-                anyInt() /* ipVersion */, anyInt() /* encapType */, anyInt() /* keepaliveDelay */);
-    }
-
-    @Test
-    public void testDataStallInIkev2VpnRecoveredByMobike() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
-        doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-        // Verify MOBIKE is triggered
-        verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
-                0 /* retryIndex */);
-        // Validation failure on VPN network should trigger a re-evaluation request for the
-        // underlying network.
-        verify(mConnectivityManager).reportNetworkConnectivity(TEST_NETWORK, false);
-
-        reset(mIkev2SessionCreator);
-        reset(mExecutor);
-
-        // Send validation status update.
-        // Recovered and get network validated. It should not trigger the ike session reset.
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_VALID);
-        // Verify that the retry count is reset. The mValidationFailRetryCount will not be reset
-        // until the executor finishes the execute() call, so wait until the all tasks are executed.
-        waitForIdleSerialExecutor(mExecutor, TEST_TIMEOUT_MS);
-        assertEquals(0,
-                ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).mValidationFailRetryCount);
-        verify(mIkev2SessionCreator, never()).createIkeSession(
-                any(), any(), any(), any(), any(), any());
-
-        reset(mIkeSessionWrapper);
-        reset(mExecutor);
-
-        // Another validation fail should trigger another reportNetworkConnectivity
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-        verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
-                0 /* retryIndex */);
-        verify(mConnectivityManager, times(2)).reportNetworkConnectivity(TEST_NETWORK, false);
-    }
-
-    @Test
-    public void testDataStallInIkev2VpnNotRecoveredByMobike() throws Exception {
-        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
-                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
-
-        int retry = 0;
-        doReturn(TEST_NETWORK).when(mMockNetworkAgent).getNetwork();
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-        verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
-                retry++);
-        // Validation failure on VPN network should trigger a re-evaluation request for the
-        // underlying network.
-        verify(mConnectivityManager).reportNetworkConnectivity(TEST_NETWORK, false);
-        reset(mIkev2SessionCreator);
-
-        // Second validation status update.
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-        verifyMobikeTriggered(vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks(),
-                retry++);
-        // Call to reportNetworkConnectivity should only happen once. No further interaction.
-        verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false);
-
-        // Use real delay to verify reset session will not be performed if there is an existing
-        // recovery for resetting the session.
-        mExecutor.delayMs = TestExecutor.REAL_DELAY;
-        mExecutor.executeDirect = true;
-        // Send validation status update should result in ike session reset.
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-
-        // Verify session reset is scheduled
-        long expectedDelay = mTestDeps.getValidationFailRecoveryMs(retry++);
-        final ArgumentCaptor<Long> delayCaptor = ArgumentCaptor.forClass(Long.class);
-        verify(mExecutor, times(retry)).schedule(any(Runnable.class), delayCaptor.capture(),
-                eq(TimeUnit.MILLISECONDS));
-        final List<Long> delays = delayCaptor.getAllValues();
-        assertEquals(expectedDelay, (long) delays.get(delays.size() - 1));
-        // Call to reportNetworkConnectivity should only happen once. No further interaction.
-        verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false);
-
-        // Another invalid status reported should not trigger other scheduled recovery.
-        expectedDelay = mTestDeps.getValidationFailRecoveryMs(retry++);
-        ((Vpn.IkeV2VpnRunner) vpnSnapShot.vpn.mVpnRunner).onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_NOT_VALID);
-        verify(mExecutor, never()).schedule(
-                any(Runnable.class), eq(expectedDelay), eq(TimeUnit.MILLISECONDS));
-
-        // Verify that session being reset
-        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS + expectedDelay))
-                .createIkeSession(any(), any(), any(), any(), any(), any());
-        // Call to reportNetworkConnectivity should only happen once. No further interaction.
-        verify(mConnectivityManager, times(1)).reportNetworkConnectivity(TEST_NETWORK, false);
-    }
-
-    @Test
-    public void testStartLegacyVpnType() throws Exception {
-        setMockedUsers(PRIMARY_USER);
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final VpnProfile profile = new VpnProfile("testProfile" /* key */);
-
-        profile.type = VpnProfile.TYPE_PPTP;
-        assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile));
-        profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK;
-        assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile));
-    }
-
-    @Test
-    public void testStartLegacyVpnModifyProfile_TypePSK() throws Exception {
-        setMockedUsers(PRIMARY_USER);
-        final Vpn vpn = createVpn(PRIMARY_USER.id);
-        final Ikev2VpnProfile ikev2VpnProfile =
-                new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
-                        .setAuthPsk(TEST_VPN_PSK)
-                        .build();
-        final VpnProfile profile = ikev2VpnProfile.toVpnProfile();
-
-        startLegacyVpn(vpn, profile);
-        assertEquals(profile, ikev2VpnProfile.toVpnProfile());
-    }
-
-    private void assertTransportInfoMatches(NetworkCapabilities nc, int type) {
-        assertNotNull(nc);
-        VpnTransportInfo ti = (VpnTransportInfo) nc.getTransportInfo();
-        assertNotNull(ti);
-        assertEquals(type, ti.getType());
-    }
-
-    // Make it public and un-final so as to spy it
-    public class TestDeps extends Vpn.Dependencies {
-        TestDeps() {}
-
-        @Override
-        public boolean isCallerSystem() {
-            return true;
-        }
-
-        @Override
-        public PendingIntent getIntentForStatusPanel(Context context) {
-            return null;
-        }
-
-        @Override
-        public ParcelFileDescriptor adoptFd(Vpn vpn, int mtu) {
-            return new ParcelFileDescriptor(new FileDescriptor());
-        }
-
-        @Override
-        public int jniCreate(Vpn vpn, int mtu) {
-            // Pick a random positive number as fd to return.
-            return 345;
-        }
-
-        @Override
-        public String jniGetName(Vpn vpn, int fd) {
-            return TEST_IFACE_NAME;
-        }
-
-        @Override
-        public int jniSetAddresses(Vpn vpn, String interfaze, String addresses) {
-            if (addresses == null) return 0;
-            // Return the number of addresses.
-            return addresses.split(" ").length;
-        }
-
-        @Override
-        public void setBlocking(FileDescriptor fd, boolean blocking) {}
-
-        @Override
-        public DeviceIdleInternal getDeviceIdleInternal() {
-            return mDeviceIdleInternal;
-        }
-
-        @Override
-        public long getValidationFailRecoveryMs(int retryCount) {
-            // Simply return retryCount as the delay seconds for retrying.
-            return retryCount * 100L;
-        }
-
-        @Override
-        public ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor() {
-            return mExecutor;
-        }
-
-        public boolean mIgnoreCallingUidChecks = true;
-        @Override
-        public void verifyCallingUidAndPackage(Context context, String packageName, int userId) {
-            if (!mIgnoreCallingUidChecks) {
-                super.verifyCallingUidAndPackage(context, packageName, userId);
-            }
-        }
-    }
-
-    /**
-     * Mock some methods of vpn object.
-     */
-    private Vpn createVpn(@UserIdInt int userId) {
-        final Context asUserContext = mock(Context.class, AdditionalAnswers.delegatesTo(mContext));
-        doReturn(UserHandle.of(userId)).when(asUserContext).getUser();
-        when(mContext.createContextAsUser(eq(UserHandle.of(userId)), anyInt()))
-                .thenReturn(asUserContext);
-        final TestLooper testLooper = new TestLooper();
-        final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, mTestDeps, mNetService,
-                mNetd, userId, mVpnProfileStore, mSystemServices, mIkev2SessionCreator);
-        verify(mConnectivityManager, times(1)).registerNetworkProvider(argThat(
-                provider -> provider.getName().contains("VpnNetworkProvider")
-        ));
-        return vpn;
-    }
-
-    /**
-     * Populate {@link #mUserManager} with a list of fake users.
-     */
-    private void setMockedUsers(UserInfo... users) {
-        final Map<Integer, UserInfo> userMap = new ArrayMap<>();
-        for (UserInfo user : users) {
-            userMap.put(user.id, user);
-        }
-
-        /**
-         * @see UserManagerService#getUsers(boolean)
-         */
-        doAnswer(invocation -> {
-            final ArrayList<UserInfo> result = new ArrayList<>(users.length);
-            for (UserInfo ui : users) {
-                if (ui.isEnabled() && !ui.partial) {
-                    result.add(ui);
-                }
-            }
-            return result;
-        }).when(mUserManager).getAliveUsers();
-
-        doAnswer(invocation -> {
-            final int id = (int) invocation.getArguments()[0];
-            return userMap.get(id);
-        }).when(mUserManager).getUserInfo(anyInt());
-    }
-
-    /**
-     * Populate {@link #mPackageManager} with a fake packageName-to-UID mapping.
-     */
-    private void setMockedPackages(final Map<String, Integer> packages) {
-        try {
-            doAnswer(invocation -> {
-                final String appName = (String) invocation.getArguments()[0];
-                final int userId = (int) invocation.getArguments()[1];
-                Integer appId = packages.get(appName);
-                if (appId == null) throw new PackageManager.NameNotFoundException(appName);
-                return UserHandle.getUid(userId, appId);
-            }).when(mPackageManager).getPackageUidAsUser(anyString(), anyInt());
-        } catch (Exception e) {
-        }
-    }
-}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index f753c93..b8ebf0f 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -38,6 +38,7 @@
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.waitForIdle
 import java.net.NetworkInterface
+import java.time.Duration
 import java.util.Objects
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.TimeUnit
@@ -313,9 +314,9 @@
                 eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any(), any()
         )
         verify(mockInterfaceAdvertiser1).addService(
-                anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE))
+                anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE), any())
         verify(mockInterfaceAdvertiser2).addService(
-                anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE))
+                anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE), any())
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
         postSync { intAdvCbCaptor1.value.onServiceProbingSucceeded(
@@ -489,15 +490,15 @@
                 eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any(), any()
         )
         verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
-                argThat { it.matches(SERVICE_1) })
+                argThat { it.matches(SERVICE_1) }, any())
         verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_2),
-                argThat { it.matches(expectedRenamed) })
+                argThat { it.matches(expectedRenamed) }, any())
         verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_1),
-                argThat { it.matches(LONG_SERVICE_1) })
+                argThat { it.matches(LONG_SERVICE_1) }, any())
         verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_2),
-            argThat { it.matches(expectedLongRenamed) })
+            argThat { it.matches(expectedLongRenamed) }, any())
         verify(mockInterfaceAdvertiser1).addService(eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
-            argThat { it.matches(expectedCaseInsensitiveRenamed) })
+            argThat { it.matches(expectedCaseInsensitiveRenamed) }, any())
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
         postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
@@ -532,7 +533,7 @@
         postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
 
         verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
-                argThat { it.matches(ALL_NETWORKS_SERVICE) })
+                argThat { it.matches(ALL_NETWORKS_SERVICE) }, any())
 
         val updateOptions = MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(true).build()
 
@@ -554,7 +555,24 @@
         // Newly created MdnsInterfaceAdvertiser will get addService() call.
         postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_2, mockSocket2, listOf(TEST_LINKADDR2)) }
         verify(mockInterfaceAdvertiser2).addService(eq(SERVICE_ID_1),
-                argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
+                argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) }, any())
+    }
+
+    @Test
+    fun testAddOrUpdateService_customTtl_registeredSuccess() {
+        val advertiser = MdnsAdvertiser(
+                thread.looper, socketProvider, cb, mockDeps, sharedlog, flags, context)
+        val updateOptions =
+                MdnsAdvertisingOptions.newBuilder().setTtl(Duration.ofSeconds(30)).build()
+
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
+                updateOptions, TEST_CLIENT_UID_1) }
+
+        val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
+        verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
+        val socketCb = socketCbCaptor.value
+        postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
+        verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1), any(), eq(updateOptions))
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index 0637ad1..28608bb 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -132,7 +132,7 @@
             knownServices.add(inv.getArgument(0))
 
             -1
-        }.`when`(repository).addService(anyInt(), any())
+        }.`when`(repository).addService(anyInt(), any(), any())
         doAnswer { inv ->
             knownServices.remove(inv.getArgument(0))
             null
@@ -403,9 +403,10 @@
     @Test
     fun testReplaceExitingService() {
         doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
-                .addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
-        advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1_SUBTYPE)
-        verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+                .addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
+        advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1_SUBTYPE,
+                MdnsAdvertisingOptions.getDefaultOptions())
+        verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
         verify(announcer).stop(TEST_SERVICE_ID_DUPLICATE)
         verify(prober).startProbing(any())
     }
@@ -413,7 +414,7 @@
     @Test
     fun testUpdateExistingService() {
         doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
-                .addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+                .addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
         val subTypes = setOf("_sub")
         advertiser.updateService(TEST_SERVICE_ID_DUPLICATE, subTypes)
         verify(repository).updateService(eq(TEST_SERVICE_ID_DUPLICATE), any())
@@ -427,8 +428,8 @@
         doReturn(serviceId).`when`(testProbingInfo).serviceId
         doReturn(testProbingInfo).`when`(repository).setServiceProbing(serviceId)
 
-        advertiser.addService(serviceId, serviceInfo)
-        verify(repository).addService(serviceId, serviceInfo)
+        advertiser.addService(serviceId, serviceInfo, MdnsAdvertisingOptions.getDefaultOptions())
+        verify(repository).addService(serviceId, serviceInfo, null /* ttl */)
         verify(prober).startProbing(testProbingInfo)
 
         // Simulate probing success: continues to announcing
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index fd8d98b..8d1dff6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -145,7 +145,8 @@
     fun testAddServiceAndProbe() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         assertEquals(0, repository.servicesCount)
-        assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
+        assertEquals(-1,
+                repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */))
         assertEquals(1, repository.servicesCount)
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -180,10 +181,10 @@
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
         assertFailsWith(NameConflictException::class) {
-            repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1)
+            repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* ttl */)
         }
         assertFailsWith(NameConflictException::class) {
-            repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3)
+            repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3, null /* ttl */)
         }
     }
 
@@ -224,9 +225,9 @@
     @Test
     fun testInvalidReuseOfServiceId() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
         assertFailsWith(IllegalArgumentException::class) {
-            repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2)
+            repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* ttl */)
         }
     }
 
@@ -235,7 +236,7 @@
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
         assertFalse(repository.hasActiveService(TEST_SERVICE_ID_1))
 
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
         assertTrue(repository.hasActiveService(TEST_SERVICE_ID_1))
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -327,7 +328,7 @@
         repository.exitService(TEST_SERVICE_ID_1)
 
         assertEquals(TEST_SERVICE_ID_1,
-                repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1))
+                repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* ttl */))
         assertEquals(1, repository.servicesCount)
 
         repository.removeService(TEST_SERVICE_ID_2)
@@ -824,7 +825,7 @@
         repository.initWithService(
                 TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, subtypes = setOf(),
                 listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
-        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
         val src = InetSocketAddress(parseNumericAddress("fe80::123"), 5353)
 
         val query = makeQuery(TYPE_AAAA to TEST_CUSTOM_HOST_1_NAME)
@@ -890,7 +891,8 @@
         repository.initWithService(
                 TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, subtypes = setOf(),
                 listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
-        repository.addService(TEST_SERVICE_CUSTOM_HOST_ID_1, TEST_SERVICE_CUSTOM_HOST_1)
+        repository.addService(
+                TEST_SERVICE_CUSTOM_HOST_ID_1, TEST_SERVICE_CUSTOM_HOST_1, null /* ttl */)
         repository.removeService(TEST_CUSTOM_HOST_ID_1)
         repository.removeService(TEST_SERVICE_CUSTOM_HOST_ID_1)
 
@@ -989,8 +991,8 @@
     @Test
     fun testGetConflictingServices() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
-        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
 
         val packet = MdnsPacket(
                 0 /* flags */,
@@ -1020,8 +1022,8 @@
     @Test
     fun testGetConflictingServicesCaseInsensitive() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
-        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
 
         val packet = MdnsPacket(
             0 /* flags */,
@@ -1050,8 +1052,8 @@
     @Test
     fun testGetConflictingServices_customHosts_differentAddresses() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
-        repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1)
-        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+        repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
 
         val packet = MdnsPacket(
                 0, /* flags */
@@ -1074,8 +1076,8 @@
     @Test
     fun testGetConflictingServices_customHosts_moreAddressesThanUs_conflict() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
-        repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1)
-        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+        repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
 
         val packet = MdnsPacket(
                 0, /* flags */
@@ -1101,8 +1103,8 @@
     @Test
     fun testGetConflictingServices_customHostsReplyHasFewerAddressesThanUs_noConflict() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
-        repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1)
-        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+        repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
 
         val packet = MdnsPacket(
                 0, /* flags */
@@ -1122,8 +1124,8 @@
     @Test
     fun testGetConflictingServices_customHostsReplyHasIdenticalHosts_noConflict() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
-        repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1)
-        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+        repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
 
         val packet = MdnsPacket(
                 0, /* flags */
@@ -1147,8 +1149,8 @@
     @Test
     fun testGetConflictingServices_customHostsCaseInsensitiveReplyHasIdenticalHosts_noConflict() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
-        repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1)
-        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2)
+        repository.addService(TEST_CUSTOM_HOST_ID_1, TEST_CUSTOM_HOST_1, null /* ttl */)
+        repository.addService(TEST_CUSTOM_HOST_ID_2, TEST_CUSTOM_HOST_2, null /* ttl */)
 
         val packet = MdnsPacket(
                 0, /* flags */
@@ -1171,8 +1173,8 @@
     @Test
     fun testGetConflictingServices_IdenticalService() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
-        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
 
         val otherTtlMillis = 1234L
         val packet = MdnsPacket(
@@ -1200,8 +1202,8 @@
     @Test
     fun testGetConflictingServicesCaseInsensitive_IdenticalService() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, makeFlags())
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
-        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* ttl */)
 
         val otherTtlMillis = 1234L
         val packet = MdnsPacket(
@@ -1256,7 +1258,8 @@
             makeFlags(includeInetAddressesInProbing = true))
         repository.updateAddresses(TEST_ADDRESSES)
         assertEquals(0, repository.servicesCount)
-        assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
+        assertEquals(-1,
+                repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* ttl */))
         assertEquals(1, repository.servicesCount)
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -1689,7 +1692,7 @@
     serviceId: Int,
     serviceInfo: NsdServiceInfo
 ): AnnouncementInfo {
-    addService(serviceId, serviceInfo)
+    addService(serviceId, serviceInfo, null /* ttl */)
     val probingInfo = setServiceProbing(serviceId)
     assertNotNull(probingInfo)
     return onProbingSucceeded(probingInfo)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
index e7d7a98..8740e80 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
@@ -35,6 +35,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.time.Instant;
 import java.util.List;
 import java.util.Map;
 
@@ -202,7 +203,8 @@
                         List.of(),
                         /* textEntries= */ null,
                         /* interfaceIndex= */ 20,
-                        network);
+                        network,
+                        Instant.MAX /* expirationTime */);
 
         assertEquals(network, info2.getNetwork());
     }
@@ -225,7 +227,8 @@
                                 MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max"),
                                 MdnsServiceInfo.TextEntry.fromString("test=")),
                         20 /* interfaceIndex */,
-                        new Network(123));
+                        new Network(123),
+                        Instant.MAX /* expirationTime */);
 
         beforeParcel.writeToParcel(parcel, 0);
         parcel.setDataPosition(0);
diff --git a/tests/unit/vpn-jarjar-rules.txt b/tests/unit/vpn-jarjar-rules.txt
index 1a6bddc..f74eab8 100644
--- a/tests/unit/vpn-jarjar-rules.txt
+++ b/tests/unit/vpn-jarjar-rules.txt
@@ -1,4 +1,2 @@
 # Only keep classes imported by ConnectivityServiceTest
-keep com.android.server.connectivity.Vpn
 keep com.android.server.connectivity.VpnProfileStore
-keep com.android.server.net.LockdownVpnTracker
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 676eb0e..c1cf0a0 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -42,6 +42,7 @@
         "guava",
         "guava-android-testlib",
         "net-tests-utils",
+        "ThreadNetworkTestUtils",
         "truth",
     ],
     libs: [
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 3bec36b..36ce4d5 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -17,7 +17,6 @@
 package android.net.thread.cts;
 
 import static android.Manifest.permission.ACCESS_NETWORK_STATE;
-import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_ROUTER;
@@ -33,7 +32,6 @@
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
-import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
 import static com.android.testutils.TestPermissionUtil.runAsShell;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -48,7 +46,6 @@
 
 import android.content.Context;
 import android.net.ConnectivityManager;
-import android.net.LinkAddress;
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
@@ -62,7 +59,9 @@
 import android.net.thread.ThreadNetworkController.StateCallback;
 import android.net.thread.ThreadNetworkException;
 import android.net.thread.ThreadNetworkManager;
+import android.net.thread.utils.TapTestNetworkTracker;
 import android.os.Build;
+import android.os.HandlerThread;
 import android.os.OutcomeReceiver;
 
 import androidx.annotation.NonNull;
@@ -74,7 +73,6 @@
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.FunctionalUtils.ThrowingRunnable;
-import com.android.testutils.TestNetworkTracker;
 
 import org.junit.After;
 import org.junit.Before;
@@ -110,7 +108,7 @@
     private static final int NETWORK_CALLBACK_TIMEOUT_MILLIS = 10 * 1000;
     private static final int CALLBACK_TIMEOUT_MILLIS = 1_000;
     private static final int ENABLED_TIMEOUT_MILLIS = 2_000;
-    private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 10 * 1000;
+    private static final int SERVICE_DISCOVERY_TIMEOUT_MILLIS = 30_000;
     private static final String MESHCOP_SERVICE_TYPE = "_meshcop._udp";
     private static final String THREAD_NETWORK_PRIVILEGED =
             "android.permission.THREAD_NETWORK_PRIVILEGED";
@@ -123,12 +121,11 @@
     private NsdManager mNsdManager;
 
     private Set<String> mGrantedPermissions;
+    private HandlerThread mHandlerThread;
+    private TapTestNetworkTracker mTestNetworkTracker;
 
     @Before
     public void setUp() throws Exception {
-
-        mGrantedPermissions = new HashSet<String>();
-        mExecutor = Executors.newSingleThreadExecutor();
         ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
         if (manager != null) {
             mController = manager.getAllThreadNetworkControllers().get(0);
@@ -138,20 +135,23 @@
         // tests if a feature is not available.
         assumeNotNull(mController);
 
-        setEnabledAndWait(mController, true);
-
+        mGrantedPermissions = new HashSet<String>();
+        mExecutor = Executors.newSingleThreadExecutor();
         mNsdManager = mContext.getSystemService(NsdManager.class);
+        mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+        mHandlerThread.start();
+
+        setEnabledAndWait(mController, true);
     }
 
     @After
     public void tearDown() throws Exception {
-        if (mController != null) {
-            grantPermissions(THREAD_NETWORK_PRIVILEGED);
-            CompletableFuture<Void> future = new CompletableFuture<>();
-            mController.leave(mExecutor, future::complete);
-            future.get(LEAVE_TIMEOUT_MILLIS, MILLISECONDS);
+        if (mController == null) {
+            return;
         }
         dropAllPermissions();
+        leaveAndWait(mController);
+        tearDownTestNetwork();
     }
 
     @Test
@@ -829,7 +829,7 @@
 
     @Test
     public void meshcopService_threadEnabledButNotJoined_discoveredButNoNetwork() throws Exception {
-        TestNetworkTracker testNetwork = setUpTestNetwork();
+        setUpTestNetwork();
 
         setEnabledAndWait(mController, true);
         leaveAndWait(mController);
@@ -845,13 +845,11 @@
         assertThat(txtMap.get("rv")).isNotNull();
         assertThat(txtMap.get("tv")).isNotNull();
         assertThat(txtMap.get("sb")).isNotNull();
-
-        tearDownTestNetwork(testNetwork);
     }
 
     @Test
     public void meshcopService_joinedNetwork_discoveredHasNetwork() throws Exception {
-        TestNetworkTracker testNetwork = setUpTestNetwork();
+        setUpTestNetwork();
 
         String networkName = "TestNet" + new Random().nextInt(10_000);
         joinRandomizedDatasetAndWait(mController, networkName);
@@ -872,27 +870,26 @@
         assertThat(txtMap.get("tv")).isNotNull();
         assertThat(txtMap.get("sb")).isNotNull();
         assertThat(txtMap.get("id").length).isEqualTo(16);
-
-        tearDownTestNetwork(testNetwork);
     }
 
     @Test
     public void meshcopService_threadDisabled_notDiscovered() throws Exception {
-        TestNetworkTracker testNetwork = setUpTestNetwork();
+        setUpTestNetwork();
 
         CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
         NsdManager.DiscoveryListener listener =
                 discoverForServiceLost(MESHCOP_SERVICE_TYPE, serviceLostFuture);
         setEnabledAndWait(mController, false);
-
         try {
-            serviceLostFuture.get(10_000, MILLISECONDS);
+            serviceLostFuture.get(SERVICE_DISCOVERY_TIMEOUT_MILLIS, MILLISECONDS);
+        } catch (InterruptedException | ExecutionException | TimeoutException ignored) {
+            // It's fine if the service lost event didn't show up. The service may not ever be
+            // advertised.
         } finally {
             mNsdManager.stopServiceDiscovery(listener);
         }
-        assertThrows(TimeoutException.class, () -> discoverService(MESHCOP_SERVICE_TYPE));
 
-        tearDownTestNetwork(testNetwork);
+        assertThrows(TimeoutException.class, () -> discoverService(MESHCOP_SERVICE_TYPE));
     }
 
     private static void dropAllPermissions() {
@@ -1163,14 +1160,17 @@
         }
     }
 
-    TestNetworkTracker setUpTestNetwork() {
-        return runAsShell(
-                MANAGE_TEST_NETWORKS,
-                () -> initTestNetwork(mContext, new LinkAddress("2001:db8:123::/64"), 10_000));
+    private void setUpTestNetwork() {
+        assertThat(mTestNetworkTracker).isNull();
+        mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper());
     }
 
-    void tearDownTestNetwork(TestNetworkTracker testNetwork) {
-        runAsShell(MANAGE_TEST_NETWORKS, () -> testNetwork.teardown());
+    private void tearDownTestNetwork() throws InterruptedException {
+        if (mTestNetworkTracker != null) {
+            mTestNetworkTracker.tearDown();
+        }
+        mHandlerThread.quitSafely();
+        mHandlerThread.join();
     }
 
     private static class DefaultDiscoveryListener implements NsdManager.DiscoveryListener {
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index 7aaae86..e8ef346 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -374,9 +374,13 @@
         subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_4);
 
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
-        mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
 
         assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+
+        // Verify ping reply from ftd1 and ftd2 separately as the order of replies can't be
+        // predicted.
+        mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
+
         assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
     }
 
@@ -405,11 +409,13 @@
         startFtdChild(ftd2);
         subscribeMulticastAddressAndWait(ftd2, GROUP_ADDR_SCOPE_5);
 
-        // Send the request twice as the order of replies from ftd1 and ftd2 are not guaranteed
-        mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
         mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
 
         assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
+
+        // Send the request twice as the order of replies from ftd1 and ftd2 are not guaranteed
+        mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
+
         assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
     }
 
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 4948c22..60a5f2b 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -16,7 +16,6 @@
 
 package com.android.server.thread;
 
-import static android.Manifest.permission.ACCESS_NETWORK_STATE;
 import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
 import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
 import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
diff --git a/thread/tests/utils/Android.bp b/thread/tests/utils/Android.bp
new file mode 100644
index 0000000..24e9bb9
--- /dev/null
+++ b/thread/tests/utils/Android.bp
@@ -0,0 +1,37 @@
+//
+// Copyright (C) 2023 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 {
+    default_team: "trendy_team_fwk_thread_network",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "ThreadNetworkTestUtils",
+    min_sdk_version: "30",
+    static_libs: [
+        "compatibility-device-util-axt",
+        "net-tests-utils",
+        "net-utils-device-common",
+        "net-utils-device-common-bpf",
+    ],
+    srcs: [
+        "src/**/*.java",
+    ],
+    defaults: [
+        "framework-connectivity-test-defaults",
+    ],
+}
diff --git a/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
new file mode 100644
index 0000000..43f177d
--- /dev/null
+++ b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
@@ -0,0 +1,185 @@
+/*
+ * 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 android.net.thread.utils;
+
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import static com.android.testutils.RecorderCallback.CallbackEntry.LINK_PROPERTIES_CHANGED;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.TestNetworkSpecifier;
+import android.os.Looper;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.testutils.TestableNetworkAgent;
+import com.android.testutils.TestableNetworkCallback;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** A class that can create/destroy a test network based on TAP interface. */
+public final class TapTestNetworkTracker {
+    private static final Duration TIMEOUT = Duration.ofSeconds(2);
+    private final Context mContext;
+    private final Looper mLooper;
+    private TestNetworkInterface mInterface;
+    private TestableNetworkAgent mAgent;
+    private final TestableNetworkCallback mNetworkCallback;
+    private final ConnectivityManager mConnectivityManager;
+
+    /**
+     * Constructs a {@link TapTestNetworkTracker}.
+     *
+     * <p>It creates a TAP interface (e.g. testtap0) and registers a test network using that
+     * interface. It also requests the test network by {@link ConnectivityManager#requestNetwork} so
+     * the test network won't be automatically turned down by {@link
+     * com.android.server.ConnectivityService}.
+     */
+    public TapTestNetworkTracker(Context context, Looper looper) {
+        mContext = context;
+        mLooper = looper;
+        mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
+        mNetworkCallback = new TestableNetworkCallback();
+        runAsShell(MANAGE_TEST_NETWORKS, this::setUpTestNetwork);
+    }
+
+    /** Tears down the test network. */
+    public void tearDown() {
+        runAsShell(MANAGE_TEST_NETWORKS, this::tearDownTestNetwork);
+    }
+
+    /** Returns the interface name of the test network. */
+    public String getInterfaceName() {
+        return mInterface.getInterfaceName();
+    }
+
+    private void setUpTestNetwork() throws Exception {
+        mInterface = mContext.getSystemService(TestNetworkManager.class).createTapInterface();
+
+        mConnectivityManager.requestNetwork(newNetworkRequest(), mNetworkCallback);
+
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(getInterfaceName());
+        mAgent =
+                new TestableNetworkAgent(
+                        mContext,
+                        mLooper,
+                        newNetworkCapabilities(),
+                        lp,
+                        new NetworkAgentConfig.Builder().build());
+        final Network network = mAgent.register();
+        mAgent.markConnected();
+
+        PollingCheck.check(
+                "No usable address on interface",
+                TIMEOUT.toMillis(),
+                () -> hasUsableAddress(network, getInterfaceName()));
+
+        lp.setLinkAddresses(makeLinkAddresses());
+        mAgent.sendLinkProperties(lp);
+        mNetworkCallback.eventuallyExpect(
+                LINK_PROPERTIES_CHANGED,
+                TIMEOUT.toMillis(),
+                l -> !l.getLp().getAddresses().isEmpty());
+    }
+
+    private void tearDownTestNetwork() throws IOException {
+        mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
+        mAgent.unregister();
+        mInterface.getFileDescriptor().close();
+        mAgent.waitForIdle(TIMEOUT.toMillis());
+    }
+
+    private NetworkRequest newNetworkRequest() {
+        return new NetworkRequest.Builder()
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .addTransportType(TRANSPORT_TEST)
+                .setNetworkSpecifier(new TestNetworkSpecifier(getInterfaceName()))
+                .build();
+    }
+
+    private NetworkCapabilities newNetworkCapabilities() {
+        return new NetworkCapabilities()
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .addTransportType(TRANSPORT_TEST)
+                .setNetworkSpecifier(new TestNetworkSpecifier(getInterfaceName()));
+    }
+
+    private List<LinkAddress> makeLinkAddresses() {
+        List<LinkAddress> linkAddresses = new ArrayList<>();
+        List<InterfaceAddress> interfaceAddresses = Collections.emptyList();
+
+        try {
+            interfaceAddresses =
+                    NetworkInterface.getByName(getInterfaceName()).getInterfaceAddresses();
+        } catch (SocketException ignored) {
+            // Ignore failures when getting the addresses.
+        }
+
+        for (InterfaceAddress address : interfaceAddresses) {
+            linkAddresses.add(
+                    new LinkAddress(address.getAddress(), address.getNetworkPrefixLength()));
+        }
+
+        return linkAddresses;
+    }
+
+    private static boolean hasUsableAddress(Network network, String interfaceName) {
+        try {
+            if (NetworkInterface.getByName(interfaceName).getInterfaceAddresses().isEmpty()) {
+                return false;
+            }
+        } catch (SocketException e) {
+            return false;
+        }
+        // Check if the link-local address can be used. Address flags are not available without
+        // elevated permissions, so check that bindSocket works.
+        try {
+            FileDescriptor sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
+            network.bindSocket(sock);
+            Os.connect(sock, parseNumericAddress("ff02::fb%" + interfaceName), 12345);
+            Os.close(sock);
+        } catch (ErrnoException | IOException e) {
+            return false;
+        }
+        return true;
+    }
+}