Snap for 10453563 from 26c3fa06288d0c97a3117f839a3360f3fce845f6 to mainline-os-statsd-release

Change-Id: Id63353d402758fd5f95a304adbf556bdedc4d88b
diff --git a/Android.bp b/Android.bp
index 5994d3f..4d912dd 100644
--- a/Android.bp
+++ b/Android.bp
@@ -2,12 +2,21 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+genrule {
+    name: "statslog-Iwlan-java-gen",
+    tools: ["stats-log-api-gen"],
+    cmd: "$(location stats-log-api-gen) --java $(out) --module iwlan --javaPackage com.google.android.iwlan"
+        + " --javaClass IwlanStatsLog",
+    out: ["com/google/android/iwlan/IwlanStatsLog.java"],
+}
+
 android_app {
     name: "Iwlan",
     manifest: "AndroidManifest.xml",
     srcs: [
         "src/**/*.java",
         "src/**/I*.aidl",
+        ":statslog-Iwlan-java-gen",
     ],
     resource_dirs: [
         "res",
@@ -53,6 +62,7 @@
     srcs: [
         "src/**/*.java",
         "test/**/*.java",
+        ":statslog-Iwlan-java-gen",
     ],
 
     platform_apis: true,
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 6a7a2d4..e0b0b3a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -8,19 +8,14 @@
   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
   <uses-permission android:name="android.permission.INTERNET"/>
+  <uses-permission android:name="android.permission.MANAGE_IPSEC_TUNNELS" />
+  <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/>
   <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
   <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
   <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
   <uses-permission android:name="android.permission.WAKE_LOCK" />
   <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
 
-  <protected-broadcast android:name="IkeAlarmReceiver.ACTION_DELETE_CHILD" />
-  <protected-broadcast android:name="IkeAlarmReceiver.ACTION_REKEY_CHILD" />
-  <protected-broadcast android:name="IkeAlarmReceiver.ACTION_DELETE_IKE" />
-  <protected-broadcast android:name="IkeAlarmReceiver.ACTION_REKEY_IKE" />
-  <protected-broadcast android:name="IkeAlarmReceiver.ACTION_DPD" />
-  <protected-broadcast android:name="IkeAlarmReceiver.ACTION_KEEPALIVE" />
-
   <application
       android:directBootAware="true"
       android:defaultToDeviceProtectedStorage="true">
diff --git a/assets/defaultiwlanerrorconfig.json b/assets/defaultiwlanerrorconfig.json
index eae1ae2..2d2be95 100644
--- a/assets/defaultiwlanerrorconfig.json
+++ b/assets/defaultiwlanerrorconfig.json
@@ -50,6 +50,10 @@
 #         3.4.3. APM_ENABLE_EVENT: APM off to on toggle.
 #         3.4.4. WIFI_AP_CHANGED_EVENT: Wifi is connected to a AP with different SSID.
 #         3.4.5. WIFI_CALLING_DISABLE_EVENT: Wifi calling button on to off toggle.
+#    3.5. "HandoverAttemptCount": Integer to specify the number of handover request attempts before
+#         using initial attach instead. It is an optional field.
+#         Note: This should only be defined in the config when handover attempt count is enabled and
+#         "ErrorType" is explicitly defined as "IKE_PROTOCOL_ERROR_TYPE".
 #
 # Note: When the value is "*" for any of "ApnName" or "ErrorType" or "ErrorDetails",
 #       it means that the config definition applies to rest of the errors for which
@@ -64,20 +68,27 @@
       {
         "ErrorType": "*",
         "ErrorDetails": ["*"],
-        "RetryArray": ["5", "10", "-1"],
+        "RetryArray": ["1", "2", "2", "10", "20", "40", "80", "160", "320", "640", "1280", "1800", "3600", "-1"],
         "UnthrottlingEvents": ["APM_ENABLE_EVENT", "APM_DISABLE_EVENT", "WIFI_DISABLE_EVENT", "WIFI_AP_CHANGED_EVENT"]
       },
       {
         "ErrorType": "GENERIC_ERROR_TYPE",
         "ErrorDetails": ["IO_EXCEPTION"],
-        "RetryArray": ["0", "0", "0", "60+r15", "120", "-1"],
+        "RetryArray": ["0", "0", "0", "30", "60+r15", "120", "-1"],
         "UnthrottlingEvents": ["APM_ENABLE_EVENT", "APM_DISABLE_EVENT", "WIFI_DISABLE_EVENT", "WIFI_AP_CHANGED_EVENT"]
       },
       {
         "ErrorType": "IKE_PROTOCOL_ERROR_TYPE",
-        "ErrorDetails": ["24"],
-        "RetryArray": ["10", "20", "40", "80", "160"],
+        "ErrorDetails": ["*"],
+        "RetryArray": ["5", "10", "10", "20", "40", "80", "160", "320", "640", "1280", "1800", "3600", "-1"],
         "UnthrottlingEvents": ["APM_ENABLE_EVENT", "WIFI_DISABLE_EVENT", "WIFI_CALLING_DISABLE_EVENT"]
+      },
+      {
+        "ErrorType": "IKE_PROTOCOL_ERROR_TYPE",
+        "ErrorDetails": ["36"],
+        "RetryArray": ["0", "0", "0", "10", "20", "40", "80", "160", "320", "640", "1280", "1800", "3600", "-1"],
+        "UnthrottlingEvents": ["APM_ENABLE_EVENT", "WIFI_DISABLE_EVENT", "WIFI_CALLING_DISABLE_EVENT"],
+        "HandoverAttemptCount": 3
       }
     ]
   }
diff --git a/com.google.android.iwlan.xml b/com.google.android.iwlan.xml
index 467a2c9..f91bb8a 100644
--- a/com.google.android.iwlan.xml
+++ b/com.google.android.iwlan.xml
@@ -2,5 +2,7 @@
 <permissions>
     <privapp-permissions package="com.google.android.iwlan">
         <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
+        <permission name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/>
+        <permission name="android.permission.SCHEDULE_EXACT_ALARM"/>
     </privapp-permissions>
 </permissions>
diff --git a/src/com/google/android/iwlan/ErrorPolicyManager.java b/src/com/google/android/iwlan/ErrorPolicyManager.java
index 59fad64..88e9a7f 100644
--- a/src/com/google/android/iwlan/ErrorPolicyManager.java
+++ b/src/com/google/android/iwlan/ErrorPolicyManager.java
@@ -32,12 +32,13 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import com.google.auto.value.AutoValue;
+
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import java.io.BufferedReader;
-import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -50,6 +51,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
@@ -70,8 +72,8 @@
 
     /**
      * This value represents rest of the errors that are not defined above. ErrorDetails should
-     * mention the specific error. If it doesn't not - the policy will be used as a fallback global
-     * policy. Currently Supported ErrorDetails "IO_EXCEPTION" "TIMEOUT_EXCEPTION"
+     * mention the specific error. If it doesn't - the policy will be used as a fallback global
+     * policy. Currently, Supported ErrorDetails "IO_EXCEPTION" "TIMEOUT_EXCEPTION"
      * "SERVER_SELECTION_FAILED" "TUNNEL_TRANSFORM_FAILED"
      */
     private static final int GENERIC_ERROR_TYPE = 2;
@@ -86,8 +88,13 @@
      */
     private static final int IKE_PROTOCOL_ERROR_TYPE = 3;
 
+    static ErrorPolicy.Builder builder() {
+        return new AutoValue_ErrorPolicyManager_ErrorPolicy.Builder()
+                .setInfiniteRetriesWithLastRetryTime(false);
+    }
+
     @IntDef({UNKNOWN_ERROR_TYPE, FALLBACK_ERROR_TYPE, GENERIC_ERROR_TYPE, IKE_PROTOCOL_ERROR_TYPE})
-    @interface ErrorPolicyErrorType {};
+    @interface ErrorPolicyErrorType {}
 
     private static final String[] GENERIC_ERROR_DETAIL_STRINGS = {
         "*",
@@ -116,31 +123,14 @@
     private static final int IKE_PROTOCOL_ERROR_PLMN_NOT_ALLOWED = 11011;
     private static final int IKE_PROTOCOL_ERROR_UNAUTHENTICATED_EMERGENCY_NOT_SUPPORTED = 11055;
 
-    @IntDef({
-        IKE_PROTOCOL_ERROR_PDN_CONNECTION_REJECTION,
-        IKE_PROTOCOL_ERROR_MAX_CONNECTION_REACHED,
-        IKE_PROTOCOL_ERROR_SEMANTIC_ERROR_IN_THE_TFT_OPERATION,
-        IKE_PROTOCOL_ERROR_SYNTACTICAL_ERROR_IN_THE_TFT_OPERATION,
-        IKE_PROTOCOL_ERROR_SEMANTIC_ERRORS_IN_PACKET_FILTERS,
-        IKE_PROTOCOL_ERROR_SYNTACTICAL_ERRORS_IN_PACKET_FILTERS,
-        IKE_PROTOCOL_ERROR_NON_3GPP_ACCESS_TO_EPC_NOT_ALLOWED,
-        IKE_PROTOCOL_ERROR_USER_UNKNOWN,
-        IKE_PROTOCOL_ERROR_NO_APN_SUBSCRIPTION,
-        IKE_PROTOCOL_ERROR_AUTHORIZATION_REJECTED,
-        IKE_PROTOCOL_ERROR_ILLEGAL_ME,
-        IKE_PROTOCOL_ERROR_NETWORK_FAILURE,
-        IKE_PROTOCOL_ERROR_RAT_TYPE_NOT_ALLOWED,
-        IKE_PROTOCOL_ERROR_IMEI_NOT_ACCEPTED,
-        IKE_PROTOCOL_ERROR_PLMN_NOT_ALLOWED,
-        IKE_PROTOCOL_ERROR_UNAUTHENTICATED_EMERGENCY_NOT_SUPPORTED
-    })
-    @interface IkeProtocolErrorType {};
+    /** Private IKEv2 notify message types, as defined in TS 124 502 (section 9.2.4.1) */
+    private static final int IKE_PROTOCOL_ERROR_CONGESTION = 15500;
 
     private final String LOG_TAG;
 
-    private static Map<Integer, ErrorPolicyManager> mInstances = new ConcurrentHashMap<>();
-    private Context mContext;
-    private int mSlotId;
+    private static final Map<Integer, ErrorPolicyManager> mInstances = new ConcurrentHashMap<>();
+    private final Context mContext;
+    private final int mSlotId;
 
     // Policies read from defaultiwlanerrorconfig.json
     // String APN as key to identify the ErrorPolicies associated with it.
@@ -148,15 +138,19 @@
 
     // Policies read from CarrierConfig
     // String APN as key to identify the ErrorPolicies associated with it.
-    private Map<String, List<ErrorPolicy>> mCarrierConfigPolicies = new HashMap<>();
+    private final Map<String, List<ErrorPolicy>> mCarrierConfigPolicies = new HashMap<>();
 
     // String APN as key to identify the ErrorInfo associated with that APN
-    private Map<String, ErrorInfo> mLastErrorForApn = new ConcurrentHashMap<>();
+    private final Map<String, ErrorInfo> mLastErrorForApn = new ConcurrentHashMap<>();
+
+    // Records the most recently reported IwlanError (including NO_ERROR), and the corresponding
+    // APN.
+    private ApnWithIwlanError mMostRecentError;
 
     // List of current Unthrottling events registered with IwlanEventListener
     private Set<Integer> mUnthrottlingEvents;
 
-    private ErrorStats mErrorStats = new ErrorStats();
+    private final ErrorStats mErrorStats = new ErrorStats();
 
     private HandlerThread mHandlerThread;
     @VisibleForTesting Handler mHandler;
@@ -178,11 +172,13 @@
         return mInstances.computeIfAbsent(slotId, k -> new ErrorPolicyManager(context, slotId));
     }
 
+    @VisibleForTesting
+    public static void resetAllInstances() {
+        mInstances.clear();
+    }
+
     /**
      * Release or reset the instance.
-     *
-     * @param context
-     * @param slotId
      */
     public void releaseInstance() {
         Log.d(LOG_TAG, "Release Instance with slotId: " + mSlotId);
@@ -202,6 +198,7 @@
     public synchronized long reportIwlanError(String apn, IwlanError iwlanError) {
         // Fail by default
         long retryTime = -1;
+        mMostRecentError = new ApnWithIwlanError(apn, iwlanError);
 
         if (iwlanError.getErrorType() == IwlanError.NO_ERROR) {
             Log.d(LOG_TAG, "reportIwlanError: NO_ERROR");
@@ -218,7 +215,7 @@
         }
         if (!mLastErrorForApn.containsKey(apn)
                 || !mLastErrorForApn.get(apn).getError().equals(iwlanError)) {
-            Log.d(LOG_TAG, "Doesn't match to the previous error" + iwlanError.toString());
+            Log.d(LOG_TAG, "Doesn't match to the previous error" + iwlanError);
             ErrorPolicy policy = findErrorPolicy(apn, iwlanError);
             ErrorInfo errorInfo = new ErrorInfo(iwlanError, policy);
             mLastErrorForApn.put(apn, errorInfo);
@@ -233,7 +230,7 @@
      *
      * @param apn apn name for which the error happened
      * @param iwlanError Error
-     * @param long backOffTime in seconds
+     * @param backOffTime in seconds
      * @return retry time which is the backoff time. -1 if it is NO_ERROR
      */
     public synchronized long reportIwlanError(String apn, IwlanError iwlanError, long backOffTime) {
@@ -256,7 +253,7 @@
         retryTime = backOffTime;
         if (!mLastErrorForApn.containsKey(apn)
                 || !mLastErrorForApn.get(apn).getError().equals(iwlanError)) {
-            Log.d(LOG_TAG, "Doesn't match to the previous error" + iwlanError.toString());
+            Log.d(LOG_TAG, "Doesn't match to the previous error" + iwlanError);
             ErrorPolicy policy = findErrorPolicy(apn, iwlanError);
             ErrorInfo errorInfo = new ErrorInfo(iwlanError, policy, backOffTime);
             mLastErrorForApn.put(apn, errorInfo);
@@ -292,28 +289,54 @@
      * @return DataFailCause corresponding to the error for the apn
      */
     public synchronized int getDataFailCause(String apn) {
-
         if (!mLastErrorForApn.containsKey(apn)) {
             return DataFailCause.NONE;
         }
         IwlanError error = mLastErrorForApn.get(apn).getError();
+        return getDataFailCause(error);
+    }
+
+    private int getDataFailCause(IwlanError error) {
         int ret = DataFailCause.ERROR_UNSPECIFIED;
-        if (error.getErrorType() == IwlanError.EPDG_SELECTOR_SERVER_SELECTION_FAILED) {
+
+        if (error.getErrorType() == IwlanError.NO_ERROR) {
+            ret = DataFailCause.NONE;
+        } else if (error.getErrorType() == IwlanError.EPDG_SELECTOR_SERVER_SELECTION_FAILED) {
             ret = DataFailCause.IWLAN_DNS_RESOLUTION_NAME_FAILURE;
+        } else if (error.getErrorType() == IwlanError.EPDG_ADDRESS_ONLY_IPV4_ALLOWED) {
+            ret = DataFailCause.ONLY_IPV4_ALLOWED;
+        } else if (error.getErrorType() == IwlanError.EPDG_ADDRESS_ONLY_IPV6_ALLOWED) {
+            ret = DataFailCause.ONLY_IPV6_ALLOWED;
         } else if (error.getErrorType() == IwlanError.IKE_INTERNAL_IO_EXCEPTION) {
             ret = DataFailCause.IWLAN_IKEV2_MSG_TIMEOUT;
         } else if (error.getErrorType() == IwlanError.SIM_NOT_READY_EXCEPTION) {
-            ret = DataFailCause.IWLAN_PDN_CONNECTION_REJECTION;
-        } else if (error.getErrorType() == IwlanError.NETWORK_FAILURE) {
-            ret = DataFailCause.NETWORK_FAILURE;
+            ret = DataFailCause.SIM_CARD_CHANGED;
+        } else if (error.getErrorType()
+                == IwlanError.IKE_SESSION_CLOSED_BEFORE_CHILD_SESSION_OPENED) {
+            ret = DataFailCause.IWLAN_IKE_SESSION_CLOSED_BEFORE_CHILD_SESSION_OPENED;
+        } else if (error.getErrorType() == IwlanError.TUNNEL_NOT_FOUND) {
+            ret = DataFailCause.IWLAN_TUNNEL_NOT_FOUND;
+        } else if (error.getErrorType() == IwlanError.IKE_INIT_TIMEOUT) {
+            ret = DataFailCause.IWLAN_IKE_INIT_TIMEOUT;
+        } else if (error.getErrorType() == IwlanError.IKE_MOBILITY_TIMEOUT) {
+            ret = DataFailCause.IWLAN_IKE_MOBILITY_TIMEOUT;
+        } else if (error.getErrorType() == IwlanError.IKE_DPD_TIMEOUT) {
+            ret = DataFailCause.IWLAN_IKE_DPD_TIMEOUT;
+        } else if (error.getErrorType() == IwlanError.TUNNEL_TRANSFORM_FAILED) {
+            ret = DataFailCause.IWLAN_TUNNEL_TRANSFORM_FAILED;
+        } else if (error.getErrorType() == IwlanError.IKE_NETWORK_LOST_EXCEPTION) {
+            ret = DataFailCause.IWLAN_IKE_NETWORK_LOST_EXCEPTION;
         } else if (error.getErrorType() == IwlanError.IKE_PROTOCOL_EXCEPTION) {
             Exception exception = error.getException();
-            if (exception != null && exception instanceof IkeProtocolException) {
+            if (exception instanceof IkeProtocolException) {
                 int protocolErrorType = ((IkeProtocolException) exception).getErrorType();
                 switch (protocolErrorType) {
                     case IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED:
                         ret = DataFailCause.IWLAN_IKEV2_AUTH_FAILURE;
                         break;
+                    case IkeProtocolException.ERROR_TYPE_INTERNAL_ADDRESS_FAILURE:
+                        ret = DataFailCause.IWLAN_EPDG_INTERNAL_ADDRESS_FAILURE;
+                        break;
                     case IKE_PROTOCOL_ERROR_PDN_CONNECTION_REJECTION:
                         ret = DataFailCause.IWLAN_PDN_CONNECTION_REJECTION;
                         break;
@@ -362,8 +385,11 @@
                     case IKE_PROTOCOL_ERROR_UNAUTHENTICATED_EMERGENCY_NOT_SUPPORTED:
                         ret = DataFailCause.IWLAN_UNAUTHENTICATED_EMERGENCY_NOT_SUPPORTED;
                         break;
+                    case IKE_PROTOCOL_ERROR_CONGESTION:
+                        ret = DataFailCause.IWLAN_CONGESTION;
+                        break;
                     default:
-                        ret = DataFailCause.IWLAN_NETWORK_FAILURE;
+                        ret = DataFailCause.IWLAN_IKE_PRIVATE_PROTOCOL_ERROR;
                         break;
                 }
             }
@@ -371,6 +397,13 @@
         return ret;
     }
 
+    public synchronized int getMostRecentDataFailCause() {
+        if (mMostRecentError != null) {
+            return getDataFailCause(mMostRecentError.mIwlanError);
+        }
+        return DataFailCause.NONE;
+    }
+
     /**
      * Returns the current retryTime based on the lastErrorForApn
      *
@@ -385,6 +418,23 @@
     }
 
     /**
+     * Returns the index of the FQDN to use for ePDG server selection, based on how many FQDNs are
+     * available, the position of the RetryArray index, and configuration of 'NumAttemptsPerFqdn'.
+     *
+     * @param numFqdns number of FQDNs discovered during ePDG server selection.
+     * @return int index of the FQDN to use for ePDG server selection. -1 (invalid) if RetryArray or
+     *     'NumAttemptsPerFqdn' is not specified in the ErrorPolicy.
+     */
+    public synchronized int getCurrentFqdnIndex(int numFqdns) {
+        String apn = mMostRecentError.mApn;
+        if (!mLastErrorForApn.containsKey(apn)) {
+            return -1;
+        }
+        ErrorInfo errorInfo = mLastErrorForApn.get(apn);
+        return errorInfo.getCurrentFqdnIndex(numFqdns);
+    }
+
+    /**
      * Returns the last error for that apn
      *
      * @param apn apn name
@@ -397,6 +447,19 @@
         return new IwlanError(IwlanError.NO_ERROR);
     }
 
+    /**
+     * Returns whether framework should retry tunnel setup with initial PDN bringup request when
+     * handover request fails.
+     *
+     * @param apn apn name
+     * @return boolean result of whether framework should retry tunnel setup with initial PDN
+     *     bringup request when handover request fails
+     */
+    public synchronized boolean shouldRetryWithInitialAttach(String apn) {
+        ErrorInfo errorInfo = mLastErrorForApn.get(apn);
+        return errorInfo != null && errorInfo.shouldRetryWithInitialAttach();
+    }
+
     public void logErrorPolicies() {
         Log.d(LOG_TAG, "mCarrierConfigPolicies:");
         for (Map.Entry<String, List<ErrorPolicy>> entry : mCarrierConfigPolicies.entrySet()) {
@@ -414,7 +477,7 @@
         }
     }
 
-    public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+    public synchronized void dump(PrintWriter pw) {
         pw.println("---- ErrorPolicyManager ----");
         for (Map.Entry<String, ErrorInfo> entry : mLastErrorForApn.entrySet()) {
             pw.print("APN: " + entry.getKey() + " IwlanError: " + entry.getValue().getError());
@@ -484,14 +547,20 @@
         return selectedPolicy;
     }
 
-    private void initHandler() {
+    @VisibleForTesting
+    void initHandler() {
+        mHandler = new EpmHandler(getLooper());
+    }
+
+    @VisibleForTesting
+    Looper getLooper() {
         mHandlerThread = new HandlerThread("ErrorPolicyManagerThread");
         mHandlerThread.start();
-        mHandler = new EpmHandler(mHandlerThread.getLooper());
+        return mHandlerThread.getLooper();
     }
 
     private String getDefaultJSONConfig() throws IOException {
-        String str = "";
+        String str;
         StringBuilder stringBuilder = new StringBuilder();
         InputStream is = mContext.getAssets().open("defaultiwlanerrorconfig.json");
         BufferedReader reader = new BufferedReader(new InputStreamReader(is));
@@ -507,7 +576,8 @@
         return stringBuilder.toString();
     }
 
-    private Map<String, List<ErrorPolicy>> readErrorPolicies(JSONArray apnArray)
+    @VisibleForTesting
+    Map<String, List<ErrorPolicy>> readErrorPolicies(JSONArray apnArray)
             throws JSONException, IllegalArgumentException {
         Map<String, List<ErrorPolicy>> errorPolicies = new HashMap<>();
         for (int i = 0; i < apnArray.length(); i++) {
@@ -521,21 +591,47 @@
 
                 String errorTypeStr = ((String) errorTypeObject.get("ErrorType")).trim();
                 JSONArray errorDetailArray = (JSONArray) errorTypeObject.get("ErrorDetails");
-                int errorType = UNKNOWN_ERROR_TYPE;
+                int errorType;
 
                 if ((errorType = getErrorPolicyErrorType(errorTypeStr)) == UNKNOWN_ERROR_TYPE) {
                     throw new IllegalArgumentException("Unknown error type in the parsing");
                 }
 
-                ErrorPolicy errorPolicy =
-                        new ErrorPolicy(
-                                errorType,
-                                parseErrorDetails(errorType, errorDetailArray),
-                                parseRetryArray((JSONArray) errorTypeObject.get("RetryArray")),
-                                parseUnthrottlingEvents(
-                                        (JSONArray) errorTypeObject.get("UnthrottlingEvents")));
+                List<Integer> retryArray =
+                        parseRetryArray((JSONArray) errorTypeObject.get("RetryArray"));
 
-                errorPolicies.putIfAbsent(apnName, new ArrayList<ErrorPolicy>());
+                ErrorPolicy.Builder errorPolicyBuilder =
+                        builder()
+                                .setErrorType(errorType)
+                                .setErrorDetails(parseErrorDetails(errorType, errorDetailArray))
+                                .setRetryArray(retryArray)
+                                .setUnthrottlingEvents(
+                                        parseUnthrottlingEvents(
+                                                (JSONArray)
+                                                        errorTypeObject.get("UnthrottlingEvents")));
+
+                if (!retryArray.isEmpty() && retryArray.get(retryArray.size() - 1) == -1L) {
+                    errorPolicyBuilder.setInfiniteRetriesWithLastRetryTime(true);
+                }
+
+                if (errorTypeObject.has("NumAttemptsPerFqdn")) {
+                    errorPolicyBuilder.setNumAttemptsPerFqdn(
+                            errorTypeObject.getInt("NumAttemptsPerFqdn"));
+                }
+
+                if (errorTypeObject.has("HandoverAttemptCount")) {
+                    if (errorType != IKE_PROTOCOL_ERROR_TYPE) {
+                        throw new IllegalArgumentException(
+                                "Handover attempt count should not be applied when errorType is not"
+                                        + " explicitly defined as IKE_PROTOCOL_ERROR_TYPE");
+                    }
+                    errorPolicyBuilder.setHandoverAttemptCount(
+                            errorTypeObject.getInt("HandoverAttemptCount"));
+                }
+
+                ErrorPolicy errorPolicy = errorPolicyBuilder.build();
+
+                errorPolicies.putIfAbsent(apnName, new ArrayList<>());
                 errorPolicies.get(apnName).add(errorPolicy);
             }
         }
@@ -551,7 +647,7 @@
             // catch misplaced -1 retry times in the array.
             // 1. if it is not placed at the last position in the array
             // 2. if it is placed in the first position (catches the case where it is
-            //    the only element.
+            //    the only element).
             if (retryTime.equals("-1") && (i != retryArray.length() - 1 || i == 0)) {
                 throw new IllegalArgumentException("Misplaced -1 in retry array");
             }
@@ -621,7 +717,7 @@
         boolean ret = true;
         if (errorDetailStr.contains("-")) {
             // verify range format
-            String rangeNumbers[] = errorDetailStr.split("-");
+            String[] rangeNumbers = errorDetailStr.split("-");
             if (rangeNumbers.length == 2) {
                 for (String range : rangeNumbers) {
                     if (!TextUtils.isDigitsOnly(range)) {
@@ -673,13 +769,13 @@
         for (Map.Entry<String, List<ErrorPolicy>> entry : mCarrierConfigPolicies.entrySet()) {
             List<ErrorPolicy> errorPolicies = entry.getValue();
             for (ErrorPolicy errorPolicy : errorPolicies) {
-                events.addAll(errorPolicy.mUnthrottlingEvents);
+                events.addAll(errorPolicy.unthrottlingEvents());
             }
         }
         for (Map.Entry<String, List<ErrorPolicy>> entry : mDefaultPolicies.entrySet()) {
             List<ErrorPolicy> errorPolicies = entry.getValue();
             for (ErrorPolicy errorPolicy : errorPolicies) {
-                events.addAll(errorPolicy.mUnthrottlingEvents);
+                events.addAll(errorPolicy.unthrottlingEvents());
             }
         }
         events.add(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT);
@@ -693,7 +789,7 @@
      */
     private synchronized void readFromCarrierConfig(int currentCarrierId) {
         String carrierConfigErrorPolicy =
-                (String) IwlanHelper.getConfig(KEY_ERROR_POLICY_CONFIG_STRING, mContext, mSlotId);
+                IwlanHelper.getConfig(KEY_ERROR_POLICY_CONFIG_STRING, mContext, mSlotId);
         if (carrierConfigErrorPolicy == null) {
             Log.e(LOG_TAG, "ErrorPolicy from Carrier Config is NULL");
             return;
@@ -712,9 +808,7 @@
                     LOG_TAG,
                     "Unable to parse the ErrorPolicy from CarrierConfig\n"
                             + carrierConfigErrorPolicy);
-            if (mCarrierConfigPolicies != null) {
-                mCarrierConfigPolicies.clear();
-            }
+            mCarrierConfigPolicies.clear();
             carrierConfigErrorPolicyString = null;
             e.printStackTrace();
         }
@@ -726,18 +820,16 @@
         registerEvents = getAllUnthrottlingEvents();
         mUnthrottlingEvents = getAllUnthrottlingEvents();
 
-        if (registerEvents != null && unregisterEvents != null) {
+        if (unregisterEvents != null) {
             registerEvents.removeAll(unregisterEvents);
             unregisterEvents.removeAll(mUnthrottlingEvents);
         }
 
-        if (registerEvents != null) {
-            IwlanEventListener.getInstance(mContext, mSlotId)
-                    .addEventListener(new ArrayList<Integer>(registerEvents), mHandler);
-        }
+        IwlanEventListener.getInstance(mContext, mSlotId)
+                .addEventListener(new ArrayList<>(registerEvents), mHandler);
         if (unregisterEvents != null) {
             IwlanEventListener.getInstance(mContext, mSlotId)
-                    .removeEventListener(new ArrayList<Integer>(unregisterEvents), mHandler);
+                    .removeEventListener(new ArrayList<>(unregisterEvents), mHandler);
         }
         Log.d(
                 LOG_TAG,
@@ -774,35 +866,55 @@
         return mErrorStats;
     }
 
-    class ErrorPolicy {
-        @ErrorPolicyErrorType int mErrorType;
-        List<String> mErrorDetails;
-        List<Integer> mRetryArray;
-        List<Integer> mUnthrottlingEvents;
+    @AutoValue
+    abstract static class ErrorPolicy {
+        private static final String LOG_TAG = ErrorPolicyManager.class.getSimpleName();
 
-        ErrorPolicy(
-                @ErrorPolicyErrorType int errorType,
-                List<String> errorDetails,
-                List<Integer> retryArray,
-                List<Integer> unthrottlingEvents) {
-            mErrorType = errorType;
-            mErrorDetails = errorDetails;
-            mRetryArray = retryArray;
-            mUnthrottlingEvents = unthrottlingEvents;
+        abstract @ErrorPolicyErrorType int errorType();
+
+        abstract List<String> errorDetails();
+
+        abstract List<Integer> retryArray();
+
+        abstract Boolean infiniteRetriesWithLastRetryTime();
+
+        abstract List<Integer> unthrottlingEvents();
+
+        abstract Optional<Integer> numAttemptsPerFqdn();
+
+        abstract Optional<Integer> handoverAttemptCount();
+
+        @AutoValue.Builder
+        abstract static class Builder {
+            abstract Builder setErrorType(int errorType);
+
+            abstract Builder setErrorDetails(List<String> errorDetails);
+
+            abstract Builder setRetryArray(List<Integer> retryArray);
+
+            abstract Builder setInfiniteRetriesWithLastRetryTime(
+                    Boolean infiniteRetriesWithLastRetryTime);
+
+            abstract Builder setUnthrottlingEvents(List<Integer> unthrottlingEvents);
+
+            abstract Builder setNumAttemptsPerFqdn(Integer numAttemptsPerFqdn);
+
+            abstract Builder setHandoverAttemptCount(Integer handoverAttemptCount);
+
+            abstract ErrorPolicy build();
         }
 
         long getRetryTime(int index) {
             long retryTime = -1;
-            if (mRetryArray.size() > 0) {
+            if (retryArray().size() > 0) {
                 // If the index is greater than or equal to the last element's index
                 // and if the last item in the retryArray is "-1" use the retryTime
                 // of the element before the last element to repeat the element.
-                if (index >= mRetryArray.size() - 1
-                        && mRetryArray.get(mRetryArray.size() - 1) == -1L) {
-                    index = mRetryArray.size() - 2;
+                if (infiniteRetriesWithLastRetryTime()) {
+                    index = Math.min(index, retryArray().size() - 2);
                 }
-                if (index >= 0 && index < mRetryArray.size()) {
-                    retryTime = mRetryArray.get(index);
+                if (index >= 0 && index < retryArray().size()) {
+                    retryTime = retryArray().get(index);
                 }
             }
 
@@ -814,25 +926,39 @@
             return retryTime;
         }
 
+        int getCurrentFqdnIndex(int retryIndex, int numFqdns) {
+            int result = -1;
+            if (numAttemptsPerFqdn().isEmpty() || retryArray().size() <= 0) {
+                return result;
+            }
+            // Cycles between 0 and (numFqdns - 1), based on the current attempt count and size of
+            // mRetryArray.
+            return (retryIndex + 1) / numAttemptsPerFqdn().get() % numFqdns;
+        }
+
         @ErrorPolicyErrorType
         int getErrorType() {
-            return mErrorType;
+            return errorType();
+        }
+
+        int getHandoverAttemptCount() {
+            return handoverAttemptCount().orElse(Integer.MAX_VALUE);
         }
 
         synchronized boolean canUnthrottle(int event) {
-            return mUnthrottlingEvents.contains(event);
+            return unthrottlingEvents().contains(event);
         }
 
         boolean match(IwlanError iwlanError) {
             // Generic by default to match to generic policy.
-            String iwlanErrorDetail = "*";
-            if (mErrorType == FALLBACK_ERROR_TYPE) {
+            String iwlanErrorDetail;
+            if (errorType() == FALLBACK_ERROR_TYPE) {
                 return true;
-            } else if (mErrorType == IKE_PROTOCOL_ERROR_TYPE
+            } else if (errorType() == IKE_PROTOCOL_ERROR_TYPE
                     && iwlanError.getErrorType() == IwlanError.IKE_PROTOCOL_EXCEPTION) {
                 IkeProtocolException exception = (IkeProtocolException) iwlanError.getException();
                 iwlanErrorDetail = String.valueOf(exception.getErrorType());
-            } else if (mErrorType == GENERIC_ERROR_TYPE) {
+            } else if (errorType() == GENERIC_ERROR_TYPE) {
                 iwlanErrorDetail = getGenericErrorDetailString(iwlanError);
                 if (iwlanErrorDetail.equals("UNKNOWN")) {
                     return false;
@@ -842,14 +968,14 @@
             }
 
             boolean ret = false;
-            for (String errorDetail : mErrorDetails) {
-                if (mErrorType == IKE_PROTOCOL_ERROR_TYPE
+            for (String errorDetail : errorDetails()) {
+                if (errorType() == IKE_PROTOCOL_ERROR_TYPE
                         && iwlanError.getErrorType() == IwlanError.IKE_PROTOCOL_EXCEPTION
                         && errorDetail.contains("-")) {
                     // error detail is stored in range format.
                     // ErrorPolicyManager#verifyIkeProtocolErrorDetail will make sure that
                     // this is stored correctly in "min-max" format.
-                    String range[] = errorDetail.split("-");
+                    String[] range = errorDetail.split("-");
                     int min = Integer.parseInt(range[0]);
                     int max = Integer.parseInt(range[1]);
                     int error = Integer.parseInt(iwlanErrorDetail);
@@ -866,18 +992,22 @@
         }
 
         void log() {
-            Log.d(LOG_TAG, "ErrorType: " + mErrorType);
-            Log.d(LOG_TAG, "ErrorDetail: " + Arrays.toString(mErrorDetails.toArray()));
-            Log.d(LOG_TAG, "RetryArray: " + Arrays.toString(mRetryArray.toArray()));
-            Log.d(LOG_TAG, "unthrottlingEvents: " + Arrays.toString(mUnthrottlingEvents.toArray()));
+            Log.d(LOG_TAG, "ErrorType: " + errorType());
+            Log.d(LOG_TAG, "ErrorDetail: " + Arrays.toString(errorDetails().toArray()));
+            Log.d(LOG_TAG, "RetryArray: " + Arrays.toString(retryArray().toArray()));
+            Log.d(
+                    LOG_TAG,
+                    "InfiniteRetriesWithLastRetryTime: " + infiniteRetriesWithLastRetryTime());
+            Log.d(
+                    LOG_TAG,
+                    "UnthrottlingEvents: " + Arrays.toString(unthrottlingEvents().toArray()));
+            Log.d(LOG_TAG, "NumAttemptsPerFqdn: " + numAttemptsPerFqdn());
+            Log.d(LOG_TAG, "handoverAttemptCount: " + handoverAttemptCount());
         }
 
         boolean isFallback() {
-            if ((mErrorType == FALLBACK_ERROR_TYPE)
-                    || (mErrorDetails.size() == 1 && mErrorDetails.get(0).equals("*"))) {
-                return true;
-            }
-            return false;
+            return (errorType() == FALLBACK_ERROR_TYPE)
+                    || (errorDetails().size() == 1 && errorDetails().get(0).equals("*"));
         }
 
         String getGenericErrorDetailString(IwlanError iwlanError) {
@@ -892,7 +1022,25 @@
                 case IwlanError.TUNNEL_TRANSFORM_FAILED:
                     ret = "TUNNEL_TRANSFORM_FAILED";
                     break;
+                case IwlanError.IKE_NETWORK_LOST_EXCEPTION:
+                    ret = "IKE_NETWORK_LOST_EXCEPTION";
+                    break;
+                case IwlanError.EPDG_ADDRESS_ONLY_IPV4_ALLOWED:
+                    ret = "EPDG_ADDRESS_ONLY_IPV4_ALLOWED";
+                    break;
+                case IwlanError.EPDG_ADDRESS_ONLY_IPV6_ALLOWED:
+                    ret = "EPDG_ADDRESS_ONLY_IPV6_ALLOWED";
+                    break;
                     // TODO: Add TIMEOUT_EXCEPTION processing
+                case IwlanError.IKE_INIT_TIMEOUT:
+                    ret = "IKE_INIT_TIMEOUT";
+                    break;
+                case IwlanError.IKE_MOBILITY_TIMEOUT:
+                    ret = "IKE_MOBILITY_TIMEOUT";
+                    break;
+                case IwlanError.IKE_DPD_TIMEOUT:
+                    ret = "IKE_DPD_TIMEOUT";
+                    break;
             }
             return ret;
         }
@@ -901,16 +1049,19 @@
     class ErrorInfo {
         IwlanError mError;
         ErrorPolicy mErrorPolicy;
+
+        // For the lifetime of the ErrorInfo object, this is a monotonically incremented value that
+        // can go beyond the size of mErrorPolicy's mRetryArray.
         int mCurrentRetryIndex;
         long mLastErrorTime;
-        boolean mIsBackOffTimeValid = false;
+        boolean mIsBackOffTimeValid;
         long mBackOffTime;
 
         ErrorInfo(IwlanError error, ErrorPolicy errorPolicy) {
             mError = error;
             mErrorPolicy = errorPolicy;
             mCurrentRetryIndex = -1;
-            mLastErrorTime = new Date().getTime();
+            mLastErrorTime = IwlanHelper.elapsedRealtime();
         }
 
         ErrorInfo(IwlanError error, ErrorPolicy errorPolicy, long backOffTime) {
@@ -919,7 +1070,7 @@
             mCurrentRetryIndex = -1;
             mIsBackOffTimeValid = true;
             mBackOffTime = backOffTime;
-            mLastErrorTime = new Date().getTime();
+            mLastErrorTime = IwlanHelper.elapsedRealtime();
         }
 
         /**
@@ -931,7 +1082,7 @@
                 return -1;
             }
             long time = mErrorPolicy.getRetryTime(++mCurrentRetryIndex);
-            mLastErrorTime = new Date().getTime();
+            mLastErrorTime = IwlanHelper.elapsedRealtime();
             Log.d(LOG_TAG, "Current RetryArray index: " + mCurrentRetryIndex + " time: " + time);
             return time;
         }
@@ -950,7 +1101,7 @@
             } else {
                 time = TimeUnit.SECONDS.toMillis(mErrorPolicy.getRetryTime(mCurrentRetryIndex));
             }
-            long currentTime = new Date().getTime();
+            long currentTime = IwlanHelper.elapsedRealtime();
             time = Math.max(0, time - (currentTime - mLastErrorTime));
             Log.d(
                     LOG_TAG,
@@ -958,13 +1109,18 @@
             return time;
         }
 
+        int getCurrentFqdnIndex(int numFqdns) {
+            ErrorPolicy errorPolicy = getErrorPolicy();
+            return errorPolicy.getCurrentFqdnIndex(mCurrentRetryIndex, numFqdns);
+        }
+
         boolean isBackOffTimeValid() {
             return mIsBackOffTimeValid;
         }
 
         void setBackOffTime(long backOffTime) {
             mBackOffTime = backOffTime;
-            mLastErrorTime = new Date().getTime();
+            mLastErrorTime = IwlanHelper.elapsedRealtime();
         }
 
         boolean canBringUpTunnel() {
@@ -979,7 +1135,7 @@
                 retryTime =
                         TimeUnit.SECONDS.toMillis(mErrorPolicy.getRetryTime(mCurrentRetryIndex));
             }
-            long currentTime = new Date().getTime();
+            long currentTime = IwlanHelper.elapsedRealtime();
             long timeDifference = currentTime - mLastErrorTime;
             if (timeDifference < retryTime) {
                 ret = false;
@@ -987,6 +1143,15 @@
             return ret;
         }
 
+        boolean shouldRetryWithInitialAttach() {
+            // UE should only uses initial attach to reset network failure, not for UE internal or
+            // DNS errors. When the number of handover failures due to network issues exceeds the
+            // configured threshold, UE should request network with initial attach instead of
+            // handover request.
+            return mErrorPolicy.getErrorType() == IKE_PROTOCOL_ERROR_TYPE
+                    && mCurrentRetryIndex + 1 >= mErrorPolicy.getHandoverAttemptCount();
+        }
+
         ErrorPolicy getErrorPolicy() {
             return mErrorPolicy;
         }
@@ -996,15 +1161,23 @@
         }
     }
 
+    static class ApnWithIwlanError {
+        @NonNull final String mApn;
+        @NonNull final IwlanError mIwlanError;
+
+        ApnWithIwlanError(@NonNull String apn, @NonNull IwlanError iwlanError) {
+            mApn = apn;
+            mIwlanError = iwlanError;
+        }
+    }
+
     private boolean isValidCarrierConfigChangedEvent(int currentCarrierId) {
         String errorPolicyConfig =
-                (String) IwlanHelper.getConfig(KEY_ERROR_POLICY_CONFIG_STRING, mContext, mSlotId);
-        boolean isValidEvent =
-                (currentCarrierId != carrierId)
-                        || (carrierConfigErrorPolicyString == null)
-                        || (errorPolicyConfig != null
-                                && !carrierConfigErrorPolicyString.equals(errorPolicyConfig));
-        return isValidEvent;
+                IwlanHelper.getConfig(KEY_ERROR_POLICY_CONFIG_STRING, mContext, mSlotId);
+        return (currentCarrierId != carrierId)
+                || (carrierConfigErrorPolicyString == null)
+                || (errorPolicyConfig != null
+                        && !carrierConfigErrorPolicyString.equals(errorPolicyConfig));
     }
 
     private final class EpmHandler extends Handler {
@@ -1042,12 +1215,12 @@
     }
 
     @VisibleForTesting
-    class ErrorStats {
+    static class ErrorStats {
         @VisibleForTesting Map<String, Map<String, Long>> mStats = new HashMap<>();
         private Date mStartTime;
-        private int mStatCount = 0;
-        private final int APN_COUNT_MAX = 10;
-        private final int ERROR_COUNT_MAX = 1000;
+        private int mStatCount;
+        private static final int APN_COUNT_MAX = 10;
+        private static final int ERROR_COUNT_MAX = 1000;
 
         ErrorStats() {
             mStartTime = Calendar.getInstance().getTime();
@@ -1059,7 +1232,7 @@
                 reset();
             }
             if (!mStats.containsKey(apn)) {
-                mStats.put(apn, new HashMap<String, Long>());
+                mStats.put(apn, new HashMap<>());
             }
             Map<String, Long> errorMap = mStats.get(apn);
             String errorString = error.toString();
@@ -1074,19 +1247,22 @@
 
         void reset() {
             mStartTime = Calendar.getInstance().getTime();
-            mStats = new HashMap<String, Map<String, Long>>();
+            mStats = new HashMap<>();
             mStatCount = 0;
         }
 
         @Override
         public String toString() {
             StringBuilder sb = new StringBuilder();
-            sb.append("mStartTime: " + mStartTime);
+            sb.append("mStartTime: ").append(mStartTime);
             sb.append("\nErrorStats");
             for (Map.Entry<String, Map<String, Long>> entry : mStats.entrySet()) {
-                sb.append("\n\tApn: " + entry.getKey());
+                sb.append("\n\tApn: ").append(entry.getKey());
                 for (Map.Entry<String, Long> errorEntry : entry.getValue().entrySet()) {
-                    sb.append("\n\t  " + errorEntry.getKey() + " : " + errorEntry.getValue());
+                    sb.append("\n\t  ")
+                            .append(errorEntry.getKey())
+                            .append(" : ")
+                            .append(errorEntry.getValue());
                 }
             }
             return sb.toString();
diff --git a/src/com/google/android/iwlan/IwlanBroadcastReceiver.java b/src/com/google/android/iwlan/IwlanBroadcastReceiver.java
index 10b10e4..e6195ba 100644
--- a/src/com/google/android/iwlan/IwlanBroadcastReceiver.java
+++ b/src/com/google/android/iwlan/IwlanBroadcastReceiver.java
@@ -29,6 +29,8 @@
 
 import com.google.android.iwlan.epdg.EpdgSelector;
 
+import java.util.Arrays;
+
 public class IwlanBroadcastReceiver extends BroadcastReceiver {
     private static final String TAG = "IwlanBroadcastReceiver";
 
@@ -96,7 +98,12 @@
                 int pcoId = intent.getIntExtra(TelephonyManager.EXTRA_PCO_ID, 0);
                 byte[] pcoData = intent.getByteArrayExtra(TelephonyManager.EXTRA_PCO_VALUE);
 
-                Log.d(TAG, "PcoID:" + String.format("0x%04x", pcoId) + " PcoData:" + pcoData);
+                Log.d(
+                        TAG,
+                        "PcoID:"
+                                + String.format("0x%04x", pcoId)
+                                + " PcoData:"
+                                + Arrays.toString(pcoData));
 
                 Context mContext = IwlanDataService.getContext();
 
diff --git a/src/com/google/android/iwlan/IwlanDataService.java b/src/com/google/android/iwlan/IwlanDataService.java
index a254535..04b6789 100644
--- a/src/com/google/android/iwlan/IwlanDataService.java
+++ b/src/com/google/android/iwlan/IwlanDataService.java
@@ -26,18 +26,27 @@
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.NetworkSpecifier;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.TransportInfo;
+import android.net.vcn.VcnTransportInfo;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
-import android.support.annotation.GuardedBy;
 import android.support.annotation.IntRange;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.telephony.AccessNetworkConstants.AccessNetworkType;
 import android.telephony.CarrierConfigManager;
 import android.telephony.CellInfo;
+import android.telephony.CellInfoGsm;
+import android.telephony.CellInfoLte;
+import android.telephony.CellInfoNr;
+import android.telephony.CellInfoWcdma;
 import android.telephony.DataFailCause;
 import android.telephony.TelephonyManager;
 import android.telephony.data.ApnSetting;
@@ -51,10 +60,13 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import com.google.android.iwlan.TunnelMetricsInterface.OnClosedMetrics;
+import com.google.android.iwlan.TunnelMetricsInterface.OnOpenedMetrics;
 import com.google.android.iwlan.epdg.EpdgSelector;
 import com.google.android.iwlan.epdg.EpdgTunnelManager;
 import com.google.android.iwlan.epdg.TunnelLinkProperties;
 import com.google.android.iwlan.epdg.TunnelSetupRequest;
+import com.google.android.iwlan.proto.MetricsAtom;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -69,45 +81,58 @@
 import java.util.List;
 import java.util.LongSummaryStatistics;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 
 public class IwlanDataService extends DataService {
 
     private static final String TAG = IwlanDataService.class.getSimpleName();
+
+    private static final String CONTEXT_ATTRIBUTION_TAG = "IWLAN";
     private static Context mContext;
     private IwlanNetworkMonitorCallback mNetworkMonitorCallback;
-    private HandlerThread mNetworkCallbackHandlerThread;
     private static boolean sNetworkConnected = false;
     private static Network sNetwork = null;
-    // TODO: Change this to a hashmap as there is only one provider per slot
-    private static List<IwlanDataServiceProvider> sIwlanDataServiceProviderList =
-            new ArrayList<IwlanDataServiceProvider>();
+    private static LinkProperties sLinkProperties = null;
+    @VisibleForTesting Handler mIwlanDataServiceHandler;
+    private HandlerThread mIwlanDataServiceHandlerThread;
+    private static final Map<Integer, IwlanDataServiceProvider> sIwlanDataServiceProviders =
+            new ConcurrentHashMap<>();
+    private static final int INVALID_SUB_ID = -1;
+
+    // The current subscription with the active internet PDN. Need not be the default data sub.
+    // If internet is over WiFi, this value will be INVALID_SUB_ID.
+    private static int mConnectedDataSub = INVALID_SUB_ID;
+
+    private static final int EVENT_BASE = IwlanEventListener.DATA_SERVICE_INTERNAL_EVENT_BASE;
+    private static final int EVENT_TUNNEL_OPENED = EVENT_BASE;
+    private static final int EVENT_TUNNEL_CLOSED = EVENT_BASE + 1;
+    private static final int EVENT_SETUP_DATA_CALL = EVENT_BASE + 2;
+    private static final int EVENT_DEACTIVATE_DATA_CALL = EVENT_BASE + 3;
+    private static final int EVENT_DATA_CALL_LIST_REQUEST = EVENT_BASE + 4;
+    private static final int EVENT_FORCE_CLOSE_TUNNEL = EVENT_BASE + 5;
+    private static final int EVENT_ADD_DATA_SERVICE_PROVIDER = EVENT_BASE + 6;
+    private static final int EVENT_REMOVE_DATA_SERVICE_PROVIDER = EVENT_BASE + 7;
+    private static final int EVENT_TUNNEL_OPENED_METRICS = EVENT_BASE + 8;
+    private static final int EVENT_TUNNEL_CLOSED_METRICS = EVENT_BASE + 9;
 
     @VisibleForTesting
     enum Transport {
         UNSPECIFIED_NETWORK,
         MOBILE,
-        WIFI;
+        WIFI
     }
 
     private static Transport sDefaultDataTransport = Transport.UNSPECIFIED_NETWORK;
 
-    enum LinkProtocolType {
-        UNKNOWN,
-        IPV4,
-        IPV6,
-        IPV4V6;
-    }
-
-    private static LinkProtocolType sLinkProtocolType = LinkProtocolType.UNKNOWN;
-
     // TODO: see if network monitor callback impl can be shared between dataservice and
     // networkservice
+    // This callback runs in the same thread as IwlanDataServiceHandler
     static class IwlanNetworkMonitorCallback extends ConnectivityManager.NetworkCallback {
 
         /** Called when the framework connects and has declared a new network ready for use. */
         @Override
-        public void onAvailable(Network network) {
+        public void onAvailable(@NonNull Network network) {
             Log.d(TAG, "onAvailable: " + network);
         }
 
@@ -119,7 +144,7 @@
          * is suddenly disconnected.
          */
         @Override
-        public void onLosing(Network network, int maxMsToLive) {
+        public void onLosing(@NonNull Network network, int maxMsToLive) {
             Log.d(TAG, "onLosing: maxMsToLive: " + maxMsToLive + " network: " + network);
         }
 
@@ -128,41 +153,53 @@
          * callback.
          */
         @Override
-        public void onLost(Network network) {
+        public void onLost(@NonNull Network network) {
             Log.d(TAG, "onLost: " + network);
+            IwlanDataService.setConnectedDataSub(INVALID_SUB_ID);
             IwlanDataService.setNetworkConnected(false, network, Transport.UNSPECIFIED_NETWORK);
         }
 
         /** Called when the network corresponding to this request changes {@link LinkProperties}. */
         @Override
-        public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) {
+        public void onLinkPropertiesChanged(
+                @NonNull Network network, @NonNull LinkProperties linkProperties) {
             Log.d(TAG, "onLinkPropertiesChanged: " + linkProperties);
-            if (isLinkProtocolTypeChanged(linkProperties)) {
-                for (IwlanDataServiceProvider dp : sIwlanDataServiceProviderList) {
+
+            if (!network.equals(sNetwork)) {
+                Log.d(TAG, "Ignore LinkProperties changes for unused Network.");
+                return;
+            }
+
+            if (!linkProperties.equals(sLinkProperties)) {
+                for (IwlanDataServiceProvider dp : sIwlanDataServiceProviders.values()) {
                     dp.dnsPrefetchCheck();
+                    sLinkProperties = linkProperties;
+                    dp.updateNetwork(network, linkProperties);
                 }
             }
         }
 
         /** Called when access to the specified network is blocked or unblocked. */
         @Override
-        public void onBlockedStatusChanged(Network network, boolean blocked) {
+        public void onBlockedStatusChanged(@NonNull Network network, boolean blocked) {
             // TODO: check if we need to handle this
             Log.d(TAG, "onBlockedStatusChanged: " + network + " BLOCKED:" + blocked);
         }
 
         @Override
         public void onCapabilitiesChanged(
-                Network network, NetworkCapabilities networkCapabilities) {
+                @NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) {
             // onCapabilitiesChanged is guaranteed to be called immediately after onAvailable per
             // API
             Log.d(TAG, "onCapabilitiesChanged: " + network + " " + networkCapabilities);
             if (networkCapabilities != null) {
                 if (networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
                     Log.d(TAG, "Network " + network + " connected using transport MOBILE");
+                    IwlanDataService.setConnectedDataSub(getConnectedDataSub(networkCapabilities));
                     IwlanDataService.setNetworkConnected(true, network, Transport.MOBILE);
                 } else if (networkCapabilities.hasTransport(TRANSPORT_WIFI)) {
                     Log.d(TAG, "Network " + network + " connected using transport WIFI");
+                    IwlanDataService.setConnectedDataSub(INVALID_SUB_ID);
                     IwlanDataService.setNetworkConnected(true, network, Transport.WIFI);
                 } else {
                     Log.w(TAG, "Network does not have cellular or wifi capability");
@@ -180,20 +217,20 @@
         private final String SUB_TAG;
         private final IwlanDataService mIwlanDataService;
         private final IwlanTunnelCallback mIwlanTunnelCallback;
-        private HandlerThread mHandlerThread;
-        @VisibleForTesting Handler mHandler;
+        private final IwlanTunnelMetricsImpl mIwlanTunnelMetrics;
         private boolean mWfcEnabled = false;
         private boolean mCarrierConfigReady = false;
-        private EpdgSelector mEpdgSelector;
-        private IwlanDataTunnelStats mTunnelStats;
+        private final EpdgSelector mEpdgSelector;
+        private final IwlanDataTunnelStats mTunnelStats;
         private CellInfo mCellInfo = null;
+        private int mCallState = TelephonyManager.CALL_STATE_IDLE;
+        private long mProcessingStartTime = 0;
 
         // apn to TunnelState
-        // Lock this at public entry and exit points if:
-        // 1) the function changes mTunnelStateForApn
-        // 2) Makes decisions based on contents of mTunnelStateForApn
-        @GuardedBy("mTunnelStateForApn")
-        private Map<String, TunnelState> mTunnelStateForApn = new ConcurrentHashMap<>();
+        // Access should be serialized inside IwlanDataServiceHandler
+        private final Map<String, TunnelState> mTunnelStateForApn = new ConcurrentHashMap<>();
+        private final Map<String, MetricsAtom> mMetricsAtomForApn = new ConcurrentHashMap<>();
+        private Calendar mCalendar;
 
         // Holds the state of a tunnel (for an APN)
         @VisibleForTesting
@@ -202,8 +239,8 @@
             // this should be ideally be based on path MTU discovery. 1280 is the minimum packet
             // size ipv6 routers have to handle so setting it to 1280 is the safest approach.
             // ideally it should be 1280 - tunnelling overhead ?
-            private static final int LINK_MTU =
-                    1280; // TODO: need to substract tunnelling overhead?
+            private static final int LINK_MTU = 1280; // TODO: need to subtract tunnelling overhead?
+            private static final int LINK_MTU_CST = 1200; // Reserve 80 bytes for VCN.
             static final int TUNNEL_DOWN = 1;
             static final int TUNNEL_IN_BRINGUP = 2;
             static final int TUNNEL_UP = 3;
@@ -216,6 +253,7 @@
             private boolean mIsHandover;
             private Date mBringUpStateTime = null;
             private Date mUpStateTime = null;
+            private boolean mIsImsOrEmergency;
 
             public int getPduSessionId() {
                 return mPduSessionId;
@@ -230,7 +268,11 @@
             }
 
             public int getLinkMtu() {
-                return LINK_MTU; // TODO: need to substract tunnelling overhead
+                if ((sDefaultDataTransport == Transport.MOBILE) && sNetworkConnected) {
+                    return LINK_MTU_CST;
+                } else {
+                    return LINK_MTU; // TODO: need to subtract tunnelling overhead
+                }
             }
 
             public void setProtocolType(int protocolType) {
@@ -264,14 +306,16 @@
                 return mState;
             }
 
-            /** @param state (TunnelState.TUNNEL_DOWN|TUNNEL_UP|TUNNEL_DOWN) */
+            /**
+             * @param state (TunnelState.TUNNEL_DOWN|TUNNEL_UP|TUNNEL_DOWN)
+             */
             public void setState(int state) {
                 mState = state;
                 if (mState == TunnelState.TUNNEL_IN_BRINGUP) {
-                    mBringUpStateTime = Calendar.getInstance().getTime();
+                    mBringUpStateTime = mCalendar.getTime();
                 }
                 if (mState == TunnelState.TUNNEL_UP) {
-                    mUpStateTime = Calendar.getInstance().getTime();
+                    mUpStateTime = mCalendar.getTime();
                 }
             }
 
@@ -291,6 +335,18 @@
                 return mUpStateTime;
             }
 
+            public Date getCurrentTime() {
+                return mCalendar.getTime();
+            }
+
+            public boolean getIsImsOrEmergency() {
+                return mIsImsOrEmergency;
+            }
+
+            public void setIsImsOrEmergency(boolean isImsOrEmergency) {
+                mIsImsOrEmergency = isImsOrEmergency;
+            }
+
             @Override
             public String toString() {
                 StringBuilder sb = new StringBuilder();
@@ -312,19 +368,23 @@
                         tunnelState = "IN FORCE CLEAN WAS IN BRINGUP";
                         break;
                 }
-                sb.append("\tCurrent State of this tunnel: " + mState + " " + tunnelState);
-                sb.append("\n\tTunnel state is in Handover: " + mIsHandover);
+                sb.append("\tCurrent State of this tunnel: ")
+                        .append(mState)
+                        .append(" ")
+                        .append(tunnelState);
+                sb.append("\n\tTunnel state is in Handover: ").append(mIsHandover);
                 if (mBringUpStateTime != null) {
-                    sb.append("\n\tTunnel bring up initiated at: " + mBringUpStateTime);
+                    sb.append("\n\tTunnel bring up initiated at: ").append(mBringUpStateTime);
                 } else {
                     sb.append("\n\tPotential leak. Null mBringUpStateTime");
                 }
                 if (mUpStateTime != null) {
-                    sb.append("\n\tTunnel is up at: " + mUpStateTime);
+                    sb.append("\n\tTunnel is up at: ").append(mUpStateTime);
                 }
                 if (mUpStateTime != null && mBringUpStateTime != null) {
                     long tunnelUpTime = mUpStateTime.getTime() - mBringUpStateTime.getTime();
-                    sb.append("\n\tTime taken for the tunnel to come up in ms: " + tunnelUpTime);
+                    sb.append("\n\tTime taken for the tunnel to come up in ms: ")
+                            .append(tunnelUpTime);
                 }
                 return sb.toString();
             }
@@ -333,10 +393,10 @@
         @VisibleForTesting
         class IwlanTunnelCallback implements EpdgTunnelManager.TunnelCallback {
 
-            DataServiceProvider mDataServiceProvider;
+            IwlanDataServiceProvider mIwlanDataServiceProvider;
 
-            public IwlanTunnelCallback(DataServiceProvider dsp) {
-                mDataServiceProvider = dsp;
+            public IwlanTunnelCallback(IwlanDataServiceProvider dsp) {
+                mIwlanDataServiceProvider = dsp;
             }
 
             // TODO: full implementation
@@ -345,154 +405,30 @@
                 Log.d(
                         SUB_TAG,
                         "Tunnel opened!. APN: " + apnName + "linkproperties: " + linkProperties);
-                synchronized (mTunnelStateForApn) {
-                    TunnelState tunnelState = mTunnelStateForApn.get(apnName);
-                    // tunnelstate should not be null, design violation.
-                    // if its null, we should crash and debug.
-                    tunnelState.setTunnelLinkProperties(linkProperties);
-                    tunnelState.setState(TunnelState.TUNNEL_UP);
-                    mTunnelStats.reportTunnelSetupSuccess(apnName, tunnelState);
-
-                    deliverCallback(
-                            CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
-                            DataServiceCallback.RESULT_SUCCESS,
-                            tunnelState.getDataServiceCallback(),
-                            apnTunnelStateToDataCallResponse(apnName));
-                }
+                getIwlanDataServiceHandler()
+                        .sendMessage(
+                                getIwlanDataServiceHandler()
+                                        .obtainMessage(
+                                                EVENT_TUNNEL_OPENED,
+                                                new TunnelOpenedData(
+                                                        apnName,
+                                                        linkProperties,
+                                                        mIwlanDataServiceProvider)));
             }
 
             public void onClosed(String apnName, IwlanError error) {
                 Log.d(SUB_TAG, "Tunnel closed!. APN: " + apnName + " Error: " + error);
                 // this is called, when a tunnel that is up, is closed.
                 // the expectation is error==NO_ERROR for user initiated/normal close.
-                synchronized (mTunnelStateForApn) {
-                    TunnelState tunnelState = mTunnelStateForApn.get(apnName);
-                    mTunnelStats.reportTunnelDown(apnName, tunnelState);
-                    mTunnelStateForApn.remove(apnName);
-
-                    if (tunnelState.getState() == TunnelState.TUNNEL_IN_BRINGUP
-                            || tunnelState.getState()
-                                    == TunnelState.TUNNEL_IN_FORCE_CLEAN_WAS_IN_BRINGUP) {
-                        DataCallResponse.Builder respBuilder = new DataCallResponse.Builder();
-                        respBuilder
-                                .setId(apnName.hashCode())
-                                .setProtocolType(tunnelState.getProtocolType());
-
-                        if (tunnelState.getIsHandover()) {
-                            respBuilder.setHandoverFailureMode(
-                                    DataCallResponse
-                                            .HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_HANDOVER);
-                        } else {
-                            respBuilder.setHandoverFailureMode(
-                                    DataCallResponse
-                                            .HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_SETUP_NORMAL);
-                        }
-
-                        if (tunnelState.getState() == TunnelState.TUNNEL_IN_BRINGUP) {
-                            respBuilder.setCause(
-                                    ErrorPolicyManager.getInstance(mContext, getSlotIndex())
-                                            .getDataFailCause(apnName));
-                            respBuilder.setRetryDurationMillis(
-                                    ErrorPolicyManager.getInstance(mContext, getSlotIndex())
-                                            .getCurrentRetryTimeMs(apnName));
-                        } else if (tunnelState.getState()
-                                == TunnelState.TUNNEL_IN_FORCE_CLEAN_WAS_IN_BRINGUP) {
-                            respBuilder.setCause(DataFailCause.IWLAN_NETWORK_FAILURE);
-                            respBuilder.setRetryDurationMillis(5000);
-                        }
-
-                        deliverCallback(
-                                CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
-                                DataServiceCallback.RESULT_SUCCESS,
-                                tunnelState.getDataServiceCallback(),
-                                respBuilder.build());
-                        return;
-                    }
-
-                    // iwlan service triggered teardown
-                    if (tunnelState.getState() == TunnelState.TUNNEL_IN_BRINGDOWN) {
-
-                        // IO exception happens when IKE library fails to retransmit requests.
-                        // This can happen for multiple reasons:
-                        // 1. Network disconnection due to wifi off.
-                        // 2. Epdg server does not respond.
-                        // 3. Socket send/receive fails.
-                        // Ignore this during tunnel bring down.
-                        if (error.getErrorType() != IwlanError.NO_ERROR
-                                && error.getErrorType() != IwlanError.IKE_INTERNAL_IO_EXCEPTION) {
-                            Log.e(SUB_TAG, "Unexpected error during tunnel bring down: " + error);
-                        }
-
-                        deliverCallback(
-                                CALLBACK_TYPE_DEACTIVATE_DATACALL_COMPLETE,
-                                DataServiceCallback.RESULT_SUCCESS,
-                                tunnelState.getDataServiceCallback(),
-                                null);
-
-                        return;
-                    }
-
-                    // just update list of data calls. No way to send error up
-                    notifyDataCallListChanged(getCallList());
-                }
-            }
-        }
-
-        private final class DSPHandler extends Handler {
-            private final String TAG =
-                    IwlanDataService.class.getSimpleName()
-                            + DSPHandler.class.getSimpleName()
-                            + getSlotIndex();
-
-            @Override
-            public void handleMessage(Message msg) {
-                Log.d(TAG, "msg.what = " + msg.what);
-                switch (msg.what) {
-                    case IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT:
-                        Log.d(TAG, "On CARRIER_CONFIG_CHANGED_EVENT");
-                        mCarrierConfigReady = true;
-                        dnsPrefetchCheck();
-                        break;
-                    case IwlanEventListener.CARRIER_CONFIG_UNKNOWN_CARRIER_EVENT:
-                        Log.d(TAG, "On CARRIER_CONFIG_UNKNOWN_CARRIER_EVENT");
-                        mCarrierConfigReady = false;
-                        break;
-                    case IwlanEventListener.WIFI_CALLING_ENABLE_EVENT:
-                        Log.d(TAG, "On WIFI_CALLING_ENABLE_EVENT");
-                        mWfcEnabled = true;
-                        dnsPrefetchCheck();
-                        break;
-                    case IwlanEventListener.WIFI_CALLING_DISABLE_EVENT:
-                        Log.d(TAG, "On WIFI_CALLING_DISABLE_EVENT");
-                        mWfcEnabled = false;
-                        break;
-                    case IwlanEventListener.CELLINFO_CHANGED_EVENT:
-                        Log.d(TAG, "On CELLINFO_CHANGED_EVENT");
-                        List<CellInfo> cellInfolist = (List<CellInfo>) msg.obj;
-
-                        if (cellInfolist != null && isRegisteredCellInfoChanged(cellInfolist)) {
-                            int[] addrResolutionMethods =
-                                    IwlanHelper.getConfig(
-                                            CarrierConfigManager.Iwlan
-                                                    .KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY,
-                                            mContext,
-                                            getSlotIndex());
-                            for (int addrResolutionMethod : addrResolutionMethods) {
-                                if (addrResolutionMethod
-                                        == CarrierConfigManager.Iwlan.EPDG_ADDRESS_CELLULAR_LOC) {
-                                    dnsPrefetchCheck();
-                                }
-                            }
-                        }
-                        break;
-                    default:
-                        Log.d(TAG, "Unknown message received!");
-                        break;
-                }
-            }
-
-            DSPHandler(Looper looper) {
-                super(looper);
+                getIwlanDataServiceHandler()
+                        .sendMessage(
+                                getIwlanDataServiceHandler()
+                                        .obtainMessage(
+                                                EVENT_TUNNEL_CLOSED,
+                                                new TunnelClosedData(
+                                                        apnName,
+                                                        error,
+                                                        mIwlanDataServiceProvider)));
             }
         }
 
@@ -522,10 +458,9 @@
 
             private long statCount;
             private final long COUNT_MAX = 1000;
-            private final int APN_COUNT_MAX = 10;
 
             public IwlanDataTunnelStats() {
-                mStartTime = Calendar.getInstance().getTime();
+                mStartTime = mCalendar.getTime();
                 statCount = 0L;
             }
 
@@ -566,7 +501,7 @@
                 }
 
                 // Unsolicited tunnel down as tunnel has to be in BRINGDOWN if
-                // there is a deactivate call associated with this.
+                // there is a deactivateDataCall() associated with this.
                 if (tunnelState.getState() == TunnelState.TUNNEL_UP) {
                     if (!mUnsolTunnelDownCounts.containsKey(apn)) {
                         mUnsolTunnelDownCounts.put(apn, 0L);
@@ -574,7 +509,7 @@
                     long count = mUnsolTunnelDownCounts.get(apn);
                     mUnsolTunnelDownCounts.put(apn, ++count);
                 }
-                Date currentTime = Calendar.getInstance().getTime();
+                Date currentTime = tunnelState.getCurrentTime();
                 Date upTime = tunnelState.getUpStateTime();
                 if (upTime != null) {
                     if (!mTunnelUpStats.containsKey(apn)) {
@@ -587,48 +522,46 @@
             }
 
             boolean maxApnReached() {
-                if (mTunnelSetupSuccessStats.size() >= APN_COUNT_MAX
+                int APN_COUNT_MAX = 10;
+                return mTunnelSetupSuccessStats.size() >= APN_COUNT_MAX
                         || mTunnelSetupFailureCounts.size() >= APN_COUNT_MAX
                         || mUnsolTunnelDownCounts.size() >= APN_COUNT_MAX
-                        || mTunnelUpStats.size() >= APN_COUNT_MAX) {
-                    return true;
-                }
-                return false;
+                        || mTunnelUpStats.size() >= APN_COUNT_MAX;
             }
 
             @Override
             public String toString() {
                 StringBuilder sb = new StringBuilder();
                 sb.append("IwlanDataTunnelStats:");
-                sb.append("\n\tmStartTime: " + mStartTime);
+                sb.append("\n\tmStartTime: ").append(mStartTime);
                 sb.append("\n\ttunnelSetupSuccessStats:");
                 for (Map.Entry<String, LongSummaryStatistics> entry :
                         mTunnelSetupSuccessStats.entrySet()) {
-                    sb.append("\n\t  Apn: " + entry.getKey());
-                    sb.append("\n\t  " + entry.getValue());
+                    sb.append("\n\t  Apn: ").append(entry.getKey());
+                    sb.append("\n\t  ").append(entry.getValue());
                 }
                 sb.append("\n\ttunnelUpStats:");
                 for (Map.Entry<String, LongSummaryStatistics> entry : mTunnelUpStats.entrySet()) {
-                    sb.append("\n\t  Apn: " + entry.getKey());
-                    sb.append("\n\t  " + entry.getValue());
+                    sb.append("\n\t  Apn: ").append(entry.getKey());
+                    sb.append("\n\t  ").append(entry.getValue());
                 }
 
                 sb.append("\n\ttunnelSetupFailureCounts: ");
                 for (Map.Entry<String, Long> entry : mTunnelSetupFailureCounts.entrySet()) {
-                    sb.append("\n\t  Apn: " + entry.getKey());
-                    sb.append("\n\t  counts: " + entry.getValue());
+                    sb.append("\n\t  Apn: ").append(entry.getKey());
+                    sb.append("\n\t  counts: ").append(entry.getValue());
                 }
                 sb.append("\n\tunsolTunnelDownCounts: ");
                 for (Map.Entry<String, Long> entry : mTunnelSetupFailureCounts.entrySet()) {
-                    sb.append("\n\t  Apn: " + entry.getKey());
-                    sb.append("\n\t  counts: " + entry.getValue());
+                    sb.append("\n\t  Apn: ").append(entry.getKey());
+                    sb.append("\n\t  counts: ").append(entry.getValue());
                 }
-                sb.append("\n\tendTime: " + Calendar.getInstance().getTime());
+                sb.append("\n\tendTime: ").append(mCalendar.getTime());
                 return sb.toString();
             }
 
             private void reset() {
-                mStartTime = Calendar.getInstance().getTime();
+                mStartTime = mCalendar.getTime();
                 mTunnelSetupSuccessStats = new HashMap<String, LongSummaryStatistics>();
                 mTunnelUpStats = new HashMap<String, LongSummaryStatistics>();
                 mTunnelSetupFailureCounts = new HashMap<String, Long>();
@@ -637,12 +570,6 @@
             }
         }
 
-        Looper getLooper() {
-            mHandlerThread = new HandlerThread("DSPHandlerThread");
-            mHandlerThread.start();
-            return mHandlerThread.getLooper();
-        }
-
         /**
          * Constructor
          *
@@ -657,26 +584,25 @@
             // get reference to resolver
             mIwlanDataService = iwlanDataService;
             mIwlanTunnelCallback = new IwlanTunnelCallback(this);
+            mIwlanTunnelMetrics = new IwlanTunnelMetricsImpl(this, getIwlanDataServiceHandler());
             mEpdgSelector = EpdgSelector.getSelectorInstance(mContext, slotIndex);
+            mCalendar = Calendar.getInstance();
             mTunnelStats = new IwlanDataTunnelStats();
 
             // Register IwlanEventListener
-            initHandler();
             List<Integer> events = new ArrayList<Integer>();
             events.add(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT);
             events.add(IwlanEventListener.CARRIER_CONFIG_UNKNOWN_CARRIER_EVENT);
             events.add(IwlanEventListener.WIFI_CALLING_ENABLE_EVENT);
             events.add(IwlanEventListener.WIFI_CALLING_DISABLE_EVENT);
+            events.add(IwlanEventListener.CROSS_SIM_CALLING_ENABLE_EVENT);
             events.add(IwlanEventListener.CELLINFO_CHANGED_EVENT);
-            IwlanEventListener.getInstance(mContext, slotIndex).addEventListener(events, mHandler);
+            events.add(IwlanEventListener.CALL_STATE_CHANGED_EVENT);
+            IwlanEventListener.getInstance(mContext, slotIndex)
+                    .addEventListener(events, getIwlanDataServiceHandler());
         }
 
-        void initHandler() {
-            mHandler = new DSPHandler(getLooper());
-        }
-
-        @VisibleForTesting
-        EpdgTunnelManager getTunnelManager() {
+        private EpdgTunnelManager getTunnelManager() {
             return EpdgTunnelManager.getInstance(mContext, getSlotIndex());
         }
 
@@ -693,23 +619,30 @@
                     .setProtocolType(tunnelState.getProtocolType())
                     .setCause(DataFailCause.NONE);
 
-            if (tunnelState.getState() != TunnelState.TUNNEL_UP) {
-                // no need to fill additional params
-                return responseBuilder.setLinkStatus(DataCallResponse.LINK_STATUS_UNKNOWN).build();
+            responseBuilder.setLinkStatus(DataCallResponse.LINK_STATUS_INACTIVE);
+            int state = tunnelState.getState();
+
+            if (state == TunnelState.TUNNEL_UP) {
+                responseBuilder.setLinkStatus(DataCallResponse.LINK_STATUS_ACTIVE);
+            }
+
+            TunnelLinkProperties tunnelLinkProperties = tunnelState.getTunnelLinkProperties();
+            if (tunnelLinkProperties == null) {
+                Log.d(TAG, "PDN with empty linkproperties. TunnelState : " + state);
+                return responseBuilder.build();
             }
 
             // fill wildcard address for gatewayList (used by DataConnection to add routes)
             List<InetAddress> gatewayList = new ArrayList<>();
-            List<LinkAddress> linkAddrList =
-                    tunnelState.getTunnelLinkProperties().internalAddresses();
-            if (linkAddrList.stream().anyMatch(t -> t.isIpv4())) {
+            List<LinkAddress> linkAddrList = tunnelLinkProperties.internalAddresses();
+            if (linkAddrList.stream().anyMatch(LinkAddress::isIpv4)) {
                 try {
                     gatewayList.add(Inet4Address.getByName("0.0.0.0"));
                 } catch (UnknownHostException e) {
                     // should never happen for static string 0.0.0.0
                 }
             }
-            if (linkAddrList.stream().anyMatch(t -> t.isIpv6())) {
+            if (linkAddrList.stream().anyMatch(LinkAddress::isIpv6)) {
                 try {
                     gatewayList.add(Inet6Address.getByName("::"));
                 } catch (UnknownHostException e) {
@@ -717,18 +650,16 @@
                 }
             }
 
-            if (tunnelState.getTunnelLinkProperties().sliceInfo().isPresent()) {
-                responseBuilder.setSliceInfo(
-                        tunnelState.getTunnelLinkProperties().sliceInfo().get());
+            if (tunnelLinkProperties.sliceInfo().isPresent()) {
+                responseBuilder.setSliceInfo(tunnelLinkProperties.sliceInfo().get());
             }
 
             return responseBuilder
                     .setAddresses(linkAddrList)
-                    .setDnsAddresses(tunnelState.getTunnelLinkProperties().dnsAddresses())
-                    .setPcscfAddresses(tunnelState.getTunnelLinkProperties().pcscfAddresses())
-                    .setInterfaceName(tunnelState.getTunnelLinkProperties().ifaceName())
+                    .setDnsAddresses(tunnelLinkProperties.dnsAddresses())
+                    .setPcscfAddresses(tunnelLinkProperties.pcscfAddresses())
+                    .setInterfaceName(tunnelLinkProperties.ifaceName())
                     .setGatewayAddresses(gatewayList)
-                    .setLinkStatus(DataCallResponse.LINK_STATUS_ACTIVE)
                     .setMtu(tunnelState.getLinkMtu())
                     .setMtuV4(tunnelState.getLinkMtu())
                     .setMtuV6(tunnelState.getLinkMtu())
@@ -827,6 +758,7 @@
                 boolean matchAllRuleAllowed,
                 @NonNull DataServiceCallback callback) {
 
+            mProcessingStartTime = System.currentTimeMillis();
             Log.d(
                     SUB_TAG,
                     "Setup data call with network: "
@@ -844,128 +776,50 @@
                             + ", pduSessionId: "
                             + pduSessionId);
 
-            // Framework will never call bringup on the same APN back 2 back.
-            // but add a safety check
-            if ((accessNetworkType != AccessNetworkType.IWLAN)
-                    || (dataProfile == null)
-                    || (linkProperties == null && reason == DataService.REQUEST_REASON_HANDOVER)) {
+            SetupDataCallData setupDataCallData =
+                    new SetupDataCallData(
+                            accessNetworkType,
+                            dataProfile,
+                            isRoaming,
+                            allowRoaming,
+                            reason,
+                            linkProperties,
+                            pduSessionId,
+                            sliceInfo,
+                            trafficDescriptor,
+                            matchAllRuleAllowed,
+                            callback,
+                            this);
 
-                deliverCallback(
-                        CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
-                        DataServiceCallback.RESULT_ERROR_INVALID_ARG,
-                        callback,
-                        null);
-                return;
+            int networkTransport = -1;
+            if (sDefaultDataTransport == Transport.MOBILE) {
+                networkTransport = TRANSPORT_CELLULAR;
+            } else if (sDefaultDataTransport == Transport.WIFI) {
+                networkTransport = TRANSPORT_WIFI;
             }
 
-            synchronized (mTunnelStateForApn) {
-                boolean isDDS = IwlanHelper.isDefaultDataSlot(mContext, getSlotIndex());
-                boolean isCSTEnabled =
-                        IwlanHelper.isCrossSimCallingEnabled(mContext, getSlotIndex());
-                boolean networkConnected = isNetworkConnected(isDDS, isCSTEnabled);
-                Log.d(
-                        SUB_TAG,
-                        "isDds: "
-                                + isDDS
-                                + ", isCstEnabled: "
-                                + isCSTEnabled
-                                + ", transport: "
-                                + sDefaultDataTransport);
-
-                if (networkConnected == false) {
-                    deliverCallback(
-                            CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
-                            5 /* DataServiceCallback.RESULT_ERROR_TEMPORARILY_UNAVAILABLE */,
-                            callback,
-                            null);
-                    return;
-                }
-
-                TunnelState tunnelState = mTunnelStateForApn.get(dataProfile.getApn());
-
-                // Return the existing PDN if the pduSessionId is the same and the tunnel state is
-                // TUNNEL_UP.
-                if (tunnelState != null) {
-                    if (tunnelState.getPduSessionId() == pduSessionId
-                            && tunnelState.getState() == TunnelState.TUNNEL_UP) {
-                        Log.w(
-                                SUB_TAG,
-                                "The tunnel for " + dataProfile.getApn() + " already exists.");
-                        deliverCallback(
-                                CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
-                                DataServiceCallback.RESULT_SUCCESS,
-                                callback,
-                                apnTunnelStateToDataCallResponse(dataProfile.getApn()));
-                        return;
-                    } else {
-                        Log.e(
-                                SUB_TAG,
-                                "Force close the existing PDN. pduSessionId = "
-                                        + tunnelState.getPduSessionId()
-                                        + " Tunnel State = "
-                                        + tunnelState.getState());
-                        getTunnelManager().closeTunnel(dataProfile.getApn(), true /* forceClose */);
-                        deliverCallback(
-                                CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
-                                5 /* DataServiceCallback.RESULT_ERROR_TEMPORARILY_UNAVAILABLE */,
-                                callback,
-                                null);
-                        return;
-                    }
-                }
-
-                TunnelSetupRequest.Builder tunnelReqBuilder =
-                        TunnelSetupRequest.builder()
-                                .setApnName(dataProfile.getApn())
-                                .setNetwork(sNetwork)
-                                .setIsRoaming(isRoaming)
-                                .setPduSessionId(pduSessionId)
-                                .setApnIpProtocol(
-                                        isRoaming
-                                                ? dataProfile.getRoamingProtocolType()
-                                                : dataProfile.getProtocolType());
-
-                if (reason == DataService.REQUEST_REASON_HANDOVER) {
-                    // for now assume that, at max,  only one address of eachtype (v4/v6).
-                    // TODO: Check if multiple ips can be sent in ike tunnel setup
-                    for (LinkAddress lAddr : linkProperties.getLinkAddresses()) {
-                        if (lAddr.isIpv4()) {
-                            tunnelReqBuilder.setSrcIpv4Address(lAddr.getAddress());
-                        } else if (lAddr.isIpv6()) {
-                            tunnelReqBuilder.setSrcIpv6Address(lAddr.getAddress());
-                            tunnelReqBuilder.setSrcIpv6AddressPrefixLength(lAddr.getPrefixLength());
-                        }
-                    }
-                }
-
-                int apnTypeBitmask = dataProfile.getSupportedApnTypesBitmask();
-                boolean isIMS = (apnTypeBitmask & ApnSetting.TYPE_IMS) == ApnSetting.TYPE_IMS;
-                boolean isEmergency =
-                        (apnTypeBitmask & ApnSetting.TYPE_EMERGENCY) == ApnSetting.TYPE_EMERGENCY;
-                tunnelReqBuilder.setRequestPcscf(isIMS || isEmergency);
-                tunnelReqBuilder.setIsEmergency(isEmergency);
-
-                setTunnelState(
-                        dataProfile,
-                        callback,
-                        TunnelState.TUNNEL_IN_BRINGUP,
-                        null,
+            if (dataProfile != null) {
+                this.setMetricsAtom(
+                        // ApnName
+                        dataProfile.getApnSetting().getApnName(),
+                        // ApnType
+                        dataProfile.getApnSetting().getApnTypeBitmask(),
+                        // IsHandover
                         (reason == DataService.REQUEST_REASON_HANDOVER),
-                        pduSessionId);
-
-                boolean result =
-                        getTunnelManager()
-                                .bringUpTunnel(tunnelReqBuilder.build(), getIwlanTunnelCallback());
-                Log.d(SUB_TAG, "bringup Tunnel with result:" + result);
-                if (!result) {
-                    deliverCallback(
-                            CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
-                            DataServiceCallback.RESULT_ERROR_INVALID_ARG,
-                            callback,
-                            null);
-                    return;
-                }
+                        // Source Rat
+                        getCurrentCellularRat(),
+                        // IsRoaming
+                        isRoaming,
+                        // Is Network Connected
+                        sNetworkConnected,
+                        // Transport Type
+                        networkTransport);
             }
+
+            getIwlanDataServiceHandler()
+                    .sendMessage(
+                            getIwlanDataServiceHandler()
+                                    .obtainMessage(EVENT_SETUP_DATA_CALL, setupDataCallData));
         }
 
         /**
@@ -992,49 +846,38 @@
                             + "callback: "
                             + callback);
 
-            synchronized (mTunnelStateForApn) {
-                for (String apnName : mTunnelStateForApn.keySet()) {
-                    if (apnName.hashCode() == cid) {
-                        /*
-                        No need to check state since dataconnection in framework serializes
-                        setup and deactivate calls using callId/cid.
-                        */
-                        mTunnelStateForApn.get(apnName).setState(TunnelState.TUNNEL_IN_BRINGDOWN);
-                        mTunnelStateForApn.get(apnName).setDataServiceCallback(callback);
-                        boolean isConnected =
-                                isNetworkConnected(
-                                        IwlanHelper.isDefaultDataSlot(mContext, getSlotIndex()),
-                                        IwlanHelper.isCrossSimCallingEnabled(
-                                                mContext, getSlotIndex()));
-                        getTunnelManager().closeTunnel(apnName, !isConnected);
-                        return;
-                    }
-                }
+            DeactivateDataCallData deactivateDataCallData =
+                    new DeactivateDataCallData(cid, reason, callback, this);
 
-                deliverCallback(
-                        CALLBACK_TYPE_DEACTIVATE_DATACALL_COMPLETE,
-                        DataServiceCallback.RESULT_ERROR_INVALID_ARG,
-                        callback,
-                        null);
-            }
+            getIwlanDataServiceHandler()
+                    .sendMessage(
+                            getIwlanDataServiceHandler()
+                                    .obtainMessage(
+                                            EVENT_DEACTIVATE_DATA_CALL, deactivateDataCallData));
         }
 
         public void forceCloseTunnelsInDeactivatingState() {
-            synchronized (mTunnelStateForApn) {
-                for (Map.Entry<String, TunnelState> entry : mTunnelStateForApn.entrySet()) {
-                    TunnelState tunnelState = entry.getValue();
-                    if (tunnelState.getState() == TunnelState.TUNNEL_IN_BRINGDOWN) {
-                        getTunnelManager().closeTunnel(entry.getKey(), true);
-                    }
+            for (Map.Entry<String, TunnelState> entry : mTunnelStateForApn.entrySet()) {
+                TunnelState tunnelState = entry.getValue();
+                if (tunnelState.getState() == TunnelState.TUNNEL_IN_BRINGDOWN) {
+                    getTunnelManager()
+                            .closeTunnel(
+                                    entry.getKey(),
+                                    true /* forceClose */,
+                                    getIwlanTunnelCallback(),
+                                    getIwlanTunnelMetrics());
                 }
             }
         }
 
         void forceCloseTunnels() {
-            synchronized (mTunnelStateForApn) {
-                for (Map.Entry<String, TunnelState> entry : mTunnelStateForApn.entrySet()) {
-                    getTunnelManager().closeTunnel(entry.getKey(), true);
-                }
+            for (Map.Entry<String, TunnelState> entry : mTunnelStateForApn.entrySet()) {
+                getTunnelManager()
+                        .closeTunnel(
+                                entry.getKey(),
+                                true /* forceClose */,
+                                getIwlanTunnelCallback(),
+                                getIwlanTunnelMetrics());
             }
         }
 
@@ -1045,11 +888,13 @@
          */
         @Override
         public void requestDataCallList(DataServiceCallback callback) {
-            deliverCallback(
-                    CALLBACK_TYPE_GET_DATACALL_LIST_COMPLETE,
-                    DataServiceCallback.RESULT_SUCCESS,
-                    callback,
-                    null);
+            getIwlanDataServiceHandler()
+                    .sendMessage(
+                            getIwlanDataServiceHandler()
+                                    .obtainMessage(
+                                            EVENT_DATA_CALL_LIST_REQUEST,
+                                            new DataCallRequestData(
+                                                    callback, IwlanDataServiceProvider.this)));
         }
 
         @VisibleForTesting
@@ -1059,14 +904,41 @@
                 int tunnelStatus,
                 TunnelLinkProperties linkProperties,
                 boolean isHandover,
-                int pduSessionId) {
+                int pduSessionId,
+                boolean isImsOrEmergency) {
             TunnelState tunnelState = new TunnelState(callback);
             tunnelState.setState(tunnelStatus);
-            tunnelState.setProtocolType(dataProfile.getProtocolType());
+            tunnelState.setProtocolType(dataProfile.getApnSetting().getProtocol());
             tunnelState.setTunnelLinkProperties(linkProperties);
             tunnelState.setIsHandover(isHandover);
             tunnelState.setPduSessionId(pduSessionId);
-            mTunnelStateForApn.put(dataProfile.getApn(), tunnelState);
+            tunnelState.setIsImsOrEmergency(isImsOrEmergency);
+            mTunnelStateForApn.put(dataProfile.getApnSetting().getApnName(), tunnelState);
+        }
+
+        @VisibleForTesting
+        void setMetricsAtom(
+                String apnName,
+                int apntype,
+                boolean isHandover,
+                int sourceRat,
+                boolean isRoaming,
+                boolean isNetworkConnected,
+                int transportType) {
+            MetricsAtom metricsAtom = new MetricsAtom();
+            metricsAtom.setApnType(apntype);
+            metricsAtom.setIsHandover(isHandover);
+            metricsAtom.setSourceRat(sourceRat);
+            metricsAtom.setIsCellularRoaming(isRoaming);
+            metricsAtom.setIsNetworkConnected(isNetworkConnected);
+            metricsAtom.setTransportType(transportType);
+            mMetricsAtomForApn.put(apnName, metricsAtom);
+        }
+
+        @VisibleForTesting
+        @Nullable
+        public MetricsAtom getMetricsAtomByApn(String apnName) {
+            return mMetricsAtomForApn.get(apnName);
         }
 
         @VisibleForTesting
@@ -1075,31 +947,40 @@
         }
 
         @VisibleForTesting
+        public IwlanTunnelMetricsImpl getIwlanTunnelMetrics() {
+            return mIwlanTunnelMetrics;
+        }
+
+        @VisibleForTesting
         IwlanDataTunnelStats getTunnelStats() {
             return mTunnelStats;
         }
 
-        private void updateNetwork(Network network) {
-            if (network != null) {
-                synchronized (mTunnelStateForApn) {
-                    for (Map.Entry<String, TunnelState> entry : mTunnelStateForApn.entrySet()) {
-                        TunnelState tunnelState = entry.getValue();
-                        if (tunnelState.getState() == TunnelState.TUNNEL_IN_BRINGUP) {
-                            // force close tunnels in bringup since IKE lib only supports
-                            // updating network for tunnels that are already up.
-                            // This may not result in actual closing of Ike Session since
-                            // epdg selection may not be complete yet.
-                            tunnelState.setState(TunnelState.TUNNEL_IN_FORCE_CLEAN_WAS_IN_BRINGUP);
-                            getTunnelManager().closeTunnel(entry.getKey(), true);
-                        } else {
-                            if (mIwlanDataService.isNetworkConnected(
-                                    IwlanHelper.isDefaultDataSlot(mContext, getSlotIndex()),
-                                    IwlanHelper.isCrossSimCallingEnabled(
-                                            mContext, getSlotIndex()))) {
-                                getTunnelManager().updateNetwork(network, entry.getKey());
-                            }
-                        }
-                    }
+        private void updateNetwork(
+                @Nullable Network network, @Nullable LinkProperties linkProperties) {
+            if (mIwlanDataService.isNetworkConnected(
+                    isActiveDataOnOtherSub(getSlotIndex()),
+                    IwlanHelper.isCrossSimCallingEnabled(mContext, getSlotIndex()))) {
+                getTunnelManager().updateNetwork(network, linkProperties);
+            }
+
+            if (Objects.equals(network, sNetwork)) {
+                return;
+            }
+            for (Map.Entry<String, TunnelState> entry : mTunnelStateForApn.entrySet()) {
+                TunnelState tunnelState = entry.getValue();
+                if (tunnelState.getState() == TunnelState.TUNNEL_IN_BRINGUP) {
+                    // force close tunnels in bringup since IKE lib only supports
+                    // updating network for tunnels that are already up.
+                    // This may not result in actual closing of Ike Session since
+                    // epdg selection may not be complete yet.
+                    tunnelState.setState(TunnelState.TUNNEL_IN_FORCE_CLEAN_WAS_IN_BRINGUP);
+                        getTunnelManager()
+                                .closeTunnel(
+                                        entry.getKey(),
+                                        true /* forceClose */,
+                                        getIwlanTunnelCallback(),
+                                        getIwlanTunnelMetrics());
                 }
             }
         }
@@ -1122,34 +1003,91 @@
         private void dnsPrefetchCheck() {
             boolean networkConnected =
                     mIwlanDataService.isNetworkConnected(
-                            IwlanHelper.isDefaultDataSlot(mContext, getSlotIndex()),
+                            isActiveDataOnOtherSub(getSlotIndex()),
                             IwlanHelper.isCrossSimCallingEnabled(mContext, getSlotIndex()));
             /* Check if we need to do prefecting */
-            synchronized (mTunnelStateForApn) {
-                if (networkConnected == true
-                        && mCarrierConfigReady == true
-                        && mWfcEnabled == true
-                        && mTunnelStateForApn.isEmpty()) {
+            if (networkConnected
+                    && mCarrierConfigReady
+                    && mWfcEnabled
+                    && mTunnelStateForApn.isEmpty()) {
 
-                    // Get roaming status
-                    TelephonyManager telephonyManager =
-                            mContext.getSystemService(TelephonyManager.class);
-                    telephonyManager =
-                            telephonyManager.createForSubscriptionId(
-                                    IwlanHelper.getSubId(mContext, getSlotIndex()));
-                    boolean isRoaming = telephonyManager.isNetworkRoaming();
-                    Log.d(TAG, "Trigger EPDG prefetch. Roaming=" + isRoaming);
+                // Get roaming status
+                TelephonyManager telephonyManager =
+                        mContext.getSystemService(TelephonyManager.class);
+                telephonyManager =
+                        telephonyManager.createForSubscriptionId(
+                                IwlanHelper.getSubId(mContext, getSlotIndex()));
+                boolean isRoaming = telephonyManager.isNetworkRoaming();
+                Log.d(TAG, "Trigger EPDG prefetch. Roaming=" + isRoaming);
 
-                    prefetchEpdgServerList(mIwlanDataService.sNetwork, isRoaming);
-                }
+                prefetchEpdgServerList(mIwlanDataService.sNetwork, isRoaming);
             }
         }
 
         private void prefetchEpdgServerList(Network network, boolean isRoaming) {
             mEpdgSelector.getValidatedServerList(
-                    0, EpdgSelector.PROTO_FILTER_IPV4V6, isRoaming, false, network, null);
+                    0,
+                    EpdgSelector.PROTO_FILTER_IPV4V6,
+                    EpdgSelector.SYSTEM_PREFERRED,
+                    isRoaming,
+                    false,
+                    network,
+                    null);
             mEpdgSelector.getValidatedServerList(
-                    0, EpdgSelector.PROTO_FILTER_IPV4V6, isRoaming, true, network, null);
+                    0,
+                    EpdgSelector.PROTO_FILTER_IPV4V6,
+                    EpdgSelector.SYSTEM_PREFERRED,
+                    isRoaming,
+                    true,
+                    network,
+                    null);
+        }
+
+        private int getCurrentCellularRat() {
+            TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
+            telephonyManager =
+                    telephonyManager.createForSubscriptionId(
+                            IwlanHelper.getSubId(mContext, getSlotIndex()));
+            List<CellInfo> cellInfoList = telephonyManager.getAllCellInfo();
+            if (cellInfoList == null) {
+                Log.e(TAG, "cellInfoList is NULL");
+                return 0;
+            }
+
+            for (CellInfo cellInfo : cellInfoList) {
+                if (!cellInfo.isRegistered()) {
+                    continue;
+                }
+                if (cellInfo instanceof CellInfoGsm) {
+                    return TelephonyManager.NETWORK_TYPE_GSM;
+                } else if (cellInfo instanceof CellInfoWcdma) {
+                    return TelephonyManager.NETWORK_TYPE_UMTS;
+                } else if (cellInfo instanceof CellInfoLte) {
+                    return TelephonyManager.NETWORK_TYPE_LTE;
+                } else if (cellInfo instanceof CellInfoNr) {
+                    return TelephonyManager.NETWORK_TYPE_NR;
+                }
+            }
+            return TelephonyManager.NETWORK_TYPE_UNKNOWN;
+        }
+
+        /* Determines if this subscription is in an active call */
+        private boolean isOnCall() {
+            return mCallState != TelephonyManager.CALL_STATE_IDLE;
+        }
+
+        /**
+         * IMS and Emergency are not allowed to retry with initial attach during call to keep call
+         * continuity. Other APNs like XCAP and MMS are allowed to retry with initial attach
+         * regardless of the call state.
+         */
+        private boolean shouldRetryWithInitialAttachForHandoverRequest(
+                String apn, TunnelState tunnelState) {
+            boolean isOnImsOrEmergencyCall = tunnelState.getIsImsOrEmergency() && isOnCall();
+            return tunnelState.getIsHandover()
+                    && !isOnImsOrEmergencyCall
+                    && ErrorPolicyManager.getInstance(mContext, getSlotIndex())
+                            .shouldRetryWithInitialAttach(apn);
         }
 
         /**
@@ -1160,109 +1098,801 @@
         public void close() {
             // TODO: call epdgtunnelmanager.releaseInstance or equivalent
             mIwlanDataService.removeDataServiceProvider(this);
-            IwlanEventListener.getInstance(mContext, getSlotIndex()).removeEventListener(mHandler);
-            mHandlerThread.quit();
+            IwlanEventListener iwlanEventListener =
+                    IwlanEventListener.getInstance(mContext, getSlotIndex());
+            iwlanEventListener.removeEventListener(getIwlanDataServiceHandler());
+            iwlanEventListener.unregisterContentObserver();
         }
 
         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
             pw.println("---- IwlanDataServiceProvider[" + getSlotIndex() + "] ----");
             boolean isDDS = IwlanHelper.isDefaultDataSlot(mContext, getSlotIndex());
             boolean isCSTEnabled = IwlanHelper.isCrossSimCallingEnabled(mContext, getSlotIndex());
-            pw.println("isDefaultDataSub: " + isDDS + " isCrossSimEnabled: " + isCSTEnabled);
+            pw.println(
+                    "isDefaultDataSlot: "
+                            + isDDS
+                            + "subID: "
+                            + IwlanHelper.getSubId(mContext, getSlotIndex())
+                            + " mConnectedDataSub: "
+                            + mConnectedDataSub
+                            + " isCrossSimEnabled: "
+                            + isCSTEnabled);
             pw.println(
                     "isNetworkConnected: "
-                            + isNetworkConnected(isDDS, isCSTEnabled)
+                            + isNetworkConnected(
+                                    isActiveDataOnOtherSub(getSlotIndex()), isCSTEnabled)
                             + " Wfc enabled: "
                             + mWfcEnabled);
-            synchronized (mTunnelStateForApn) {
-                for (Map.Entry<String, TunnelState> entry : mTunnelStateForApn.entrySet()) {
-                    pw.println("Tunnel state for APN: " + entry.getKey());
-                    pw.println(entry.getValue());
-                }
+            for (Map.Entry<String, TunnelState> entry : mTunnelStateForApn.entrySet()) {
+                pw.println("Tunnel state for APN: " + entry.getKey());
+                pw.println(entry.getValue());
             }
             pw.println(mTunnelStats);
-            EpdgTunnelManager.getInstance(mContext, getSlotIndex()).dump(fd, pw, args);
-            ErrorPolicyManager.getInstance(mContext, getSlotIndex()).dump(fd, pw, args);
+            EpdgTunnelManager.getInstance(mContext, getSlotIndex()).dump(pw);
+            ErrorPolicyManager.getInstance(mContext, getSlotIndex()).dump(pw);
             pw.println("-------------------------------------");
         }
+
+        @VisibleForTesting
+        public void setCalendar(Calendar c) {
+            mCalendar = c;
+        }
     }
 
+    private final class IwlanDataServiceHandler extends Handler {
+        private final String TAG = IwlanDataServiceHandler.class.getSimpleName();
+
+        @Override
+        public void handleMessage(Message msg) {
+            Log.d(TAG, "msg.what = " + eventToString(msg.what));
+
+            String apnName;
+            IwlanDataServiceProvider iwlanDataServiceProvider;
+            IwlanDataServiceProvider.TunnelState tunnelState;
+            DataServiceCallback callback;
+            int reason;
+            int slotId;
+            int retryTimeMillis;
+            int errorCause;
+            MetricsAtom metricsAtom;
+
+            switch (msg.what) {
+                case EVENT_TUNNEL_OPENED:
+                    TunnelOpenedData tunnelOpenedData = (TunnelOpenedData) msg.obj;
+                    iwlanDataServiceProvider = tunnelOpenedData.mIwlanDataServiceProvider;
+                    apnName = tunnelOpenedData.mApnName;
+                    TunnelLinkProperties tunnelLinkProperties =
+                            tunnelOpenedData.mTunnelLinkProperties;
+
+                    tunnelState = iwlanDataServiceProvider.mTunnelStateForApn.get(apnName);
+                    // tunnelstate should not be null, design violation.
+                    // if its null, we should crash and debug.
+                    tunnelState.setTunnelLinkProperties(tunnelLinkProperties);
+                    tunnelState.setState(IwlanDataServiceProvider.TunnelState.TUNNEL_UP);
+                    iwlanDataServiceProvider.mTunnelStats.reportTunnelSetupSuccess(
+                            apnName, tunnelState);
+
+                    iwlanDataServiceProvider.deliverCallback(
+                            IwlanDataServiceProvider.CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
+                            DataServiceCallback.RESULT_SUCCESS,
+                            tunnelState.getDataServiceCallback(),
+                            iwlanDataServiceProvider.apnTunnelStateToDataCallResponse(apnName));
+                    break;
+
+                case EVENT_TUNNEL_CLOSED:
+                    TunnelClosedData tunnelClosedData = (TunnelClosedData) msg.obj;
+                    iwlanDataServiceProvider = tunnelClosedData.mIwlanDataServiceProvider;
+                    apnName = tunnelClosedData.mApnName;
+                    IwlanError iwlanError = tunnelClosedData.mIwlanError;
+
+                    tunnelState = iwlanDataServiceProvider.mTunnelStateForApn.get(apnName);
+
+                    if (tunnelState == null) {
+                        // On a successful handover to EUTRAN, the NW may initiate an IKE DEL before
+                        // the UE initiates a deactivateDataCall(). There may be a race condition
+                        // where the deactivateDataCall() arrives immediately before
+                        // IwlanDataService receives EVENT_TUNNEL_CLOSED (and clears TunnelState).
+                        // Even though there is no tunnel, EpdgTunnelManager will still process the
+                        // bringdown request and send back an onClosed() to ensure state coherence.
+                        if (iwlanError.getErrorType() != IwlanError.TUNNEL_NOT_FOUND) {
+                            Log.w(
+                                    TAG,
+                                    "Tunnel state does not exist! Unexpected IwlanError: "
+                                            + iwlanError);
+                        }
+                        break;
+                    }
+
+                    iwlanDataServiceProvider.mTunnelStats.reportTunnelDown(apnName, tunnelState);
+                    iwlanDataServiceProvider.mTunnelStateForApn.remove(apnName);
+                    metricsAtom = iwlanDataServiceProvider.mMetricsAtomForApn.get(apnName);
+
+                    if (tunnelState.getState()
+                                    == IwlanDataServiceProvider.TunnelState.TUNNEL_IN_BRINGUP
+                            || tunnelState.getState()
+                                    == IwlanDataServiceProvider.TunnelState
+                                            .TUNNEL_IN_FORCE_CLEAN_WAS_IN_BRINGUP) {
+                        DataCallResponse.Builder respBuilder = new DataCallResponse.Builder();
+                        respBuilder
+                                .setId(apnName.hashCode())
+                                .setProtocolType(tunnelState.getProtocolType());
+
+                        if (iwlanDataServiceProvider.shouldRetryWithInitialAttachForHandoverRequest(
+                                apnName, tunnelState)) {
+                            respBuilder.setHandoverFailureMode(
+                                    DataCallResponse
+                                            .HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_SETUP_NORMAL);
+                            metricsAtom.setHandoverFailureMode(
+                                    DataCallResponse
+                                            .HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_SETUP_NORMAL);
+                        } else if (tunnelState.getIsHandover()) {
+                            respBuilder.setHandoverFailureMode(
+                                    DataCallResponse
+                                            .HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_HANDOVER);
+                            metricsAtom.setHandoverFailureMode(
+                                    DataCallResponse
+                                            .HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_HANDOVER);
+                        }
+
+                        errorCause =
+                                ErrorPolicyManager.getInstance(
+                                                mContext, iwlanDataServiceProvider.getSlotIndex())
+                                        .getDataFailCause(apnName);
+                        if (errorCause != DataFailCause.NONE) {
+                            respBuilder.setCause(errorCause);
+                            metricsAtom.setDataCallFailCause(errorCause);
+
+                            retryTimeMillis =
+                                    (int)
+                                            ErrorPolicyManager.getInstance(
+                                                            mContext,
+                                                            iwlanDataServiceProvider.getSlotIndex())
+                                                    .getCurrentRetryTimeMs(apnName);
+                            respBuilder.setRetryDurationMillis(retryTimeMillis);
+                            metricsAtom.setRetryDurationMillis(retryTimeMillis);
+                        } else {
+                            // TODO(b/265215349): Use a different DataFailCause for scenario where
+                            // tunnel in bringup is closed or force-closed without error.
+                            respBuilder.setCause(DataFailCause.IWLAN_NETWORK_FAILURE);
+                            metricsAtom.setDataCallFailCause(DataFailCause.IWLAN_NETWORK_FAILURE);
+                            respBuilder.setRetryDurationMillis(5000);
+                            metricsAtom.setRetryDurationMillis(5000);
+                        }
+
+                        // Record setup result for the Metrics
+                        metricsAtom.setSetupRequestResult(DataServiceCallback.RESULT_SUCCESS);
+                        metricsAtom.setIwlanError(iwlanError.getErrorType());
+
+                        metricsAtom.setIwlanErrorWrappedClassnameAndStack(iwlanError);
+
+                        metricsAtom.setTunnelState(tunnelState.getState());
+                        metricsAtom.setMessageId(
+                                IwlanStatsLog.IWLAN_SETUP_DATA_CALL_RESULT_REPORTED);
+
+                        iwlanDataServiceProvider.deliverCallback(
+                                IwlanDataServiceProvider.CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
+                                DataServiceCallback.RESULT_SUCCESS,
+                                tunnelState.getDataServiceCallback(),
+                                respBuilder.build());
+                        return;
+                    }
+
+                    // iwlan service triggered teardown
+                    if (tunnelState.getState()
+                            == IwlanDataServiceProvider.TunnelState.TUNNEL_IN_BRINGDOWN) {
+
+                        // IO exception happens when IKE library fails to retransmit requests.
+                        // This can happen for multiple reasons:
+                        // 1. Network disconnection due to wifi off.
+                        // 2. Epdg server does not respond.
+                        // 3. Socket send/receive fails.
+                        // Ignore this during tunnel bring down.
+                        if (iwlanError.getErrorType() != IwlanError.NO_ERROR
+                                && iwlanError.getErrorType()
+                                        != IwlanError.IKE_INTERNAL_IO_EXCEPTION) {
+                            Log.e(TAG, "Unexpected error during tunnel bring down: " + iwlanError);
+                        }
+
+                        iwlanDataServiceProvider.deliverCallback(
+                                IwlanDataServiceProvider.CALLBACK_TYPE_DEACTIVATE_DATACALL_COMPLETE,
+                                DataServiceCallback.RESULT_SUCCESS,
+                                tunnelState.getDataServiceCallback(),
+                                null);
+
+                        return;
+                    }
+
+                    // just update list of data calls. No way to send error up
+                    iwlanDataServiceProvider.notifyDataCallListChanged(
+                            iwlanDataServiceProvider.getCallList());
+
+                    // Report IwlanPdnDisconnectedReason due to the disconnection is neither for
+                    // SETUP_DATA_CALL nor DEACTIVATE_DATA_CALL request.
+                    metricsAtom.setDataCallFailCause(
+                            ErrorPolicyManager.getInstance(
+                                            mContext, iwlanDataServiceProvider.getSlotIndex())
+                                    .getDataFailCause(apnName));
+
+                    WifiManager wifiManager = mContext.getSystemService(WifiManager.class);
+                    if (wifiManager == null) {
+                        Log.e(TAG, "Could not find wifiManager");
+                        return;
+                    }
+
+                    WifiInfo wifiInfo = wifiManager.getConnectionInfo();
+                    if (wifiInfo == null) {
+                        Log.e(TAG, "wifiInfo is null");
+                        return;
+                    }
+
+                    metricsAtom.setWifiSignalValue(wifiInfo.getRssi());
+                    metricsAtom.setMessageId(IwlanStatsLog.IWLAN_PDN_DISCONNECTED_REASON_REPORTED);
+                    break;
+
+                case IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT:
+                    iwlanDataServiceProvider =
+                            (IwlanDataServiceProvider) getDataServiceProvider(msg.arg1);
+
+                    iwlanDataServiceProvider.mCarrierConfigReady = true;
+                    iwlanDataServiceProvider.dnsPrefetchCheck();
+                    break;
+
+                case IwlanEventListener.CARRIER_CONFIG_UNKNOWN_CARRIER_EVENT:
+                    iwlanDataServiceProvider =
+                            (IwlanDataServiceProvider) getDataServiceProvider(msg.arg1);
+
+                    iwlanDataServiceProvider.mCarrierConfigReady = false;
+                    break;
+
+                case IwlanEventListener.WIFI_CALLING_ENABLE_EVENT:
+                    iwlanDataServiceProvider =
+                            (IwlanDataServiceProvider) getDataServiceProvider(msg.arg1);
+
+                    iwlanDataServiceProvider.mWfcEnabled = true;
+                    iwlanDataServiceProvider.dnsPrefetchCheck();
+                    break;
+
+                case IwlanEventListener.WIFI_CALLING_DISABLE_EVENT:
+                    iwlanDataServiceProvider =
+                            (IwlanDataServiceProvider) getDataServiceProvider(msg.arg1);
+
+                    iwlanDataServiceProvider.mWfcEnabled = false;
+                    break;
+
+                case IwlanEventListener.CROSS_SIM_CALLING_ENABLE_EVENT:
+                    iwlanDataServiceProvider =
+                            (IwlanDataServiceProvider) getDataServiceProvider(msg.arg1);
+                    iwlanDataServiceProvider.updateNetwork(sNetwork, sLinkProperties);
+                    break;
+
+                case IwlanEventListener.CELLINFO_CHANGED_EVENT:
+                    List<CellInfo> cellInfolist = (List<CellInfo>) msg.obj;
+                    iwlanDataServiceProvider =
+                            (IwlanDataServiceProvider) getDataServiceProvider(msg.arg1);
+
+                    if (cellInfolist != null
+                            && iwlanDataServiceProvider.isRegisteredCellInfoChanged(cellInfolist)) {
+                        int[] addrResolutionMethods =
+                                IwlanHelper.getConfig(
+                                        CarrierConfigManager.Iwlan
+                                                .KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY,
+                                        mContext,
+                                        iwlanDataServiceProvider.getSlotIndex());
+                        for (int addrResolutionMethod : addrResolutionMethods) {
+                            if (addrResolutionMethod
+                                    == CarrierConfigManager.Iwlan.EPDG_ADDRESS_CELLULAR_LOC) {
+                                iwlanDataServiceProvider.dnsPrefetchCheck();
+                            }
+                        }
+                    }
+                    break;
+
+                case IwlanEventListener.CALL_STATE_CHANGED_EVENT:
+                    iwlanDataServiceProvider =
+                            (IwlanDataServiceProvider) getDataServiceProvider(msg.arg1);
+
+                    iwlanDataServiceProvider.mCallState = msg.arg2;
+                    break;
+
+                case EVENT_SETUP_DATA_CALL:
+                    SetupDataCallData setupDataCallData = (SetupDataCallData) msg.obj;
+                    int accessNetworkType = setupDataCallData.mAccessNetworkType;
+                    @NonNull DataProfile dataProfile = setupDataCallData.mDataProfile;
+                    boolean isRoaming = setupDataCallData.mIsRoaming;
+                    reason = setupDataCallData.mReason;
+                    LinkProperties linkProperties = setupDataCallData.mLinkProperties;
+                    @IntRange(from = 0, to = 15)
+                    int pduSessionId = setupDataCallData.mPduSessionId;
+                    callback = setupDataCallData.mCallback;
+                    iwlanDataServiceProvider = setupDataCallData.mIwlanDataServiceProvider;
+
+                    if ((accessNetworkType != AccessNetworkType.IWLAN)
+                            || (dataProfile == null)
+                            || (linkProperties == null
+                                    && reason == DataService.REQUEST_REASON_HANDOVER)) {
+
+                        iwlanDataServiceProvider.deliverCallback(
+                                IwlanDataServiceProvider.CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
+                                DataServiceCallback.RESULT_ERROR_INVALID_ARG,
+                                callback,
+                                null);
+                        return;
+                    }
+
+                    slotId = iwlanDataServiceProvider.getSlotIndex();
+                    boolean isCSTEnabled = IwlanHelper.isCrossSimCallingEnabled(mContext, slotId);
+                    boolean networkConnected =
+                            isNetworkConnected(isActiveDataOnOtherSub(slotId), isCSTEnabled);
+                    Log.d(
+                            TAG + "[" + slotId + "]",
+                            "isDds: "
+                                    + IwlanHelper.isDefaultDataSlot(mContext, slotId)
+                                    + ", isActiveDataOnOtherSub: "
+                                    + isActiveDataOnOtherSub(slotId)
+                                    + ", isCstEnabled: "
+                                    + isCSTEnabled
+                                    + ", transport: "
+                                    + sDefaultDataTransport);
+
+                    if (!networkConnected) {
+                        iwlanDataServiceProvider.deliverCallback(
+                                IwlanDataServiceProvider.CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
+                                5 /* DataServiceCallback.RESULT_ERROR_TEMPORARILY_UNAVAILABLE
+                                   */,
+                                callback,
+                                null);
+                        return;
+                    }
+
+                    tunnelState =
+                            iwlanDataServiceProvider.mTunnelStateForApn.get(
+                                    dataProfile.getApnSetting().getApnName());
+
+                    // Return the existing PDN if the pduSessionId is the same and the tunnel
+                    // state is
+                    // TUNNEL_UP.
+                    if (tunnelState != null) {
+                        if (tunnelState.getPduSessionId() == pduSessionId
+                                && tunnelState.getState()
+                                        == IwlanDataServiceProvider.TunnelState.TUNNEL_UP) {
+                            Log.w(
+                                    TAG + "[" + slotId + "]",
+                                    "The tunnel for "
+                                            + dataProfile.getApnSetting().getApnName()
+                                            + " already exists.");
+                            iwlanDataServiceProvider.deliverCallback(
+                                    IwlanDataServiceProvider.CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
+                                    DataServiceCallback.RESULT_SUCCESS,
+                                    callback,
+                                    iwlanDataServiceProvider.apnTunnelStateToDataCallResponse(
+                                            dataProfile.getApnSetting().getApnName()));
+                        } else {
+                            Log.e(
+                                    TAG + "[" + slotId + "]",
+                                    "Force close the existing PDN. pduSessionId = "
+                                            + tunnelState.getPduSessionId()
+                                            + " Tunnel State = "
+                                            + tunnelState.getState());
+                            iwlanDataServiceProvider
+                                    .getTunnelManager()
+                                    .closeTunnel(
+                                            dataProfile.getApnSetting().getApnName(),
+                                            true /* forceClose */,
+                                            iwlanDataServiceProvider.getIwlanTunnelCallback(),
+                                            iwlanDataServiceProvider.getIwlanTunnelMetrics());
+                            iwlanDataServiceProvider.deliverCallback(
+                                    IwlanDataServiceProvider.CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
+                                    5 /* DataServiceCallback
+                                      .RESULT_ERROR_TEMPORARILY_UNAVAILABLE */,
+                                    callback,
+                                    null);
+                        }
+                        return;
+                    }
+
+                    TunnelSetupRequest.Builder tunnelReqBuilder =
+                            TunnelSetupRequest.builder()
+                                    .setApnName(dataProfile.getApnSetting().getApnName())
+                                    .setIsRoaming(isRoaming)
+                                    .setPduSessionId(pduSessionId)
+                                    .setApnIpProtocol(
+                                            isRoaming
+                                                    ? dataProfile
+                                                            .getApnSetting()
+                                                            .getRoamingProtocol()
+                                                    : dataProfile.getApnSetting().getProtocol());
+
+                    if (reason == DataService.REQUEST_REASON_HANDOVER) {
+                        // for now assume that, at max,  only one address of eachtype (v4/v6).
+                        // TODO: Check if multiple ips can be sent in ike tunnel setup
+                        for (LinkAddress lAddr : linkProperties.getLinkAddresses()) {
+                            if (lAddr.isIpv4()) {
+                                tunnelReqBuilder.setSrcIpv4Address(lAddr.getAddress());
+                            } else if (lAddr.isIpv6()) {
+                                tunnelReqBuilder.setSrcIpv6Address(lAddr.getAddress());
+                                tunnelReqBuilder.setSrcIpv6AddressPrefixLength(
+                                        lAddr.getPrefixLength());
+                            }
+                        }
+                    }
+
+                    int apnTypeBitmask = dataProfile.getApnSetting().getApnTypeBitmask();
+                    boolean isIMS = hasApnTypes(apnTypeBitmask, ApnSetting.TYPE_IMS);
+                    boolean isEmergency = hasApnTypes(apnTypeBitmask, ApnSetting.TYPE_EMERGENCY);
+                    tunnelReqBuilder.setRequestPcscf(isIMS || isEmergency);
+                    tunnelReqBuilder.setIsEmergency(isEmergency);
+
+                    iwlanDataServiceProvider.setTunnelState(
+                            dataProfile,
+                            callback,
+                            IwlanDataServiceProvider.TunnelState.TUNNEL_IN_BRINGUP,
+                            null,
+                            (reason == DataService.REQUEST_REASON_HANDOVER),
+                            pduSessionId,
+                            isIMS || isEmergency);
+
+                    boolean result =
+                            iwlanDataServiceProvider
+                                    .getTunnelManager()
+                                    .bringUpTunnel(
+                                            tunnelReqBuilder.build(),
+                                            iwlanDataServiceProvider.getIwlanTunnelCallback(),
+                                            iwlanDataServiceProvider.getIwlanTunnelMetrics());
+                    Log.d(TAG + "[" + slotId + "]", "bringup Tunnel with result:" + result);
+                    if (!result) {
+                        iwlanDataServiceProvider.deliverCallback(
+                                IwlanDataServiceProvider.CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
+                                DataServiceCallback.RESULT_ERROR_INVALID_ARG,
+                                callback,
+                                null);
+                        return;
+                    }
+                    break;
+
+                case EVENT_DEACTIVATE_DATA_CALL:
+                    DeactivateDataCallData deactivateDataCallData =
+                            (DeactivateDataCallData) msg.obj;
+                    iwlanDataServiceProvider = deactivateDataCallData.mIwlanDataServiceProvider;
+                    callback = deactivateDataCallData.mCallback;
+                    reason = deactivateDataCallData.mReason;
+
+                    int cid = deactivateDataCallData.mCid;
+                    slotId = iwlanDataServiceProvider.getSlotIndex();
+                    boolean isNetworkLost =
+                            !isNetworkConnected(
+                                    isActiveDataOnOtherSub(slotId),
+                                    IwlanHelper.isCrossSimCallingEnabled(mContext, slotId));
+                    boolean isHandOutSuccessful = (reason == REQUEST_REASON_HANDOVER);
+
+                    for (String apn : iwlanDataServiceProvider.mTunnelStateForApn.keySet()) {
+                        if (apn.hashCode() == cid) {
+                            // No need to check state since dataconnection in framework serializes
+                            // setup and deactivate calls using callId/cid.
+                            iwlanDataServiceProvider
+                                    .mTunnelStateForApn
+                                    .get(apn)
+                                    .setState(
+                                            IwlanDataServiceProvider.TunnelState
+                                                    .TUNNEL_IN_BRINGDOWN);
+                            iwlanDataServiceProvider
+                                    .mTunnelStateForApn
+                                    .get(apn)
+                                    .setDataServiceCallback(callback);
+
+                            // According to the handover procedure in 3GPP specifications (TS 23.402
+                            // clause 8.6.1 for S1; TS 23.502 clause 4.11.4.1 for N1), if the PDN is
+                            // handed out to another RAT, the IKE tunnel over ePDG SHOULD be
+                            // released by the network.  Thus, UE just released the tunnel locally.
+                            iwlanDataServiceProvider
+                                    .getTunnelManager()
+                                    .closeTunnel(
+                                            apn,
+                                            isNetworkLost || isHandOutSuccessful /* forceClose */,
+                                            iwlanDataServiceProvider.getIwlanTunnelCallback(),
+                                            iwlanDataServiceProvider.getIwlanTunnelMetrics());
+                            return;
+                        }
+                    }
+
+                    iwlanDataServiceProvider.deliverCallback(
+                            IwlanDataServiceProvider.CALLBACK_TYPE_DEACTIVATE_DATACALL_COMPLETE,
+                            DataServiceCallback.RESULT_ERROR_INVALID_ARG,
+                            callback,
+                            null);
+                    break;
+
+                case EVENT_DATA_CALL_LIST_REQUEST:
+                    DataCallRequestData dataCallRequestData = (DataCallRequestData) msg.obj;
+                    callback = dataCallRequestData.mCallback;
+                    iwlanDataServiceProvider = dataCallRequestData.mIwlanDataServiceProvider;
+
+                    iwlanDataServiceProvider.deliverCallback(
+                            IwlanDataServiceProvider.CALLBACK_TYPE_GET_DATACALL_LIST_COMPLETE,
+                            DataServiceCallback.RESULT_SUCCESS,
+                            callback,
+                            null);
+                    break;
+
+                case EVENT_FORCE_CLOSE_TUNNEL:
+                    for (IwlanDataServiceProvider dp : sIwlanDataServiceProviders.values()) {
+                        dp.forceCloseTunnels();
+                    }
+                    break;
+
+                case EVENT_ADD_DATA_SERVICE_PROVIDER:
+                    iwlanDataServiceProvider = (IwlanDataServiceProvider) msg.obj;
+                    addIwlanDataServiceProvider(iwlanDataServiceProvider);
+                    break;
+
+                case EVENT_REMOVE_DATA_SERVICE_PROVIDER:
+                    iwlanDataServiceProvider = (IwlanDataServiceProvider) msg.obj;
+
+                    slotId = iwlanDataServiceProvider.getSlotIndex();
+                    IwlanDataServiceProvider dsp = sIwlanDataServiceProviders.remove(slotId);
+                    if (dsp == null) {
+                        Log.w(TAG + "[" + slotId + "]", "No DataServiceProvider exists for slot!");
+                    }
+
+                    if (sIwlanDataServiceProviders.isEmpty()) {
+                        deinitNetworkCallback();
+                    }
+                    break;
+
+                case EVENT_TUNNEL_OPENED_METRICS:
+                    OnOpenedMetrics openedMetricsData = (OnOpenedMetrics) msg.obj;
+                    iwlanDataServiceProvider = openedMetricsData.getIwlanDataServiceProvider();
+                    apnName = openedMetricsData.getApnName();
+
+                    // Record setup result for the Metrics
+                    metricsAtom = iwlanDataServiceProvider.mMetricsAtomForApn.get(apnName);
+                    tunnelState = iwlanDataServiceProvider.mTunnelStateForApn.get(apnName);
+                    metricsAtom.setSetupRequestResult(DataServiceCallback.RESULT_SUCCESS);
+                    metricsAtom.setIwlanError(IwlanError.NO_ERROR);
+                    metricsAtom.setDataCallFailCause(DataFailCause.NONE);
+                    metricsAtom.setTunnelState(tunnelState.getState());
+                    metricsAtom.setHandoverFailureMode(-1);
+                    metricsAtom.setRetryDurationMillis(0);
+                    metricsAtom.setMessageId(IwlanStatsLog.IWLAN_SETUP_DATA_CALL_RESULT_REPORTED);
+                    metricsAtom.setEpdgServerAddress(openedMetricsData.getEpdgServerAddress());
+                    metricsAtom.setProcessingDurationMillis(
+                            (int)
+                                    (System.currentTimeMillis()
+                                            - iwlanDataServiceProvider.mProcessingStartTime));
+                    metricsAtom.setEpdgServerSelectionDurationMillis(
+                            openedMetricsData.getEpdgServerSelectionDuration());
+                    metricsAtom.setIkeTunnelEstablishmentDurationMillis(
+                            openedMetricsData.getIkeTunnelEstablishmentDuration());
+
+                    metricsAtom.sendMetricsData();
+                    break;
+
+                case EVENT_TUNNEL_CLOSED_METRICS:
+                    OnClosedMetrics closedMetricsData = (OnClosedMetrics) msg.obj;
+                    iwlanDataServiceProvider = closedMetricsData.getIwlanDataServiceProvider();
+                    apnName = closedMetricsData.getApnName();
+
+                    metricsAtom = iwlanDataServiceProvider.mMetricsAtomForApn.get(apnName);
+                    if (metricsAtom == null) {
+                        Log.w(TAG, "EVENT_TUNNEL_CLOSED_METRICS: MetricsAtom is null!");
+                        break;
+                    }
+                    metricsAtom.setEpdgServerAddress(closedMetricsData.getEpdgServerAddress());
+                    metricsAtom.setProcessingDurationMillis(
+                            iwlanDataServiceProvider.mProcessingStartTime > 0
+                                    ? (int)
+                                            (System.currentTimeMillis()
+                                                    - iwlanDataServiceProvider.mProcessingStartTime)
+                                    : 0);
+                    metricsAtom.setEpdgServerSelectionDurationMillis(
+                            closedMetricsData.getEpdgServerSelectionDuration());
+                    metricsAtom.setIkeTunnelEstablishmentDurationMillis(
+                            closedMetricsData.getIkeTunnelEstablishmentDuration());
+
+                    metricsAtom.sendMetricsData();
+                    iwlanDataServiceProvider.mMetricsAtomForApn.remove(apnName);
+                    break;
+
+                default:
+                    throw new IllegalStateException("Unexpected value: " + msg.what);
+            }
+        }
+
+        IwlanDataServiceHandler(Looper looper) {
+            super(looper);
+        }
+    }
+
+    private static final class TunnelOpenedData {
+        final String mApnName;
+        final TunnelLinkProperties mTunnelLinkProperties;
+        final IwlanDataServiceProvider mIwlanDataServiceProvider;
+
+        private TunnelOpenedData(
+                String apnName,
+                TunnelLinkProperties tunnelLinkProperties,
+                IwlanDataServiceProvider dsp) {
+            mApnName = apnName;
+            mTunnelLinkProperties = tunnelLinkProperties;
+            mIwlanDataServiceProvider = dsp;
+        }
+    }
+
+    private static final class TunnelClosedData {
+        final String mApnName;
+        final IwlanError mIwlanError;
+        final IwlanDataServiceProvider mIwlanDataServiceProvider;
+
+        private TunnelClosedData(
+                String apnName, IwlanError iwlanError, IwlanDataServiceProvider dsp) {
+            mApnName = apnName;
+            mIwlanError = iwlanError;
+            mIwlanDataServiceProvider = dsp;
+        }
+    }
+
+    private static final class SetupDataCallData {
+        final int mAccessNetworkType;
+        @NonNull final DataProfile mDataProfile;
+        final boolean mIsRoaming;
+        final boolean mAllowRoaming;
+        final int mReason;
+        @Nullable final LinkProperties mLinkProperties;
+
+        @IntRange(from = 0, to = 15)
+        final int mPduSessionId;
+
+        @Nullable final NetworkSliceInfo mSliceInfo;
+        @Nullable final TrafficDescriptor mTrafficDescriptor;
+        final boolean mMatchAllRuleAllowed;
+        @NonNull final DataServiceCallback mCallback;
+        final IwlanDataServiceProvider mIwlanDataServiceProvider;
+
+        private SetupDataCallData(
+                int accessNetworkType,
+                DataProfile dataProfile,
+                boolean isRoaming,
+                boolean allowRoaming,
+                int reason,
+                LinkProperties linkProperties,
+                int pduSessionId,
+                NetworkSliceInfo sliceInfo,
+                TrafficDescriptor trafficDescriptor,
+                boolean matchAllRuleAllowed,
+                DataServiceCallback callback,
+                IwlanDataServiceProvider dsp) {
+            mAccessNetworkType = accessNetworkType;
+            mDataProfile = dataProfile;
+            mIsRoaming = isRoaming;
+            mAllowRoaming = allowRoaming;
+            mReason = reason;
+            mLinkProperties = linkProperties;
+            mPduSessionId = pduSessionId;
+            mSliceInfo = sliceInfo;
+            mTrafficDescriptor = trafficDescriptor;
+            mMatchAllRuleAllowed = matchAllRuleAllowed;
+            mCallback = callback;
+            mIwlanDataServiceProvider = dsp;
+        }
+    }
+
+    private static final class DeactivateDataCallData {
+        final int mCid;
+        final int mReason;
+        final DataServiceCallback mCallback;
+        final IwlanDataServiceProvider mIwlanDataServiceProvider;
+
+        private DeactivateDataCallData(
+                int cid, int reason, DataServiceCallback callback, IwlanDataServiceProvider dsp) {
+            mCid = cid;
+            mReason = reason;
+            mCallback = callback;
+            mIwlanDataServiceProvider = dsp;
+        }
+    }
+
+    private static final class DataCallRequestData {
+        final DataServiceCallback mCallback;
+        final IwlanDataServiceProvider mIwlanDataServiceProvider;
+
+        private DataCallRequestData(DataServiceCallback callback, IwlanDataServiceProvider dsp) {
+            mCallback = callback;
+            mIwlanDataServiceProvider = dsp;
+        }
+    }
+
+    static int getConnectedDataSub(NetworkCapabilities networkCapabilities) {
+        int connectedDataSub = INVALID_SUB_ID;
+        NetworkSpecifier specifier = networkCapabilities.getNetworkSpecifier();
+        TransportInfo transportInfo = networkCapabilities.getTransportInfo();
+
+        if (specifier instanceof TelephonyNetworkSpecifier) {
+            connectedDataSub = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
+        } else if (transportInfo instanceof VcnTransportInfo) {
+            connectedDataSub = ((VcnTransportInfo) transportInfo).getSubId();
+        }
+        return connectedDataSub;
+    }
+
+    static void setConnectedDataSub(int subId) {
+        mConnectedDataSub = subId;
+    }
+
     @VisibleForTesting
-    static synchronized boolean isNetworkConnected(boolean isDds, boolean isCstEnabled) {
-        if (!isDds && isCstEnabled) {
-            // Only Non-DDS sub with CST enabled, can use any transport.
+    static boolean isActiveDataOnOtherSub(int slotId) {
+        int subId = IwlanHelper.getSubId(mContext, slotId);
+        return mConnectedDataSub != INVALID_SUB_ID && subId != mConnectedDataSub;
+    }
+
+    @VisibleForTesting
+    static boolean isNetworkConnected(boolean isActiveDataOnOtherSub, boolean isCstEnabled) {
+        if (isActiveDataOnOtherSub && isCstEnabled) {
+            // For cross-SIM IWLAN (Transport.MOBILE), an active data PDN must be maintained on the
+            // other subscription.
+            if (sNetworkConnected && (sDefaultDataTransport != Transport.MOBILE)) {
+                Log.e(TAG, "Internet is on other slot, but default transport is not MOBILE!");
+            }
             return sNetworkConnected;
         } else {
-            // For all other cases, only wifi transport can be used.
+            // For all other cases, only Transport.WIFI can be used.
             return ((sDefaultDataTransport == Transport.WIFI) && sNetworkConnected);
         }
     }
 
-    @VisibleForTesting
     /* Note: this api should have valid transport if networkConnected==true */
-    // Only synchronize on IwlanDataService.class for changes being made to static variables
-    // Calls to DataServiceProvider object methods (or any objects in the future) should
-    // not be made within synchronized block protected by IwlanDataService.class
     static void setNetworkConnected(
-            boolean networkConnected, Network network, Transport transport) {
+            boolean networkConnected, @NonNull Network network, Transport transport) {
 
         boolean hasNetworkChanged = false;
         boolean hasTransportChanged = false;
         boolean hasNetworkConnectedChanged = false;
 
-        synchronized (IwlanDataService.class) {
-            if (sNetworkConnected == networkConnected
-                    && network.equals(sNetwork)
-                    && sDefaultDataTransport == transport) {
-                // Nothing changed
-                return;
-            }
-
-            // safety check
-            if (networkConnected && transport == Transport.UNSPECIFIED_NETWORK) {
-                Log.e(TAG, "setNetworkConnected: Network connected but transport unspecified");
-                return;
-            }
-
-            if (!network.equals(sNetwork)) {
-                Log.e(TAG, "setNetworkConnected NW changed from: " + sNetwork + " TO: " + network);
-                hasNetworkChanged = true;
-            }
-
-            if (transport != sDefaultDataTransport) {
-                Log.d(
-                        TAG,
-                        "Transport was changed from "
-                                + sDefaultDataTransport.name()
-                                + " to "
-                                + transport.name());
-                hasTransportChanged = true;
-            }
-
-            if (sNetworkConnected != networkConnected) {
-                Log.d(
-                        TAG,
-                        "Network connected state change from "
-                                + sNetworkConnected
-                                + " to "
-                                + networkConnected);
-                hasNetworkConnectedChanged = true;
-            }
-
-            sNetworkConnected = networkConnected;
-            sDefaultDataTransport = transport;
-            sNetwork = network;
-            if (!networkConnected) {
-                // reset link protocol type
-                sLinkProtocolType = LinkProtocolType.UNKNOWN;
-            }
+        if (sNetworkConnected == networkConnected
+                && network.equals(sNetwork)
+                && sDefaultDataTransport == transport) {
+            // Nothing changed
+            return;
         }
 
+        // safety check
+        if (networkConnected && transport == Transport.UNSPECIFIED_NETWORK) {
+            Log.e(TAG, "setNetworkConnected: Network connected but transport unspecified");
+            return;
+        }
+
+        if (!network.equals(sNetwork)) {
+            Log.e(TAG, "System default network changed from: " + sNetwork + " TO: " + network);
+            hasNetworkChanged = true;
+        }
+
+        if (transport != sDefaultDataTransport) {
+            Log.d(
+                    TAG,
+                    "Transport was changed from "
+                            + sDefaultDataTransport.name()
+                            + " to "
+                            + transport.name());
+            hasTransportChanged = true;
+        }
+
+        if (sNetworkConnected != networkConnected) {
+            Log.d(
+                    TAG,
+                    "Network connected state change from "
+                            + sNetworkConnected
+                            + " to "
+                            + networkConnected);
+            hasNetworkConnectedChanged = true;
+        }
+
+        sNetworkConnected = networkConnected;
+        sDefaultDataTransport = transport;
+        sNetwork = network;
+
         if (networkConnected) {
             if (hasTransportChanged) {
                 // Perform forceClose for tunnels in bringdown.
                 // let framework handle explicit teardown
-                for (IwlanDataServiceProvider dp : sIwlanDataServiceProviderList) {
+                for (IwlanDataServiceProvider dp : sIwlanDataServiceProviders.values()) {
                     dp.forceCloseTunnelsInDeactivatingState();
                 }
             }
@@ -1272,14 +1902,18 @@
             }
             // only prefetch dns and updateNetwork if Network has changed
             if (hasNetworkChanged) {
-                for (IwlanDataServiceProvider dp : sIwlanDataServiceProviderList) {
+                ConnectivityManager connectivityManager =
+                        mContext.getSystemService(ConnectivityManager.class);
+                LinkProperties linkProperties = connectivityManager.getLinkProperties(network);
+                sLinkProperties = linkProperties;
+                for (IwlanDataServiceProvider dp : sIwlanDataServiceProviders.values()) {
                     dp.dnsPrefetchCheck();
-                    dp.updateNetwork(sNetwork);
+                    dp.updateNetwork(sNetwork, linkProperties);
                 }
                 IwlanHelper.updateCountryCodeWhenNetworkConnected();
             }
         } else {
-            for (IwlanDataServiceProvider dp : sIwlanDataServiceProviderList) {
+            for (IwlanDataServiceProvider dp : sIwlanDataServiceProviders.values()) {
                 // once network is disconnected, even NAT KA offload fails
                 // But we should still let framework do an explicit teardown
                 // so as to not affect an ongoing handover
@@ -1289,48 +1923,6 @@
         }
     }
 
-    static boolean isLinkProtocolTypeChanged(LinkProperties linkProperties) {
-        boolean hasIPV4 = false;
-        boolean hasIPV6 = false;
-
-        LinkProtocolType linkProtocolType = null;
-        if (linkProperties != null) {
-            for (LinkAddress linkAddress : linkProperties.getLinkAddresses()) {
-                InetAddress inetaddr = linkAddress.getAddress();
-                // skip linklocal and loopback addresses
-                if (!inetaddr.isLoopbackAddress() && !inetaddr.isLinkLocalAddress()) {
-                    if (inetaddr instanceof Inet4Address) {
-                        hasIPV4 = true;
-                    } else if (inetaddr instanceof Inet6Address) {
-                        hasIPV6 = true;
-                    }
-                }
-            }
-
-            if (hasIPV4 && hasIPV6) {
-                linkProtocolType = LinkProtocolType.IPV4V6;
-            } else if (hasIPV4) {
-                linkProtocolType = LinkProtocolType.IPV4;
-            } else if (hasIPV6) {
-                linkProtocolType = LinkProtocolType.IPV6;
-            }
-
-            if (sLinkProtocolType != linkProtocolType) {
-                Log.d(
-                        TAG,
-                        "LinkProtocolType was changed from "
-                                + sLinkProtocolType
-                                + " to "
-                                + linkProtocolType);
-                sLinkProtocolType = linkProtocolType;
-                return true;
-            }
-            return false;
-        }
-        Log.w(TAG, "linkProperties is NULL.");
-        return false;
-    }
-
     /**
      * Get the DataServiceProvider associated with the slotId
      *
@@ -1338,16 +1930,7 @@
      * @return DataService.DataServiceProvider associated with the slot
      */
     public static DataService.DataServiceProvider getDataServiceProvider(int slotId) {
-        DataServiceProvider ret = null;
-        if (!sIwlanDataServiceProviderList.isEmpty()) {
-            for (IwlanDataServiceProvider provider : sIwlanDataServiceProviderList) {
-                if (provider.getSlotIndex() == slotId) {
-                    ret = provider;
-                    break;
-                }
-            }
-        }
-        return ret;
+        return sIwlanDataServiceProviders.get(slotId);
     }
 
     public static Context getContext() {
@@ -1360,42 +1943,55 @@
         Log.d(TAG, "Creating provider for " + slotIndex);
 
         if (mNetworkMonitorCallback == null) {
-            // start monitoring network
-            mNetworkCallbackHandlerThread =
-                    new HandlerThread(IwlanNetworkService.class.getSimpleName());
-            mNetworkCallbackHandlerThread.start();
-            Looper looper = mNetworkCallbackHandlerThread.getLooper();
-            Handler handler = new Handler(looper);
-
-            // register for default network callback
+            // start monitoring network and register for default network callback
             ConnectivityManager connectivityManager =
                     mContext.getSystemService(ConnectivityManager.class);
             mNetworkMonitorCallback = new IwlanNetworkMonitorCallback();
-            connectivityManager.registerDefaultNetworkCallback(mNetworkMonitorCallback, handler);
+            if (connectivityManager != null) {
+                connectivityManager.registerSystemDefaultNetworkCallback(
+                        mNetworkMonitorCallback, getIwlanDataServiceHandler());
+            }
             Log.d(TAG, "Registered with Connectivity Service");
         }
 
         IwlanDataServiceProvider dp = new IwlanDataServiceProvider(slotIndex, this);
-        addIwlanDataServiceProvider(dp);
+
+        getIwlanDataServiceHandler()
+                .sendMessage(
+                        getIwlanDataServiceHandler()
+                                .obtainMessage(EVENT_ADD_DATA_SERVICE_PROVIDER, dp));
         return dp;
     }
 
     public void removeDataServiceProvider(IwlanDataServiceProvider dp) {
-        sIwlanDataServiceProviderList.remove(dp);
-        if (sIwlanDataServiceProviderList.isEmpty()) {
-            // deinit network related stuff
-            ConnectivityManager connectivityManager =
-                    mContext.getSystemService(ConnectivityManager.class);
-            connectivityManager.unregisterNetworkCallback(mNetworkMonitorCallback);
-            mNetworkCallbackHandlerThread.quit(); // no need to quitSafely
-            mNetworkCallbackHandlerThread = null;
-            mNetworkMonitorCallback = null;
-        }
+        getIwlanDataServiceHandler()
+                .sendMessage(
+                        getIwlanDataServiceHandler()
+                                .obtainMessage(EVENT_REMOVE_DATA_SERVICE_PROVIDER, dp));
     }
 
     @VisibleForTesting
     void addIwlanDataServiceProvider(IwlanDataServiceProvider dp) {
-        sIwlanDataServiceProviderList.add(dp);
+        int slotIndex = dp.getSlotIndex();
+        if (sIwlanDataServiceProviders.containsKey(slotIndex)) {
+            throw new IllegalStateException(
+                    "DataServiceProvider already exists for slot " + slotIndex);
+        }
+        sIwlanDataServiceProviders.put(slotIndex, dp);
+    }
+
+    void deinitNetworkCallback() {
+        // deinit network related stuff
+        ConnectivityManager connectivityManager =
+                mContext.getSystemService(ConnectivityManager.class);
+        if (connectivityManager != null) {
+            connectivityManager.unregisterNetworkCallback(mNetworkMonitorCallback);
+        }
+        mNetworkMonitorCallback = null;
+    }
+
+    boolean hasApnTypes(int apnTypeBitmask, int expectedApn) {
+        return (apnTypeBitmask & expectedApn) != 0;
     }
 
     @VisibleForTesting
@@ -1408,9 +2004,67 @@
         return mNetworkMonitorCallback;
     }
 
+    @VisibleForTesting
+    @NonNull
+    Handler getIwlanDataServiceHandler() {
+        if (mIwlanDataServiceHandler == null) {
+            mIwlanDataServiceHandler = new IwlanDataServiceHandler(getLooper());
+        }
+        return mIwlanDataServiceHandler;
+    }
+
+    @VisibleForTesting
+    Looper getLooper() {
+        mIwlanDataServiceHandlerThread = new HandlerThread("IwlanDataServiceThread");
+        mIwlanDataServiceHandlerThread.start();
+        return mIwlanDataServiceHandlerThread.getLooper();
+    }
+
+    private static String eventToString(int event) {
+        switch (event) {
+            case EVENT_TUNNEL_OPENED:
+                return "EVENT_TUNNEL_OPENED";
+            case EVENT_TUNNEL_CLOSED:
+                return "EVENT_TUNNEL_CLOSED";
+            case EVENT_SETUP_DATA_CALL:
+                return "EVENT_SETUP_DATA_CALL";
+            case EVENT_DEACTIVATE_DATA_CALL:
+                return "EVENT_DEACTIVATE_DATA_CALL";
+            case EVENT_DATA_CALL_LIST_REQUEST:
+                return "EVENT_DATA_CALL_LIST_REQUEST";
+            case EVENT_FORCE_CLOSE_TUNNEL:
+                return "EVENT_FORCE_CLOSE_TUNNEL";
+            case EVENT_ADD_DATA_SERVICE_PROVIDER:
+                return "EVENT_ADD_DATA_SERVICE_PROVIDER";
+            case EVENT_REMOVE_DATA_SERVICE_PROVIDER:
+                return "EVENT_REMOVE_DATA_SERVICE_PROVIDER";
+            case IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT:
+                return "CARRIER_CONFIG_CHANGED_EVENT";
+            case IwlanEventListener.CARRIER_CONFIG_UNKNOWN_CARRIER_EVENT:
+                return "CARRIER_CONFIG_UNKNOWN_CARRIER_EVENT";
+            case IwlanEventListener.WIFI_CALLING_ENABLE_EVENT:
+                return "WIFI_CALLING_ENABLE_EVENT";
+            case IwlanEventListener.WIFI_CALLING_DISABLE_EVENT:
+                return "WIFI_CALLING_DISABLE_EVENT";
+            case IwlanEventListener.CROSS_SIM_CALLING_ENABLE_EVENT:
+                return "CROSS_SIM_CALLING_ENABLE_EVENT";
+            case IwlanEventListener.CELLINFO_CHANGED_EVENT:
+                return "CELLINFO_CHANGED_EVENT";
+            case EVENT_TUNNEL_OPENED_METRICS:
+                return "EVENT_TUNNEL_OPENED_METRICS";
+            case EVENT_TUNNEL_CLOSED_METRICS:
+                return "EVENT_TUNNEL_CLOSED_METRICS";
+            case IwlanEventListener.CALL_STATE_CHANGED_EVENT:
+                return "CALL_STATE_CHANGED_EVENT";
+            default:
+                return "Unknown(" + event + ")";
+        }
+    }
+
     @Override
     public void onCreate() {
-        setAppContext(getApplicationContext());
+        Context context = getApplicationContext().createAttributionContext(CONTEXT_ATTRIBUTION_TAG);
+        setAppContext(context);
         IwlanBroadcastReceiver.startListening(mContext);
         IwlanHelper.startCountryDetector(mContext);
     }
@@ -1422,18 +2076,15 @@
 
     @Override
     public IBinder onBind(Intent intent) {
-        Log.d(TAG, "Iwlanservice onBind");
+        Log.d(TAG, "IwlanDataService onBind");
         return super.onBind(intent);
     }
 
     @Override
     public boolean onUnbind(Intent intent) {
-        Log.d(TAG, "IwlanService onUnbind");
-        // force close all the tunnels when there are no clients
-        // active
-        for (IwlanDataServiceProvider dp : sIwlanDataServiceProviderList) {
-            dp.forceCloseTunnels();
-        }
+        Log.d(TAG, "IwlanDataService onUnbind");
+        getIwlanDataServiceHandler()
+                .sendMessage(getIwlanDataServiceHandler().obtainMessage(EVENT_FORCE_CLOSE_TUNNEL));
         return super.onUnbind(intent);
     }
 
@@ -1446,7 +2097,7 @@
             transport = "WIFI";
         }
         pw.println("Default transport: " + transport);
-        for (IwlanDataServiceProvider provider : sIwlanDataServiceProviderList) {
+        for (IwlanDataServiceProvider provider : sIwlanDataServiceProviders.values()) {
             pw.println();
             provider.dump(fd, pw, args);
             pw.println();
diff --git a/src/com/google/android/iwlan/IwlanError.java b/src/com/google/android/iwlan/IwlanError.java
index f621a7a..fc03a01 100644
--- a/src/com/google/android/iwlan/IwlanError.java
+++ b/src/com/google/android/iwlan/IwlanError.java
@@ -16,16 +16,15 @@
 
 package com.google.android.iwlan;
 
-import android.net.ipsec.ike.exceptions.IkeException;
 import android.net.ipsec.ike.exceptions.IkeIOException;
 import android.net.ipsec.ike.exceptions.IkeInternalException;
+import android.net.ipsec.ike.exceptions.IkeNetworkLostException;
 import android.net.ipsec.ike.exceptions.IkeProtocolException;
 import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 
 import java.io.IOException;
 import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 
 public class IwlanError {
 
@@ -41,10 +40,14 @@
     public static final int EPDG_SELECTOR_SERVER_SELECTION_FAILED = 4;
     public static final int TUNNEL_TRANSFORM_FAILED = 5;
     public static final int SIM_NOT_READY_EXCEPTION = 6;
-    public static final int NETWORK_FAILURE = 7;
-
-    // Catch all exception
-    public static final int UNKNOWN_EXCEPTION = 8; // catch all
+    public static final int IKE_SESSION_CLOSED_BEFORE_CHILD_SESSION_OPENED = 7;
+    public static final int IKE_NETWORK_LOST_EXCEPTION = 8;
+    public static final int TUNNEL_NOT_FOUND = 9;
+    public static final int EPDG_ADDRESS_ONLY_IPV4_ALLOWED = 10;
+    public static final int EPDG_ADDRESS_ONLY_IPV6_ALLOWED = 11;
+    public static final int IKE_INIT_TIMEOUT = 12;
+    public static final int IKE_MOBILITY_TIMEOUT = 13;
+    public static final int IKE_DPD_TIMEOUT = 14;
 
     @IntDef({
         NO_ERROR,
@@ -54,28 +57,38 @@
         EPDG_SELECTOR_SERVER_SELECTION_FAILED,
         TUNNEL_TRANSFORM_FAILED,
         SIM_NOT_READY_EXCEPTION,
-        NETWORK_FAILURE,
-        UNKNOWN_EXCEPTION
+        IKE_SESSION_CLOSED_BEFORE_CHILD_SESSION_OPENED,
+        IKE_NETWORK_LOST_EXCEPTION,
+        TUNNEL_NOT_FOUND,
+        EPDG_ADDRESS_ONLY_IPV4_ALLOWED,
+        EPDG_ADDRESS_ONLY_IPV6_ALLOWED,
+        IKE_INIT_TIMEOUT,
+        IKE_MOBILITY_TIMEOUT,
+        IKE_DPD_TIMEOUT
     })
-    @interface IwlanErrorType {};
+    @interface IwlanErrorType {}
 
     private static final Map<Integer, String> sErrorTypeStrings =
-            new ConcurrentHashMap<>() {
-                {
-                    put(NO_ERROR, "IWLAN_NO_ERROR");
-                    put(IKE_PROTOCOL_EXCEPTION, "IWLAN_IKE_PROTOCOL_EXCEPTION");
-                    put(IKE_INTERNAL_IO_EXCEPTION, "IWLAN_IKE_INTERNAL_IO_EXCEPTION");
-                    put(IKE_GENERIC_EXCEPTION, "IWLAN_IKE_GENERIC_EXCEPTION");
-                    put(
+            Map.ofEntries(
+                    Map.entry(NO_ERROR, "IWLAN_NO_ERROR"),
+                    Map.entry(IKE_PROTOCOL_EXCEPTION, "IWLAN_IKE_PROTOCOL_EXCEPTION"),
+                    Map.entry(IKE_INTERNAL_IO_EXCEPTION, "IWLAN_IKE_INTERNAL_IO_EXCEPTION"),
+                    Map.entry(IKE_GENERIC_EXCEPTION, "IWLAN_IKE_GENERIC_EXCEPTION"),
+                    Map.entry(
                             EPDG_SELECTOR_SERVER_SELECTION_FAILED,
-                            "IWLAN_EPDG_SELECTOR_SERVER_SELECTION_FAILED");
-                    put(TUNNEL_TRANSFORM_FAILED, "IWLAN_TUNNEL_TRANSFORM_FAILED");
-                    put(SIM_NOT_READY_EXCEPTION, "IWLAN_SIM_NOT_READY_EXCEPTION");
-                    put(NETWORK_FAILURE, "IWLAN_NETWORK_FAILURE");
-                    put(UNKNOWN_EXCEPTION, "IWLAN_UNKNOWN_EXCEPTION");
-                }
-            };
-
+                            "IWLAN_EPDG_SELECTOR_SERVER_SELECTION_FAILED"),
+                    Map.entry(TUNNEL_TRANSFORM_FAILED, "IWLAN_TUNNEL_TRANSFORM_FAILED"),
+                    Map.entry(SIM_NOT_READY_EXCEPTION, "IWLAN_SIM_NOT_READY_EXCEPTION"),
+                    Map.entry(
+                            IKE_SESSION_CLOSED_BEFORE_CHILD_SESSION_OPENED,
+                            "IKE_SESSION_CLOSED_BEFORE_CHILD_SESSION_OPENED"),
+                    Map.entry(IKE_NETWORK_LOST_EXCEPTION, "IWLAN_IKE_NETWORK_LOST_EXCEPTION"),
+                    Map.entry(TUNNEL_NOT_FOUND, "IWLAN_TUNNEL_NOT_FOUND"),
+                    Map.entry(IKE_INIT_TIMEOUT, "IKE_INIT_TIMEOUT"),
+                    Map.entry(IKE_MOBILITY_TIMEOUT, "IKE_MOBILITY_TIMEOUT"),
+                    Map.entry(IKE_DPD_TIMEOUT, "IKE_DPD_TIMEOUT"),
+                    Map.entry(EPDG_ADDRESS_ONLY_IPV4_ALLOWED, "EPDG_ADDRESS_ONLY_IPV4_ALLOWED"),
+                    Map.entry(EPDG_ADDRESS_ONLY_IPV6_ALLOWED, "EPDG_ADDRESS_ONLY_IPV6_ALLOWED"));
     private int mErrorType;
     private Exception mException;
 
@@ -83,11 +96,16 @@
         mErrorType = err;
     }
 
+    public IwlanError(@IwlanErrorType int err, @NonNull Exception exception) {
+        mErrorType = err;
+        mException = exception;
+    }
+
     /**
      * Sets the IwlanError based on the Exception: 1. IkeException is base the class for all IKE
      * exception ErrorType: IKE_GENERIC_EXCEPTION. 2. IkeProtocolException is for specific protocol
      * errors (like IKE notify error codes) ErrorType: IKE_PROTOCOL_EXCEPTION 3.
-     * IkeInternalException is just a wrapper for various exeptions that IKE lib may encounter
+     * IkeInternalException is just a wrapper for various exceptions that IKE lib may encounter
      * ErrorType: IKE_INTERNAL_IO_EXCEPTION if the Exception is instance of IOException ErrorType:
      * IKE_GENERIC_EXCEPTION for all the other.
      */
@@ -99,11 +117,10 @@
             IwlanErrorIkeIOException((IkeIOException) exception);
         } else if (exception instanceof IkeInternalException) {
             IwlanErrorIkeInternalException((IkeInternalException) exception);
-        } else if (exception instanceof IkeException) {
-            mErrorType = IKE_GENERIC_EXCEPTION;
-            mException = exception;
+        } else if (exception instanceof IkeNetworkLostException) {
+            IwlanErrorIkeNetworkLostException((IkeNetworkLostException) exception);
         } else {
-            mErrorType = UNKNOWN_EXCEPTION;
+            mErrorType = IKE_GENERIC_EXCEPTION;
             mException = exception;
         }
     }
@@ -127,6 +144,11 @@
         mException = exception;
     }
 
+    private void IwlanErrorIkeNetworkLostException(@NonNull IkeNetworkLostException exception) {
+        mErrorType = IKE_NETWORK_LOST_EXCEPTION;
+        mException = exception;
+    }
+
     public @IwlanErrorType int getErrorType() {
         return mErrorType;
     }
@@ -154,60 +176,31 @@
 
         switch (mErrorType) {
             case IKE_GENERIC_EXCEPTION:
-                sb.append("MSG: " + mException.getMessage() + "\n CAUSE: ");
+                sb.append("MSG: ").append(mException.getMessage()).append("\n CAUSE: ");
                 sb.append(mException.getCause());
                 break;
-            case UNKNOWN_EXCEPTION:
-                sb.append(mException.toString());
-                break;
             case IKE_PROTOCOL_EXCEPTION:
-                sb.append("ERR: " + ((IkeProtocolException) mException).getErrorType() + "\nDATA:");
+                sb.append("ERR: ")
+                        .append(((IkeProtocolException) mException).getErrorType())
+                        .append("\nDATA:");
                 for (byte b : ((IkeProtocolException) mException).getErrorData()) {
                     sb.append(String.format("%02x ", b));
                 }
                 break;
+            case IKE_NETWORK_LOST_EXCEPTION:
+                sb.append("ERR: ")
+                        .append(mException.getMessage())
+                        .append("\n CAUSE: ")
+                        .append(mException.getCause())
+                        .append("\n NETWORK: ")
+                        .append(((IkeNetworkLostException) mException).getNetwork());
+                break;
             default:
                 sb.append("-No Details-");
         }
         return sb.toString();
     }
 
-    /**
-     * Returns the error of the String. String that matches the name of the Error
-     *
-     * @param errorType string form of errorType
-     * @return IwlanErrorType
-     */
-    public static int getErrorType(String errorType) {
-        int ret = IwlanError.UNKNOWN_EXCEPTION;
-
-        // TODO: Add representation for Global error
-        switch (errorType) {
-            case "IKE_PROTOCOL_EXCEPTION":
-                ret = IwlanError.IKE_PROTOCOL_EXCEPTION;
-                break;
-            case "IKE_INTERNAL_IO_EXCEPTION":
-                ret = IwlanError.IKE_INTERNAL_IO_EXCEPTION;
-                break;
-            case "IKE_GENERIC_EXCEPTION":
-                ret = IwlanError.IKE_GENERIC_EXCEPTION;
-                break;
-            case "EPDG_SELECTOR_SERVER_SELECTION_FAILED":
-                ret = IwlanError.EPDG_SELECTOR_SERVER_SELECTION_FAILED;
-                break;
-            case "TUNNEL_TRANSFORM_FAILED":
-                ret = IwlanError.TUNNEL_TRANSFORM_FAILED;
-                break;
-            case "SIM_NOT_READY_EXCEPTION":
-                ret = IwlanError.SIM_NOT_READY_EXCEPTION;
-                break;
-            case "NETWORK_FAILURE":
-                ret = IwlanError.NETWORK_FAILURE;
-                break;
-        }
-        return ret;
-    }
-
     @Override
     public boolean equals(Object o) {
         if (!(o instanceof IwlanError)) {
@@ -231,4 +224,3 @@
         return ret;
     }
 }
-;
diff --git a/src/com/google/android/iwlan/IwlanEventListener.java b/src/com/google/android/iwlan/IwlanEventListener.java
index b4d38e2..f76c179 100644
--- a/src/com/google/android/iwlan/IwlanEventListener.java
+++ b/src/com/google/android/iwlan/IwlanEventListener.java
@@ -43,6 +43,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -58,7 +59,7 @@
 
     /** Airplane mode turned off or disabled. */
     public static final int APM_DISABLE_EVENT = 3;
-    /** Airplame mode turned on or enabled */
+    /** Airplane mode turned on or enabled */
     public static final int APM_ENABLE_EVENT = 4;
 
     /** Wifi AccessPoint changed. */
@@ -85,6 +86,15 @@
     /** On Cellinfo changed */
     public static final int CELLINFO_CHANGED_EVENT = 11;
 
+    /** On Call state changed */
+    public static final int CALL_STATE_CHANGED_EVENT = 12;
+
+    /* Events used and handled by IwlanDataService internally */
+    public static final int DATA_SERVICE_INTERNAL_EVENT_BASE = 100;
+
+    /* Events used and handled by IwlanNetworkService internally */
+    public static final int NETWORK_SERVICE_INTERNAL_EVENT_BASE = 200;
+
     @IntDef({
         CARRIER_CONFIG_CHANGED_EVENT,
         WIFI_DISABLE_EVENT,
@@ -96,27 +106,27 @@
         CROSS_SIM_CALLING_ENABLE_EVENT,
         CROSS_SIM_CALLING_DISABLE_EVENT,
         CARRIER_CONFIG_UNKNOWN_CARRIER_EVENT,
-        CELLINFO_CHANGED_EVENT
+        CELLINFO_CHANGED_EVENT,
+        CALL_STATE_CHANGED_EVENT
     })
-    @interface IwlanEventType {};
+    @interface IwlanEventType {}
 
-    private static String LOG_TAG = IwlanEventListener.class.getSimpleName();
+    private static final String LOG_TAG = IwlanEventListener.class.getSimpleName();
 
     private final String SUB_TAG;
 
     private static Boolean sIsAirplaneModeOn;
 
-    private static String sWifiSSID = new String();
+    private static String sWifiSSID = "";
 
-    private static Map<Integer, IwlanEventListener> mInstances = new ConcurrentHashMap<>();
+    private static final Map<Integer, IwlanEventListener> mInstances = new ConcurrentHashMap<>();
 
-    private Context mContext;
-    private int mSlotId;
+    private final Context mContext;
+    private final int mSlotId;
     private int mSubId;
     private Uri mCrossSimCallingUri;
     private Uri mWfcEnabledUri;
     private UserSettingContentObserver mUserSettingContentObserver;
-    private HandlerThread mUserSettingHandlerThread;
     private RadioInfoTelephonyCallback mTelephonyCallback;
 
     SparseArray<Set<Handler>> eventHandlers = new SparseArray<>();
@@ -128,6 +138,8 @@
 
         @Override
         public void onChange(boolean selfChange, Uri uri) {
+            Objects.requireNonNull(mCrossSimCallingUri, "CrossSimCallingUri must not be null");
+            Objects.requireNonNull(mWfcEnabledUri, "WfcEnabledUri must not be null");
             if (mCrossSimCallingUri.equals(uri)) {
                 notifyCurrentSetting(uri);
             } else if (mWfcEnabledUri.equals(uri)) {
@@ -137,26 +149,46 @@
     }
 
     private class RadioInfoTelephonyCallback extends TelephonyCallback
-            implements TelephonyCallback.CellInfoListener {
+            implements TelephonyCallback.CellInfoListener, TelephonyCallback.CallStateListener {
         @Override
         public void onCellInfoChanged(List<CellInfo> arrayCi) {
             Log.d(LOG_TAG, "Cellinfo changed");
 
-            int event = CELLINFO_CHANGED_EVENT;
             for (Map.Entry<Integer, IwlanEventListener> entry : mInstances.entrySet()) {
                 IwlanEventListener instance = entry.getValue();
                 if (instance != null) {
-                    instance.updateHandlers(event, arrayCi);
+                    instance.updateHandlers(arrayCi);
+                }
+            }
+        }
+
+        @Override
+        public void onCallStateChanged(int state) {
+            Log.d(
+                    LOG_TAG,
+                    "Call state changed to " + callStateToString(state) + " for slot " + mSlotId);
+
+            for (Map.Entry<Integer, IwlanEventListener> entry : mInstances.entrySet()) {
+                IwlanEventListener instance = entry.getValue();
+                if (instance != null) {
+                    instance.updateHandlers(CALL_STATE_CHANGED_EVENT, state);
                 }
             }
         }
     }
 
-    /** Returns IwlanEventListener instance */
+    /**
+     * Returns IwlanEventListener instance
+     */
     public static IwlanEventListener getInstance(@NonNull Context context, int slotId) {
         return mInstances.computeIfAbsent(slotId, k -> new IwlanEventListener(context, slotId));
     }
 
+    @VisibleForTesting
+    public static void resetAllInstances() {
+        mInstances.clear();
+    }
+
     /**
      * Adds handler for the list of events.
      *
@@ -219,7 +251,7 @@
      * Report a Broadcast received. Mainly used by IwlanBroadcastReceiver to report the following
      * broadcasts CARRIER_CONFIG_CHANGED
      *
-     * @param Intent intent
+     * @param intent intent
      */
     public static synchronized void onBroadcastReceived(Intent intent) {
         int event = UNKNOWN_EVENT;
@@ -235,11 +267,11 @@
                                 TelephonyManager.UNKNOWN_CARRIER_ID);
                 Context context = IwlanDataService.getContext();
                 if (slotId != SubscriptionManager.INVALID_SIM_SLOT_INDEX && context != null) {
-                    getInstance(context, slotId).onCarrierConfigChanged(slotId, carrierId);
+                    getInstance(context, slotId).onCarrierConfigChanged(carrierId);
                 }
                 break;
             case Intent.ACTION_AIRPLANE_MODE_CHANGED:
-                Boolean isAirplaneModeOn = new Boolean(intent.getBooleanExtra("state", false));
+                Boolean isAirplaneModeOn = intent.getBooleanExtra("state", false);
                 if (sIsAirplaneModeOn != null && sIsAirplaneModeOn.equals(isAirplaneModeOn)) {
                     // no change in apm state
                     break;
@@ -269,7 +301,7 @@
     /**
      * Broadcast WIFI_AP_CHANGED_EVENT if Wifi SSID changed after Wifi connected.
      *
-     * @param Context context
+     * @param context context
      */
     public static void onWifiConnected(Context context) {
         WifiManager wifiManager = context.getSystemService(WifiManager.class);
@@ -292,11 +324,10 @@
         // Wifi.
         if (sWifiSSID.length() > 0 && !sWifiSSID.equals(wifiSSID)) {
             Log.d(LOG_TAG, "Wifi SSID changed");
-            int event = WIFI_AP_CHANGED_EVENT;
             for (Map.Entry<Integer, IwlanEventListener> entry : mInstances.entrySet()) {
                 IwlanEventListener instance = entry.getValue();
                 if (instance != null) {
-                    instance.updateHandlers(event);
+                    instance.updateHandlers(WIFI_AP_CHANGED_EVENT);
                 }
             }
         }
@@ -307,7 +338,6 @@
      * Returns the Event id of the String. String that matches the name of the event
      *
      * @param event String form of the event.
-     * @param int form of the event.
      */
     public static int getUnthrottlingEvent(String event) {
         int ret = UNKNOWN_EVENT;
@@ -357,7 +387,7 @@
         sIsAirplaneModeOn = null;
     }
 
-    private void onCarrierConfigChanged(int slotId, int carrierId) {
+    private void onCarrierConfigChanged(int carrierId) {
         Log.d(SUB_TAG, "onCarrierConfigChanged");
         int subId = IwlanHelper.getSubId(mContext, mSlotId);
         if (subId != mSubId) {
@@ -379,7 +409,7 @@
     }
 
     /** Unregister ContentObserver. */
-    private void unregisterContentObserver() {
+    void unregisterContentObserver() {
         if (mUserSettingContentObserver != null) {
             mContext.getContentResolver().unregisterContentObserver(mUserSettingContentObserver);
         }
@@ -390,10 +420,10 @@
     /** Initiate ContentObserver if it is not created. And, register it with the current sub id. */
     private void registerContentObserver() {
         if (mUserSettingContentObserver == null) {
-            mUserSettingHandlerThread =
+            HandlerThread userSettingHandlerThread =
                     new HandlerThread(IwlanNetworkService.class.getSimpleName());
-            mUserSettingHandlerThread.start();
-            Looper looper = mUserSettingHandlerThread.getLooper();
+            userSettingHandlerThread.start();
+            Looper looper = userSettingHandlerThread.getLooper();
             Handler handler = new Handler(looper);
             mUserSettingContentObserver = new UserSettingContentObserver(handler);
         }
@@ -448,7 +478,12 @@
                 Log.e(SUB_TAG, "Could not find  ImsMmTelManager");
                 return;
             }
-            boolean wfcEnabled = imsMmTelManager.isVoWiFiSettingEnabled();
+            boolean wfcEnabled = false;
+            try {
+                wfcEnabled = imsMmTelManager.isVoWiFiSettingEnabled();
+            } catch (IllegalArgumentException e) {
+                Log.w(SUB_TAG, e.getMessage());
+            }
             int event = (wfcEnabled) ? WIFI_CALLING_ENABLE_EVENT : WIFI_CALLING_DISABLE_EVENT;
             getInstance(mContext, slotIndex).updateHandlers(event);
         } else {
@@ -461,9 +496,10 @@
         Log.d(SUB_TAG, "registerTelephonyCallback");
         TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
         telephonyManager =
-                telephonyManager.createForSubscriptionId(IwlanHelper.getSubId(mContext, mSlotId));
+                Objects.requireNonNull(telephonyManager)
+                        .createForSubscriptionId(IwlanHelper.getSubId(mContext, mSlotId));
         mTelephonyCallback = new RadioInfoTelephonyCallback();
-        telephonyManager.registerTelephonyCallback(r -> r.run(), mTelephonyCallback);
+        telephonyManager.registerTelephonyCallback(Runnable::run, mTelephonyCallback);
     }
 
     @VisibleForTesting
@@ -485,17 +521,40 @@
         if (eventHandlers.contains(event)) {
             Log.d(SUB_TAG, "Updating handlers for the event: " + event);
             for (Handler handler : eventHandlers.get(event)) {
-                handler.obtainMessage(event).sendToTarget();
+                handler.obtainMessage(event, mSlotId, 0 /* unused */).sendToTarget();
             }
         }
     }
 
-    private synchronized void updateHandlers(int event, List<CellInfo> arrayCi) {
+    private synchronized void updateHandlers(List<CellInfo> arrayCi) {
+        int event = IwlanEventListener.CELLINFO_CHANGED_EVENT;
         if (eventHandlers.contains(event)) {
             Log.d(SUB_TAG, "Updating handlers for the event: " + event);
             for (Handler handler : eventHandlers.get(event)) {
-                handler.obtainMessage(event, arrayCi).sendToTarget();
+                handler.obtainMessage(event, mSlotId, 0 /* unused */, arrayCi).sendToTarget();
             }
         }
     }
+
+    private synchronized void updateHandlers(int event, int state) {
+        if (eventHandlers.contains(event)) {
+            Log.d(SUB_TAG, "Updating handlers for the event: " + event);
+            for (Handler handler : eventHandlers.get(event)) {
+                handler.obtainMessage(event, mSlotId, state).sendToTarget();
+            }
+        }
+    }
+
+    private String callStateToString(int state) {
+        switch (state) {
+            case TelephonyManager.CALL_STATE_IDLE:
+                return "CALL_STATE_IDLE";
+            case TelephonyManager.CALL_STATE_RINGING:
+                return "CALL_STATE_RINGING";
+            case TelephonyManager.CALL_STATE_OFFHOOK:
+                return "CALL_STATE_OFFHOOK";
+            default:
+                return "Unknown Call State (" + state + ")";
+        }
+    }
 }
diff --git a/src/com/google/android/iwlan/IwlanHelper.java b/src/com/google/android/iwlan/IwlanHelper.java
index 7e39059..ade7189 100644
--- a/src/com/google/android/iwlan/IwlanHelper.java
+++ b/src/com/google/android/iwlan/IwlanHelper.java
@@ -27,6 +27,7 @@
 import android.net.LinkProperties;
 import android.net.Network;
 import android.os.PersistableBundle;
+import android.os.SystemClock;
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
@@ -109,30 +110,8 @@
         return info;
     }
 
-    public static List<InetAddress> getAddressesForNetwork(Network network, Context context) {
-        ConnectivityManager connectivityManager =
-                context.getSystemService(ConnectivityManager.class);
-        List<InetAddress> gatewayList = new ArrayList<>();
-        if (network != null) {
-            LinkProperties linkProperties = connectivityManager.getLinkProperties(network);
-            if (linkProperties != null) {
-                for (LinkAddress linkAddr : linkProperties.getLinkAddresses()) {
-                    InetAddress inetAddr = linkAddr.getAddress();
-                    // skip linklocal and loopback addresses
-                    if (!inetAddr.isLoopbackAddress() && !inetAddr.isLinkLocalAddress()) {
-                        gatewayList.add(inetAddr);
-                    }
-                }
-                if (linkProperties.getNat64Prefix() != null) {
-                    mNat64Prefix = linkProperties.getNat64Prefix();
-                }
-            }
-        }
-        return gatewayList;
-    }
-
-    public static List<InetAddress> getStackedAddressesForNetwork(
-            Network network, Context context) {
+    // Retrieves all IP addresses for this Network, including stacked IPv4 link addresses.
+    public static List<InetAddress> getAllAddressesForNetwork(Network network, Context context) {
         ConnectivityManager connectivityManager =
                 context.getSystemService(ConnectivityManager.class);
         List<InetAddress> gatewayList = new ArrayList<>();
@@ -141,10 +120,14 @@
             if (linkProperties != null) {
                 for (LinkAddress linkAddr : linkProperties.getAllLinkAddresses()) {
                     InetAddress inetAddr = linkAddr.getAddress();
-                    if ((inetAddr instanceof Inet4Address)) {
+                    // skip linklocal and loopback addresses
+                    if (!inetAddr.isLoopbackAddress() && !inetAddr.isLinkLocalAddress()) {
                         gatewayList.add(inetAddr);
                     }
                 }
+                if (linkProperties.getNat64Prefix() != null) {
+                    mNat64Prefix = linkProperties.getNat64Prefix();
+                }
             }
         }
         return gatewayList;
@@ -161,22 +144,24 @@
     }
 
     public static boolean hasIpv6Address(List<InetAddress> localAddresses) {
-        for (InetAddress address : localAddresses) {
-            if (address instanceof Inet6Address) {
-                return true;
+        if (localAddresses != null) {
+            for (InetAddress address : localAddresses) {
+                if (address instanceof Inet6Address) {
+                    return true;
+                }
             }
         }
-
         return false;
     }
 
     public static boolean hasIpv4Address(List<InetAddress> localAddresses) {
-        for (InetAddress address : localAddresses) {
-            if (address instanceof Inet4Address) {
-                return true;
+        if (localAddresses != null) {
+            for (InetAddress address : localAddresses) {
+                if (address instanceof Inet4Address) {
+                    return true;
+                }
             }
         }
-
         return false;
     }
 
@@ -313,4 +298,9 @@
             }
         }
     }
+
+    static long elapsedRealtime() {
+        /*Returns milliseconds since boot, including time spent in sleep.*/
+        return SystemClock.elapsedRealtime();
+    }
 }
diff --git a/src/com/google/android/iwlan/IwlanNetworkService.java b/src/com/google/android/iwlan/IwlanNetworkService.java
index 6adbaff..00f0907 100644
--- a/src/com/google/android/iwlan/IwlanNetworkService.java
+++ b/src/com/google/android/iwlan/IwlanNetworkService.java
@@ -25,11 +25,17 @@
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.NetworkSpecifier;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.TransportInfo;
+import android.net.vcn.VcnTransportInfo;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.support.annotation.NonNull;
 import android.telephony.AccessNetworkConstants;
 import android.telephony.NetworkRegistrationInfo;
 import android.telephony.NetworkService;
@@ -41,28 +47,42 @@
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
 
 public class IwlanNetworkService extends NetworkService {
     private static final String TAG = IwlanNetworkService.class.getSimpleName();
-    private Context mContext;
+    private static Context mContext;
     private IwlanNetworkMonitorCallback mNetworkMonitorCallback;
     private IwlanOnSubscriptionsChangedListener mSubsChangeListener;
-    private HandlerThread mNetworkCallbackHandlerThread;
+    private Handler mIwlanNetworkServiceHandler;
+    private HandlerThread mIwlanNetworkServiceHandlerThread;
     private static boolean sNetworkConnected;
-    private static List<IwlanNetworkServiceProvider> sIwlanNetworkServiceProviderList =
-            new ArrayList<IwlanNetworkServiceProvider>();
+    private static final Map<Integer, IwlanNetworkServiceProvider> sIwlanNetworkServiceProviders =
+            new ConcurrentHashMap<>();
+    private static final int INVALID_SUB_ID = -1;
+
+    // The current subscription with the active internet PDN. Need not be the default data sub.
+    // If internet is over WiFi, this value will be INVALID_SUB_ID.
+    private static int mConnectedDataSub = INVALID_SUB_ID;
+
+    private static final int EVENT_BASE = IwlanEventListener.NETWORK_SERVICE_INTERNAL_EVENT_BASE;
+    private static final int EVENT_NETWORK_REGISTRATION_INFO_REQUEST = EVENT_BASE;
+    private static final int EVENT_CREATE_NETWORK_SERVICE_PROVIDER = EVENT_BASE + 1;
+    private static final int EVENT_REMOVE_NETWORK_SERVICE_PROVIDER = EVENT_BASE + 2;
 
     @VisibleForTesting
     enum Transport {
         UNSPECIFIED_NETWORK,
         MOBILE,
-        WIFI;
+        WIFI
     }
 
     private static Transport sDefaultDataTransport = Transport.UNSPECIFIED_NETWORK;
 
+    // This callback runs in the same thread as IwlanNetworkServiceHandler
     final class IwlanNetworkMonitorCallback extends ConnectivityManager.NetworkCallback {
         /** Called when the framework connects and has declared a new network ready for use. */
         @Override
@@ -72,10 +92,11 @@
 
         /**
          * Called when the network is about to be lost, typically because there are no outstanding
-         * requests left for it. This may be paired with a {@link NetworkCallback#onAvailable} call
-         * with the new replacement network for graceful handover. This method is not guaranteed to
-         * be called before {@link NetworkCallback#onLost} is called, for example in case a network
-         * is suddenly disconnected.
+         * requests left for it. This may be paired with a {@link
+         * ConnectivityManager.NetworkCallback#onAvailable} call with the new replacement network
+         * for graceful handover. This method is not guaranteed to be called before {@link
+         * ConnectivityManager.NetworkCallback#onLost} is called, for example in case a network is
+         * suddenly disconnected.
          */
         @Override
         public void onLosing(Network network, int maxMsToLive) {
@@ -89,6 +110,7 @@
         @Override
         public void onLost(Network network) {
             Log.d(TAG, "onLost: " + network);
+            IwlanNetworkService.setConnectedDataSub(INVALID_SUB_ID);
             IwlanNetworkService.setNetworkConnected(false, Transport.UNSPECIFIED_NETWORK);
         }
 
@@ -113,9 +135,12 @@
             Log.d(TAG, "onCapabilitiesChanged: " + network);
             if (networkCapabilities != null) {
                 if (networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
+                    IwlanNetworkService.setConnectedDataSub(
+                            getConnectedDataSub(networkCapabilities));
                     IwlanNetworkService.setNetworkConnected(
                             true, IwlanNetworkService.Transport.MOBILE);
                 } else if (networkCapabilities.hasTransport(TRANSPORT_WIFI)) {
+                    IwlanNetworkService.setConnectedDataSub(INVALID_SUB_ID);
                     IwlanNetworkService.setNetworkConnected(
                             true, IwlanNetworkService.Transport.WIFI);
                 } else {
@@ -128,12 +153,12 @@
     final class IwlanOnSubscriptionsChangedListener
             extends SubscriptionManager.OnSubscriptionsChangedListener {
         /**
-         * Callback invoked when there is any change to any SubscriptionInfo. Typically this method
+         * Callback invoked when there is any change to any SubscriptionInfo. Typically, this method
          * invokes {@link SubscriptionManager#getActiveSubscriptionInfoList}
          */
         @Override
         public void onSubscriptionsChanged() {
-            for (IwlanNetworkServiceProvider np : sIwlanNetworkServiceProviderList) {
+            for (IwlanNetworkServiceProvider np : sIwlanNetworkServiceProviders.values()) {
                 np.subscriptionChanged();
             }
         }
@@ -144,45 +169,6 @@
         private final IwlanNetworkService mIwlanNetworkService;
         private final String SUB_TAG;
         private boolean mIsSubActive = false;
-        private HandlerThread mHandlerThread;
-        private Handler mHandler;
-
-        private final class NSPHandler extends Handler {
-            private final String TAG =
-                    IwlanNetworkService.class.getSimpleName()
-                            + NSPHandler.class.getSimpleName()
-                            + "["
-                            + getSlotIndex()
-                            + "]";
-
-            @Override
-            public void handleMessage(Message msg) {
-                Log.d(TAG, "msg.what = " + msg.what);
-                switch (msg.what) {
-                    case IwlanEventListener.CROSS_SIM_CALLING_ENABLE_EVENT:
-                        Log.d(TAG, "CROSS_SIM_CALLING_ENABLE_EVENT");
-                        notifyNetworkRegistrationInfoChanged();
-                        break;
-                    case IwlanEventListener.CROSS_SIM_CALLING_DISABLE_EVENT:
-                        Log.d(TAG, "CROSS_SIM_CALLING_DISABLE_EVENT");
-                        notifyNetworkRegistrationInfoChanged();
-                        break;
-                    default:
-                        Log.d(TAG, "Unknown message received!");
-                        break;
-                }
-            }
-
-            NSPHandler(Looper looper) {
-                super(looper);
-            }
-        }
-
-        Looper getLooper() {
-            mHandlerThread = new HandlerThread("NSPHandlerThread");
-            mHandlerThread.start();
-            return mHandlerThread.getLooper();
-        }
 
         /**
          * Constructor
@@ -195,53 +181,22 @@
             mIwlanNetworkService = iwlanNetworkService;
 
             // Register IwlanEventListener
-            initHandler();
             List<Integer> events = new ArrayList<Integer>();
             events.add(IwlanEventListener.CROSS_SIM_CALLING_ENABLE_EVENT);
             events.add(IwlanEventListener.CROSS_SIM_CALLING_DISABLE_EVENT);
-            IwlanEventListener.getInstance(mContext, slotIndex).addEventListener(events, mHandler);
-        }
-
-        void initHandler() {
-            mHandler = new NSPHandler(getLooper());
+            IwlanEventListener.getInstance(mContext, slotIndex)
+                    .addEventListener(events, getIwlanNetworkServiceHandler());
         }
 
         @Override
         public void requestNetworkRegistrationInfo(int domain, NetworkServiceCallback callback) {
-            if (callback == null) {
-                Log.d(SUB_TAG, "Error: callback is null. returning");
-                return;
-            }
-            if (domain != NetworkRegistrationInfo.DOMAIN_PS) {
-                callback.onRequestNetworkRegistrationInfoComplete(
-                        NetworkServiceCallback.RESULT_ERROR_UNSUPPORTED, null);
-                return;
-            }
-
-            NetworkRegistrationInfo.Builder nriBuilder = new NetworkRegistrationInfo.Builder();
-            nriBuilder
-                    .setAvailableServices(Arrays.asList(NetworkRegistrationInfo.SERVICE_TYPE_DATA))
-                    .setTransportType(AccessNetworkConstants.TRANSPORT_TYPE_WLAN)
-                    .setEmergencyOnly(!mIsSubActive)
-                    .setDomain(NetworkRegistrationInfo.DOMAIN_PS);
-
-            if (!IwlanNetworkService.isNetworkConnected(
-                    IwlanHelper.isDefaultDataSlot(mContext, getSlotIndex()),
-                    IwlanHelper.isCrossSimCallingEnabled(mContext, getSlotIndex()))) {
-                nriBuilder
-                        .setRegistrationState(
-                                NetworkRegistrationInfo.REGISTRATION_STATE_NOT_REGISTERED_SEARCHING)
-                        .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_UNKNOWN);
-                Log.d(SUB_TAG, "reg state REGISTRATION_STATE_NOT_REGISTERED_SEARCHING");
-            } else {
-                nriBuilder
-                        .setRegistrationState(NetworkRegistrationInfo.REGISTRATION_STATE_HOME)
-                        .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_IWLAN);
-                Log.d(SUB_TAG, "reg state REGISTRATION_STATE_HOME");
-            }
-
-            callback.onRequestNetworkRegistrationInfoComplete(
-                    NetworkServiceCallback.RESULT_SUCCESS, nriBuilder.build());
+            getIwlanNetworkServiceHandler()
+                    .sendMessage(
+                            getIwlanNetworkServiceHandler()
+                                    .obtainMessage(
+                                            EVENT_NETWORK_REGISTRATION_INFO_REQUEST,
+                                            new NetworkRegistrationInfoRequestData(
+                                                    domain, callback, this)));
         }
 
         /**
@@ -252,17 +207,16 @@
         @Override
         public void close() {
             mIwlanNetworkService.removeNetworkServiceProvider(this);
-            IwlanEventListener.getInstance(mContext, getSlotIndex()).removeEventListener(mHandler);
-            mHandlerThread.quit();
+            IwlanEventListener.getInstance(mContext, getSlotIndex())
+                    .removeEventListener(getIwlanNetworkServiceHandler());
         }
 
         @VisibleForTesting
         void subscriptionChanged() {
-            boolean subActive = false;
-            SubscriptionManager sm = SubscriptionManager.from(mContext);
-            if (sm.getActiveSubscriptionInfoForSimSlotIndex(getSlotIndex()) != null) {
-                subActive = true;
-            }
+            boolean subActive =
+                    getSubscriptionManager()
+                                    .getActiveSubscriptionInfoForSimSlotIndex(getSlotIndex())
+                            != null;
             if (subActive == mIsSubActive) {
                 return;
             }
@@ -277,6 +231,122 @@
         }
     }
 
+    private final class IwlanNetworkServiceHandler extends Handler {
+        private final String TAG = IwlanNetworkServiceHandler.class.getSimpleName();
+
+        @Override
+        public void handleMessage(Message msg) {
+            Log.d(TAG, "msg.what = " + eventToString(msg.what));
+
+            IwlanNetworkServiceProvider iwlanNetworkServiceProvider;
+            int slotId;
+
+            switch (msg.what) {
+                case IwlanEventListener.CROSS_SIM_CALLING_ENABLE_EVENT:
+                case IwlanEventListener.CROSS_SIM_CALLING_DISABLE_EVENT:
+                    iwlanNetworkServiceProvider = getNetworkServiceProvider(msg.arg1);
+                    iwlanNetworkServiceProvider.notifyNetworkRegistrationInfoChanged();
+                    break;
+
+                case EVENT_NETWORK_REGISTRATION_INFO_REQUEST:
+                    NetworkRegistrationInfoRequestData networkRegistrationInfoRequestData =
+                            (NetworkRegistrationInfoRequestData) msg.obj;
+                    int domain = networkRegistrationInfoRequestData.mDomain;
+                    NetworkServiceCallback callback = networkRegistrationInfoRequestData.mCallback;
+                    iwlanNetworkServiceProvider =
+                            networkRegistrationInfoRequestData.mIwlanNetworkServiceProvider;
+
+                    if (callback == null) {
+                        Log.d(TAG, "Error: callback is null. returning");
+                        return;
+                    }
+                    if (domain != NetworkRegistrationInfo.DOMAIN_PS) {
+                        callback.onRequestNetworkRegistrationInfoComplete(
+                                NetworkServiceCallback.RESULT_ERROR_UNSUPPORTED, null);
+                        return;
+                    }
+
+                    NetworkRegistrationInfo.Builder nriBuilder =
+                            new NetworkRegistrationInfo.Builder();
+                    nriBuilder
+                            .setAvailableServices(
+                                    List.of(NetworkRegistrationInfo.SERVICE_TYPE_DATA))
+                            .setTransportType(AccessNetworkConstants.TRANSPORT_TYPE_WLAN)
+                            .setEmergencyOnly(!iwlanNetworkServiceProvider.mIsSubActive)
+                            .setDomain(NetworkRegistrationInfo.DOMAIN_PS);
+
+                    slotId = iwlanNetworkServiceProvider.getSlotIndex();
+                    if (!IwlanNetworkService.isNetworkConnected(
+                            isActiveDataOnOtherSub(slotId),
+                            IwlanHelper.isCrossSimCallingEnabled(mContext, slotId))) {
+                        nriBuilder
+                                .setRegistrationState(
+                                        NetworkRegistrationInfo
+                                                .REGISTRATION_STATE_NOT_REGISTERED_SEARCHING)
+                                .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_UNKNOWN);
+                        Log.d(
+                                TAG + "[" + slotId + "]",
+                                ": reg state" + " REGISTRATION_STATE_NOT_REGISTERED_SEARCHING");
+                    } else {
+                        nriBuilder
+                                .setRegistrationState(
+                                        NetworkRegistrationInfo.REGISTRATION_STATE_HOME)
+                                .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_IWLAN);
+                        Log.d(TAG + "[" + slotId + "]", ": reg state REGISTRATION_STATE_HOME");
+                    }
+
+                    callback.onRequestNetworkRegistrationInfoComplete(
+                            NetworkServiceCallback.RESULT_SUCCESS, nriBuilder.build());
+                    break;
+
+                case EVENT_CREATE_NETWORK_SERVICE_PROVIDER:
+                    iwlanNetworkServiceProvider = (IwlanNetworkServiceProvider) msg.obj;
+
+                    if (sIwlanNetworkServiceProviders.isEmpty()) {
+                        initCallback();
+                    }
+
+                    addIwlanNetworkServiceProvider(iwlanNetworkServiceProvider);
+                    break;
+
+                case EVENT_REMOVE_NETWORK_SERVICE_PROVIDER:
+                    iwlanNetworkServiceProvider = (IwlanNetworkServiceProvider) msg.obj;
+                    slotId = iwlanNetworkServiceProvider.getSlotIndex();
+                    IwlanNetworkServiceProvider nsp = sIwlanNetworkServiceProviders.remove(slotId);
+                    if (nsp == null) {
+                        Log.w(
+                                TAG + "[" + slotId + "]",
+                                "No NetworkServiceProvider exists for slot!");
+                        return;
+                    }
+                    if (sIwlanNetworkServiceProviders.isEmpty()) {
+                        deinitCallback();
+                    }
+                    break;
+
+                default:
+                    throw new IllegalStateException("Unexpected value: " + msg.what);
+            }
+        }
+
+        IwlanNetworkServiceHandler(Looper looper) {
+            super(looper);
+        }
+    }
+
+    private static final class NetworkRegistrationInfoRequestData {
+        final int mDomain;
+        final NetworkServiceCallback mCallback;
+        final IwlanNetworkServiceProvider mIwlanNetworkServiceProvider;
+
+        private NetworkRegistrationInfoRequestData(
+                int domain, NetworkServiceCallback callback, IwlanNetworkServiceProvider nsp) {
+            mDomain = domain;
+            mCallback = callback;
+            mIwlanNetworkServiceProvider = nsp;
+        }
+    }
+
     /**
      * Create the instance of {@link NetworkServiceProvider}. Network service provider must override
      * this method to facilitate the creation of {@link NetworkServiceProvider} instances. The
@@ -292,37 +362,43 @@
 
         // TODO: validity check slot index
 
-        if (sIwlanNetworkServiceProviderList.isEmpty()) {
-            // first invocation
-            mNetworkCallbackHandlerThread =
-                    new HandlerThread(IwlanNetworkService.class.getSimpleName());
-            mNetworkCallbackHandlerThread.start();
-            Looper looper = mNetworkCallbackHandlerThread.getLooper();
-            Handler handler = new Handler(looper);
-
-            // register for default network callback
-            ConnectivityManager connectivityManager =
-                    mContext.getSystemService(ConnectivityManager.class);
-            mNetworkMonitorCallback = new IwlanNetworkMonitorCallback();
-            connectivityManager.registerDefaultNetworkCallback(mNetworkMonitorCallback, handler);
-            Log.d(TAG, "Registered with Connectivity Service");
-
-            /* register with subscription manager */
-            SubscriptionManager subscriptionManager =
-                    mContext.getSystemService(SubscriptionManager.class);
-            mSubsChangeListener = new IwlanOnSubscriptionsChangedListener();
-            subscriptionManager.addOnSubscriptionsChangedListener(mSubsChangeListener);
-            Log.d(TAG, "Registered with Subscription Service");
-        }
-
         IwlanNetworkServiceProvider np = new IwlanNetworkServiceProvider(slotIndex, this);
-        sIwlanNetworkServiceProviderList.add(np);
+        getIwlanNetworkServiceHandler()
+                .sendMessage(
+                        getIwlanNetworkServiceHandler()
+                                .obtainMessage(EVENT_CREATE_NETWORK_SERVICE_PROVIDER, np));
         return np;
     }
 
-    public static boolean isNetworkConnected(boolean isDds, boolean isCstEnabled) {
-        if (!isDds && isCstEnabled) {
-            // Only Non-DDS sub with CST enabled, can use any transport.
+    static void setConnectedDataSub(int subId) {
+        mConnectedDataSub = subId;
+    }
+
+    static int getConnectedDataSub(NetworkCapabilities networkCapabilities) {
+        int connectedDataSub = INVALID_SUB_ID;
+        NetworkSpecifier specifier = networkCapabilities.getNetworkSpecifier();
+        TransportInfo transportInfo = networkCapabilities.getTransportInfo();
+
+        if (specifier instanceof TelephonyNetworkSpecifier) {
+            connectedDataSub = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
+        } else if (transportInfo instanceof VcnTransportInfo) {
+            connectedDataSub = ((VcnTransportInfo) transportInfo).getSubId();
+        }
+        return connectedDataSub;
+    }
+
+    static boolean isActiveDataOnOtherSub(int slotId) {
+        int subId = IwlanHelper.getSubId(mContext, slotId);
+        return mConnectedDataSub != INVALID_SUB_ID && subId != mConnectedDataSub;
+    }
+
+    public static boolean isNetworkConnected(boolean isActiveDataOnOtherSub, boolean isCstEnabled) {
+        if (isActiveDataOnOtherSub && isCstEnabled) {
+            // For cross-SIM IWLAN (Transport.MOBILE), an active data PDN must be maintained on the
+            // other subscription.
+            if (sNetworkConnected && (sDefaultDataTransport != Transport.MOBILE)) {
+                Log.e(TAG, "Internet is on other slot, but default transport is not MOBILE!");
+            }
             return sNetworkConnected;
         } else {
             // For all other cases, only wifi transport can be used.
@@ -340,32 +416,56 @@
         sNetworkConnected = connected;
         sDefaultDataTransport = transport;
 
-        for (IwlanNetworkServiceProvider np : sIwlanNetworkServiceProviderList) {
+        for (IwlanNetworkServiceProvider np : sIwlanNetworkServiceProviders.values()) {
             np.notifyNetworkRegistrationInfoChanged();
         }
     }
 
-    public void removeNetworkServiceProvider(IwlanNetworkServiceProvider np) {
-        sIwlanNetworkServiceProviderList.remove(np);
-        if (sIwlanNetworkServiceProviderList.isEmpty()) {
-            // deinit network related stuff
-            ConnectivityManager connectivityManager =
-                    mContext.getSystemService(ConnectivityManager.class);
-            connectivityManager.unregisterNetworkCallback(mNetworkMonitorCallback);
-            mNetworkCallbackHandlerThread.quit(); // no need to quitSafely
-            mNetworkCallbackHandlerThread = null;
-            mNetworkMonitorCallback = null;
-
-            // deinit subscription manager related stuff
-            SubscriptionManager subscriptionManager =
-                    mContext.getSystemService(SubscriptionManager.class);
-            subscriptionManager.removeOnSubscriptionsChangedListener(mSubsChangeListener);
-            mSubsChangeListener = null;
+    void addIwlanNetworkServiceProvider(IwlanNetworkServiceProvider np) {
+        int slotIndex = np.getSlotIndex();
+        if (sIwlanNetworkServiceProviders.containsKey(slotIndex)) {
+            throw new IllegalStateException(
+                    "NetworkServiceProvider already exists for slot " + slotIndex);
         }
+        sIwlanNetworkServiceProviders.put(slotIndex, np);
     }
 
-    Context getContext() {
-        return getApplicationContext();
+    public void removeNetworkServiceProvider(IwlanNetworkServiceProvider np) {
+        getIwlanNetworkServiceHandler()
+                .sendMessage(
+                        getIwlanNetworkServiceHandler()
+                                .obtainMessage(EVENT_REMOVE_NETWORK_SERVICE_PROVIDER, np));
+    }
+
+    void initCallback() {
+        // register for default network callback
+        mNetworkMonitorCallback = new IwlanNetworkMonitorCallback();
+        getConnectivityManager()
+                .registerSystemDefaultNetworkCallback(
+                        mNetworkMonitorCallback, getIwlanNetworkServiceHandler());
+        Log.d(TAG, "Registered with Connectivity Service");
+
+        /* register with subscription manager */
+        mSubsChangeListener = new IwlanOnSubscriptionsChangedListener();
+        getSubscriptionManager()
+                .addOnSubscriptionsChangedListener(
+                        new HandlerExecutor(getIwlanNetworkServiceHandler()), mSubsChangeListener);
+        Log.d(TAG, "Registered with Subscription Service");
+    }
+
+    void deinitCallback() {
+        // deinit network related stuff
+        getConnectivityManager().unregisterNetworkCallback(mNetworkMonitorCallback);
+        mNetworkMonitorCallback = null;
+
+        // deinit subscription manager related stuff
+        getSubscriptionManager().removeOnSubscriptionsChangedListener(mSubsChangeListener);
+        mSubsChangeListener = null;
+        if (mIwlanNetworkServiceHandlerThread != null) {
+            mIwlanNetworkServiceHandlerThread.quit();
+            mIwlanNetworkServiceHandlerThread = null;
+        }
+        mIwlanNetworkServiceHandler = null;
     }
 
     @VisibleForTesting
@@ -375,12 +475,45 @@
 
     @VisibleForTesting
     IwlanNetworkServiceProvider getNetworkServiceProvider(int slotIndex) {
-        for (IwlanNetworkServiceProvider np : sIwlanNetworkServiceProviderList) {
-            if (np.getSlotIndex() == slotIndex) {
-                return np;
-            }
+        return sIwlanNetworkServiceProviders.get(slotIndex);
+    }
+
+    @VisibleForTesting
+    IwlanNetworkMonitorCallback getNetworkMonitorCallback() {
+        return mNetworkMonitorCallback;
+    }
+
+    @VisibleForTesting
+    @NonNull
+    Handler getIwlanNetworkServiceHandler() {
+        if (mIwlanNetworkServiceHandler == null) {
+            mIwlanNetworkServiceHandler = new IwlanNetworkServiceHandler(getLooper());
         }
-        return null;
+        return mIwlanNetworkServiceHandler;
+    }
+
+    @VisibleForTesting
+    Looper getLooper() {
+        mIwlanNetworkServiceHandlerThread = new HandlerThread("IwlanNetworkServiceThread");
+        mIwlanNetworkServiceHandlerThread.start();
+        return mIwlanNetworkServiceHandlerThread.getLooper();
+    }
+
+    private static String eventToString(int event) {
+        switch (event) {
+            case IwlanEventListener.CROSS_SIM_CALLING_ENABLE_EVENT:
+                return "CROSS_SIM_CALLING_ENABLE_EVENT";
+            case IwlanEventListener.CROSS_SIM_CALLING_DISABLE_EVENT:
+                return "CROSS_SIM_CALLING_DISABLE_EVENT";
+            case EVENT_NETWORK_REGISTRATION_INFO_REQUEST:
+                return "EVENT_NETWORK_REGISTRATION_INFO_REQUEST";
+            case EVENT_CREATE_NETWORK_SERVICE_PROVIDER:
+                return "EVENT_CREATE_NETWORK_SERVICE_PROVIDER";
+            case EVENT_REMOVE_NETWORK_SERVICE_PROVIDER:
+                return "EVENT_REMOVE_NETWORK_SERVICE_PROVIDER";
+            default:
+                return "Unknown(" + event + ")";
+        }
     }
 
     @Override
@@ -393,4 +526,14 @@
         Log.d(TAG, "IwlanNetworkService onBind");
         return super.onBind(intent);
     }
+
+    @NonNull
+    ConnectivityManager getConnectivityManager() {
+        return Objects.requireNonNull(mContext.getSystemService(ConnectivityManager.class));
+    }
+
+    @NonNull
+    SubscriptionManager getSubscriptionManager() {
+        return Objects.requireNonNull(mContext.getSystemService(SubscriptionManager.class));
+    }
 }
diff --git a/src/com/google/android/iwlan/IwlanTunnelMetricsImpl.java b/src/com/google/android/iwlan/IwlanTunnelMetricsImpl.java
new file mode 100644
index 0000000..3c04de5
--- /dev/null
+++ b/src/com/google/android/iwlan/IwlanTunnelMetricsImpl.java
@@ -0,0 +1,45 @@
+/*
+ * 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.google.android.iwlan;
+
+import android.os.Handler;
+import com.google.android.iwlan.IwlanDataService.IwlanDataServiceProvider;
+
+public class IwlanTunnelMetricsImpl implements TunnelMetricsInterface {
+    IwlanDataServiceProvider mDataServiceProvider;
+    Handler mIwlanDataServiceHandler;
+
+    private static final int EVENT_BASE = IwlanEventListener.DATA_SERVICE_INTERNAL_EVENT_BASE;
+    private static final int EVENT_TUNNEL_OPENED_METRICS = EVENT_BASE + 8;
+    private static final int EVENT_TUNNEL_CLOSED_METRICS = EVENT_BASE + 9;
+
+    public IwlanTunnelMetricsImpl(IwlanDataServiceProvider dsp, Handler handler) {
+        mDataServiceProvider = dsp;
+        mIwlanDataServiceHandler = handler;
+    }
+
+    public void onOpened(OnOpenedMetrics metricsData) {
+        metricsData.setIwlanDataServiceProvider(mDataServiceProvider);
+        mIwlanDataServiceHandler.sendMessage(
+                mIwlanDataServiceHandler.obtainMessage(EVENT_TUNNEL_OPENED_METRICS, metricsData));
+    }
+
+    public void onClosed(OnClosedMetrics metricsData) {
+        metricsData.setIwlanDataServiceProvider(mDataServiceProvider);
+        mIwlanDataServiceHandler.sendMessage(
+                mIwlanDataServiceHandler.obtainMessage(EVENT_TUNNEL_CLOSED_METRICS, metricsData));
+    }
+}
diff --git a/src/com/google/android/iwlan/TunnelMetricsInterface.java b/src/com/google/android/iwlan/TunnelMetricsInterface.java
new file mode 100644
index 0000000..6e79688
--- /dev/null
+++ b/src/com/google/android/iwlan/TunnelMetricsInterface.java
@@ -0,0 +1,141 @@
+/*
+ * 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.google.android.iwlan;
+
+import java.net.InetAddress;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.google.android.iwlan.IwlanDataService.IwlanDataServiceProvider;
+
+import java.util.Objects;
+
+public interface TunnelMetricsInterface {
+    /** Called for logging the tunnel is opened. */
+    void onOpened(OnOpenedMetrics metricsData);
+    /** Called for logging the tunnel is closed or bring up failed. */
+    void onClosed(OnClosedMetrics metricsData);
+
+    static class TunnelMetricsData {
+        private final String mApnName;
+        private final String mEpdgServerAddress;
+        private final int mEpdgServerSelectionDuration;
+        private final int mIkeTunnelEstablishmentDuration;
+        private IwlanDataServiceProvider mIwlanDataServiceProvider;
+
+        protected TunnelMetricsData(Builder builder) {
+            this.mApnName = builder.mApnName;
+            this.mEpdgServerAddress = builder.mEpdgServerAddress;
+            this.mEpdgServerSelectionDuration = builder.mEpdgServerSelectionDuration;
+            this.mIkeTunnelEstablishmentDuration = builder.mIkeTunnelEstablishmentDuration;
+        }
+
+        @Nullable
+        public String getApnName() {
+            return mApnName;
+        }
+
+        @Nullable
+        public String getEpdgServerAddress() {
+            return mEpdgServerAddress;
+        }
+
+        public int getEpdgServerSelectionDuration() {
+            return mEpdgServerSelectionDuration;
+        }
+
+        public int getIkeTunnelEstablishmentDuration() {
+            return mIkeTunnelEstablishmentDuration;
+        }
+
+        public IwlanDataServiceProvider getIwlanDataServiceProvider() {
+            return mIwlanDataServiceProvider;
+        }
+
+        public void setIwlanDataServiceProvider(IwlanDataServiceProvider dsp) {
+            mIwlanDataServiceProvider = dsp;
+        }
+
+        public static class Builder<T extends Builder> {
+            @Nullable private String mApnName = null;
+            @Nullable private String mEpdgServerAddress = null;
+            private int mEpdgServerSelectionDuration = 0;
+            private int mIkeTunnelEstablishmentDuration = 0;
+
+            /** Default constructor for Builder. */
+            public Builder() {}
+
+            public T setApnName(@NonNull String apnName) {
+                mApnName = Objects.requireNonNull(apnName, "apnName must not be null");
+                return (T) this;
+            }
+
+            public T setEpdgServerAddress(InetAddress epdgAddress) {
+                mEpdgServerAddress = epdgAddress == null ? null : epdgAddress.getHostAddress();
+                return (T) this;
+            }
+
+            public T setEpdgServerSelectionDuration(int epdgServerSelectionDuration) {
+                mEpdgServerSelectionDuration = epdgServerSelectionDuration;
+                return (T) this;
+            }
+
+            public T setIkeTunnelEstablishmentDuration(int ikeTunnelEstablishmentDuration) {
+                mIkeTunnelEstablishmentDuration = ikeTunnelEstablishmentDuration;
+                return (T) this;
+            }
+
+            public TunnelMetricsData build() {
+                if (mApnName == null) {
+                    throw new IllegalArgumentException("Necessary parameter missing.");
+                }
+                return new TunnelMetricsData(this);
+            }
+        }
+    }
+
+    static class OnOpenedMetrics extends TunnelMetricsData {
+
+        protected OnOpenedMetrics(Builder builder) {
+            super(builder);
+        }
+
+        public static class Builder extends TunnelMetricsData.Builder<Builder> {
+
+            public Builder() {}
+
+            public OnOpenedMetrics build() {
+                return new OnOpenedMetrics(this);
+            }
+        }
+    }
+
+    static class OnClosedMetrics extends TunnelMetricsData {
+
+        protected OnClosedMetrics(Builder builder) {
+            super(builder);
+        }
+
+        public static class Builder extends TunnelMetricsData.Builder<Builder> {
+
+            public Builder() {}
+
+            public OnClosedMetrics build() {
+                return new OnClosedMetrics(this);
+            }
+        }
+    }
+}
diff --git a/src/com/google/android/iwlan/epdg/EpdgSelector.java b/src/com/google/android/iwlan/epdg/EpdgSelector.java
index 54b082f..4534b81 100644
--- a/src/com/google/android/iwlan/epdg/EpdgSelector.java
+++ b/src/com/google/android/iwlan/epdg/EpdgSelector.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.net.DnsResolver;
 import android.net.DnsResolver.DnsException;
+import android.net.InetAddresses;
 import android.net.Network;
 import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
@@ -34,6 +35,7 @@
 import android.telephony.CellInfoNr;
 import android.telephony.CellInfoTdscdma;
 import android.telephony.CellInfoWcdma;
+import android.telephony.DataFailCause;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
@@ -42,6 +44,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import com.google.android.iwlan.ErrorPolicyManager;
 import com.google.android.iwlan.IwlanError;
 import com.google.android.iwlan.IwlanHelper;
 import com.google.android.iwlan.epdg.NaptrDnsResolver.NaptrTarget;
@@ -51,32 +54,79 @@
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.util.*;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
 
 public class EpdgSelector {
     private static final String TAG = "EpdgSelector";
-    private Context mContext;
-    private int mSlotId;
-    private static ConcurrentHashMap<Integer, EpdgSelector> mSelectorInstances =
+    private final Context mContext;
+    private final int mSlotId;
+    private static final ConcurrentHashMap<Integer, EpdgSelector> mSelectorInstances =
             new ConcurrentHashMap<>();
     private int mV4PcoId = -1;
     private int mV6PcoId = -1;
     private byte[] mV4PcoData = null;
     private byte[] mV6PcoData = null;
+    @NonNull private final ErrorPolicyManager mErrorPolicyManager;
+
+    // The default DNS timeout in the DNS module is set to 5 seconds. To account for IPC overhead,
+    // IWLAN applies an internal timeout of 6 seconds, slightly longer than the default timeout
+    private static final long DNS_RESOLVER_TIMEOUT_DURATION_SEC = 6L;
+
+    private static final long PARALLEL_STATIC_RESOLUTION_TIMEOUT_DURATION_SEC = 6L;
+    private static final long PARALLEL_PLMN_RESOLUTION_TIMEOUT_DURATION_SEC = 20L;
+    private static final int NUM_EPDG_SELECTION_EXECUTORS = 2; // 1 each for normal selection, SOS.
+    private static final int MAX_EPDG_SELECTION_THREADS = 2; // 1 each for prefetch, tunnel bringup.
+    private static final int MAX_DNS_RESOLVER_THREADS = 25; // Do not expect > 25 FQDNs per carrier.
+    private static final String NO_DOMAIN = "NO_DOMAIN";
+
+    BlockingQueue<Runnable> dnsResolutionQueue =
+            new ArrayBlockingQueue<>(
+                    MAX_DNS_RESOLVER_THREADS
+                            * MAX_EPDG_SELECTION_THREADS
+                            * NUM_EPDG_SELECTION_EXECUTORS);
+
+    Executor mDnsResolutionExecutor =
+            new ThreadPoolExecutor(
+                    0, MAX_DNS_RESOLVER_THREADS, 60L, TimeUnit.SECONDS, dnsResolutionQueue);
+
+    ExecutorService mEpdgSelectionExecutor =
+            new ThreadPoolExecutor(
+                    0,
+                    MAX_EPDG_SELECTION_THREADS,
+                    60L,
+                    TimeUnit.SECONDS,
+                    new SynchronousQueue<Runnable>());
+    Future mDnsPrefetchFuture;
+
+    ExecutorService mSosEpdgSelectionExecutor =
+            new ThreadPoolExecutor(
+                    0,
+                    MAX_EPDG_SELECTION_THREADS,
+                    60L,
+                    TimeUnit.SECONDS,
+                    new SynchronousQueue<Runnable>());
+    Future mSosDnsPrefetchFuture;
 
     final Comparator<InetAddress> inetAddressComparator =
-            new Comparator<InetAddress>() {
-                @Override
-                public int compare(InetAddress ip1, InetAddress ip2) {
-                    if ((ip1 instanceof Inet4Address) && (ip2 instanceof Inet6Address)) {
-                        return -1;
-                    } else if ((ip1 instanceof Inet6Address) && (ip2 instanceof Inet4Address)) {
-                        return 1;
-                    } else {
-                        return 0;
-                    }
+            (ip1, ip2) -> {
+                if ((ip1 instanceof Inet4Address) && (ip2 instanceof Inet6Address)) {
+                    return -1;
+                } else if ((ip1 instanceof Inet6Address) && (ip2 instanceof Inet4Address)) {
+                    return 1;
+                } else {
+                    return 0;
                 }
             };
 
@@ -87,9 +137,16 @@
     @IntDef({PROTO_FILTER_IPV4, PROTO_FILTER_IPV6, PROTO_FILTER_IPV4V6})
     @interface ProtoFilter {}
 
+    public static final int IPV4_PREFERRED = 0;
+    public static final int IPV6_PREFERRED = 1;
+    public static final int SYSTEM_PREFERRED = 2;
+
+    @IntDef({IPV4_PREFERRED, IPV6_PREFERRED, SYSTEM_PREFERRED})
+    @interface EpdgAddressOrder {}
+
     public interface EpdgSelectorCallback {
         /*gives priority ordered list of addresses*/
-        void onServerListChanged(int transactionId, ArrayList<InetAddress> validIPList);
+        void onServerListChanged(int transactionId, List<InetAddress> validIPList);
 
         void onError(int transactionId, IwlanError error);
     }
@@ -98,6 +155,8 @@
     EpdgSelector(Context context, int slotId) {
         mContext = context;
         mSlotId = slotId;
+
+        mErrorPolicyManager = ErrorPolicyManager.getInstance(mContext, mSlotId);
     }
 
     public static EpdgSelector getSelectorInstance(Context context, int slotId) {
@@ -106,7 +165,12 @@
     }
 
     public boolean setPcoData(int pcoId, byte[] pcoData) {
-        Log.d(TAG, "onReceive PcoId:" + String.format("0x%04x", pcoId) + " PcoData:" + pcoData);
+        Log.d(
+                TAG,
+                "onReceive PcoId:"
+                        + String.format("0x%04x", pcoId)
+                        + " PcoData:"
+                        + Arrays.toString(pcoData));
 
         int PCO_ID_IPV6 =
                 IwlanHelper.getConfig(
@@ -143,69 +207,242 @@
         mV6PcoData = null;
     }
 
+    private CompletableFuture<Map.Entry<String, List<InetAddress>>> submitDnsResolverQuery(
+            String domainName, Network network, int queryType, Executor executor) {
+        CompletableFuture<Map.Entry<String, List<InetAddress>>> result = new CompletableFuture();
+
+        final DnsResolver.Callback<List<InetAddress>> cb =
+                new DnsResolver.Callback<List<InetAddress>>() {
+                    @Override
+                    public void onAnswer(@NonNull final List<InetAddress> answer, final int rcode) {
+                        if (rcode != 0) {
+                            Log.e(
+                                    TAG,
+                                    "DnsResolver Response Code = "
+                                            + rcode
+                                            + " for domain "
+                                            + domainName);
+                        }
+                        Map.Entry<String, List<InetAddress>> entry = Map.entry(domainName, answer);
+                        result.complete(entry);
+                    }
+
+                    @Override
+                    public void onError(@Nullable final DnsResolver.DnsException error) {
+                        Log.e(
+                                TAG,
+                                "Resolve DNS with error: " + error + " for domain: " + domainName);
+                        result.complete(null);
+                    }
+                };
+        DnsResolver.getInstance()
+                .query(network, domainName, queryType, DnsResolver.FLAG_EMPTY, executor, null, cb);
+        return result;
+    }
+
+    private List<InetAddress> v4v6ProtocolFilter(List<InetAddress> ipList, int filter) {
+        List<InetAddress> validIpList = new ArrayList<>();
+        for (InetAddress ipAddress : ipList) {
+            if (IwlanHelper.isIpv4EmbeddedIpv6Address(ipAddress)) {
+                continue;
+            }
+            switch (filter) {
+                case PROTO_FILTER_IPV4:
+                    if (ipAddress instanceof Inet4Address) {
+                        validIpList.add(ipAddress);
+                    }
+                    break;
+                case PROTO_FILTER_IPV6:
+                    if (ipAddress instanceof Inet6Address) {
+                        validIpList.add(ipAddress);
+                    }
+                    break;
+                case PROTO_FILTER_IPV4V6:
+                    validIpList.add(ipAddress);
+                    break;
+                default:
+                    Log.d(TAG, "Invalid ProtoFilter : " + filter);
+            }
+        }
+        return validIpList;
+    }
+
+    // Converts a list of CompletableFutures of type T into a single CompletableFuture containing a
+    // list of T. The resulting CompletableFuture waits for all futures to complete,
+    // even if any future throw an exception.
+    private <T> CompletableFuture<List<T>> allOf(List<CompletableFuture<T>> futuresList) {
+        CompletableFuture<Void> allFuturesResult =
+                CompletableFuture.allOf(
+                        futuresList.toArray(new CompletableFuture[futuresList.size()]));
+        return allFuturesResult.thenApply(
+                v ->
+                        futuresList.stream()
+                                .map(CompletableFuture::join)
+                                .filter(Objects::nonNull)
+                                .collect(Collectors.<T>toList()));
+    }
+
+    @VisibleForTesting
+    protected boolean hasIpv4Address(Network network) {
+        return IwlanHelper.hasIpv4Address(IwlanHelper.getAllAddressesForNetwork(network, mContext));
+    }
+
+    @VisibleForTesting
+    protected boolean hasIpv6Address(Network network) {
+        return IwlanHelper.hasIpv6Address(IwlanHelper.getAllAddressesForNetwork(network, mContext));
+    }
+
+    private void printParallelDnsResult(Map<String, List<InetAddress>> domainNameToIpAddresses) {
+        Log.d(TAG, "Parallel DNS resolution result:");
+        for (String domain : domainNameToIpAddresses.keySet()) {
+            Log.d(TAG, domain + ": " + domainNameToIpAddresses.get(domain));
+        }
+    }
+    /**
+     * Returns a list of unique IP addresses corresponding to the given domain names, in the same
+     * order of the input. Runs DNS resolution across parallel threads.
+     *
+     * @param domainNames Domain names for which DNS resolution needs to be performed.
+     * @param filter Selects for IPv4, IPv6 (or both) addresses from the resulting DNS records
+     * @param network {@link Network} Network on which to run the DNS query.
+     * @param timeout timeout in seconds.
+     * @return List of unique IP addresses corresponding to the domainNames.
+     */
+    private LinkedHashMap<String, List<InetAddress>> getIP(
+            List<String> domainNames, int filter, Network network, long timeout) {
+        // LinkedHashMap preserves insertion order (and hence priority) of domain names passed in.
+        LinkedHashMap<String, List<InetAddress>> domainNameToIpAddr = new LinkedHashMap<>();
+
+        List<CompletableFuture<Map.Entry<String, List<InetAddress>>>> futuresList =
+                new ArrayList<>();
+        for (String domainName : domainNames) {
+            if (InetAddresses.isNumericAddress(domainName)) {
+                Log.d(TAG, domainName + " is a numeric IP address!");
+                InetAddress inetAddr = InetAddresses.parseNumericAddress(domainName);
+                domainNameToIpAddr.put(NO_DOMAIN, new ArrayList<>(List.of(inetAddr)));
+                continue;
+            }
+
+            domainNameToIpAddr.put(domainName, new ArrayList<>());
+            // Dispatches separate IPv4 and IPv6 queries to avoid being blocked on either result.
+            if (hasIpv4Address(network)) {
+                futuresList.add(
+                        submitDnsResolverQuery(
+                                domainName, network, DnsResolver.TYPE_A, mDnsResolutionExecutor));
+            }
+            if (hasIpv6Address(network)) {
+                futuresList.add(
+                        submitDnsResolverQuery(
+                                domainName,
+                                network,
+                                DnsResolver.TYPE_AAAA,
+                                mDnsResolutionExecutor));
+            }
+        }
+        CompletableFuture<List<Map.Entry<String, List<InetAddress>>>> allFuturesResult =
+                allOf(futuresList);
+
+        List<Map.Entry<String, List<InetAddress>>> resultList = null;
+        try {
+            resultList = allFuturesResult.get(timeout, TimeUnit.SECONDS);
+        } catch (ExecutionException e) {
+            Log.e(TAG, "Cause of ExecutionException: ", e.getCause());
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            Log.e(TAG, "InterruptedException: ", e);
+        } catch (TimeoutException e) {
+            Log.e(TAG, "TimeoutException: ", e);
+        } finally {
+            if (resultList == null) {
+                Log.w(TAG, "No IP addresses in parallel DNS query!");
+            } else {
+                for (Map.Entry<String, List<InetAddress>> entry : resultList) {
+                    String resultDomainName = entry.getKey();
+                    List<InetAddress> resultIpAddr = v4v6ProtocolFilter(entry.getValue(), filter);
+
+                    if (!domainNameToIpAddr.containsKey(resultDomainName)) {
+                        Log.w(
+                                TAG,
+                                "Unexpected domain name in DnsResolver result: "
+                                        + resultDomainName);
+                        continue;
+                    }
+                    domainNameToIpAddr.get(resultDomainName).addAll(resultIpAddr);
+                }
+            }
+        }
+        return domainNameToIpAddr;
+    }
+
+    /**
+     * Updates the validIpList with the IP addresses corresponding to this domainName. Runs blocking
+     * DNS resolution on the same thread.
+     *
+     * @param domainName Domain name for which DNS resolution needs to be performed.
+     * @param filter Selects for IPv4, IPv6 (or both) addresses from the resulting DNS records
+     * @param validIpList A running list of IP addresses that needs to be updated.
+     * @param network {@link Network} Network on which to run the DNS query.
+     */
     private void getIP(
-            String domainName, int filter, ArrayList<InetAddress> validIpList, Network network) {
-        InetAddress[] ipList;
+            String domainName, int filter, List<InetAddress> validIpList, Network network) {
+        List<InetAddress> ipList = new ArrayList<InetAddress>();
 
         // Get All IP for each domain name
         Log.d(TAG, "Input domainName : " + domainName);
-        try {
-            CompletableFuture<List<InetAddress>> result = new CompletableFuture();
-            final DnsResolver.Callback<List<InetAddress>> cb =
-                    new DnsResolver.Callback<List<InetAddress>>() {
-                        @Override
-                        public void onAnswer(
-                                @NonNull final List<InetAddress> answer, final int rcode) {
-                            if (rcode != 0) {
-                                Log.e(TAG, "DnsResolver Response Code = " + rcode);
+
+        if (InetAddresses.isNumericAddress(domainName)) {
+            Log.d(TAG, domainName + " is a numeric IP address!");
+            ipList.add(InetAddresses.parseNumericAddress(domainName));
+        } else {
+            try {
+                CompletableFuture<List<InetAddress>> result = new CompletableFuture();
+                final DnsResolver.Callback<List<InetAddress>> cb =
+                        new DnsResolver.Callback<List<InetAddress>>() {
+                            @Override
+                            public void onAnswer(
+                                    @NonNull final List<InetAddress> answer, final int rcode) {
+                                if (rcode != 0) {
+                                    Log.e(TAG, "DnsResolver Response Code = " + rcode);
+                                }
+                                result.complete(answer);
                             }
-                            result.complete(answer);
-                        }
 
-                        @Override
-                        public void onError(@Nullable final DnsResolver.DnsException error) {
-                            Log.e(TAG, "Resolve DNS with error : " + error);
-                            result.completeExceptionally(error);
-                        }
-                    };
-            DnsResolver.getInstance()
-                    .query(network, domainName, DnsResolver.FLAG_EMPTY, r -> r.run(), null, cb);
-
-            // Filter the IP list by input ProtoFilter
-            for (InetAddress ipAddress : result.get()) {
-                switch (filter) {
-                    case PROTO_FILTER_IPV4:
-                        if (ipAddress instanceof Inet4Address) {
-                            validIpList.add(ipAddress);
-                        }
-                        break;
-                    case PROTO_FILTER_IPV6:
-                        if (!IwlanHelper.isIpv4EmbeddedIpv6Address(ipAddress)) {
-                            validIpList.add(ipAddress);
-                        }
-                        break;
-                    case PROTO_FILTER_IPV4V6:
-                        validIpList.add(ipAddress);
-                        break;
-                    default:
-                        Log.d(TAG, "Invalid ProtoFilter : " + filter);
+                            @Override
+                            public void onError(@Nullable final DnsResolver.DnsException error) {
+                                Log.e(TAG, "Resolve DNS with error : " + error);
+                                result.completeExceptionally(error);
+                            }
+                        };
+                DnsResolver.getInstance()
+                        .query(
+                                network,
+                                domainName,
+                                DnsResolver.FLAG_EMPTY,
+                                Runnable::run,
+                                null,
+                                cb);
+                ipList =
+                        new ArrayList<>(
+                                result.get(DNS_RESOLVER_TIMEOUT_DURATION_SEC, TimeUnit.SECONDS));
+            } catch (ExecutionException e) {
+                Log.e(TAG, "Cause of ExecutionException: ", e.getCause());
+            } catch (InterruptedException e) {
+                Thread thread = Thread.currentThread();
+                if (thread.interrupted()) {
+                    thread.interrupt();
                 }
+                Log.e(TAG, "InterruptedException: ", e);
+            } catch (TimeoutException e) {
+                Log.e(TAG, "TimeoutException: ", e);
             }
-        } catch (Exception e) {
-            Log.e(TAG, "Exception when resolving domainName : " + domainName + ".", e);
         }
+
+        List<InetAddress> filteredIpList = v4v6ProtocolFilter(ipList, filter);
+        validIpList.addAll(filteredIpList);
     }
 
     private String[] getPlmnList() {
-        List<String> plmnsFromSubInfo = new ArrayList<>();
-
-        List<String> plmnsFromCarrierConfig =
-                new ArrayList<>(
-                        Arrays.asList(
-                                IwlanHelper.getConfig(
-                                        CarrierConfigManager.Iwlan.KEY_MCC_MNCS_STRING_ARRAY,
-                                        mContext,
-                                        mSlotId)));
+        List<String> plmnsFromCarrierConfig = getPlmnsFromCarrierConfig();
         Log.d(TAG, "plmnsFromCarrierConfig:" + plmnsFromCarrierConfig);
 
         // Get Ehplmns & mccmnc from SubscriptionManager
@@ -223,59 +460,72 @@
             return plmnsFromCarrierConfig.toArray(new String[plmnsFromCarrierConfig.size()]);
         }
 
-        // There are three sources of plmns - sim plmn, plmn list from carrier config and
-        // Ehplmn list from subscription info.
-        // The plmns are prioritized as follows:
-        // 1. Sim plmn
-        // 2. Plmns common to both lists.
-        // 3. Remaining plmns in the lists.
-        List<String> combinedList = new ArrayList<>();
         // Get MCCMNC from IMSI
-        String plmnFromImsi =
-                new StringBuilder()
-                        .append(subInfo.getMccString())
-                        .append("-")
-                        .append(subInfo.getMncString())
-                        .toString();
-        combinedList.add(plmnFromImsi);
+        String plmnFromImsi = subInfo.getMccString() + subInfo.getMncString();
 
-        // Get Ehplmns from TelephonyManager
-        for (String ehplmn : getEhplmns()) {
-            if (ehplmn.length() == 5 || ehplmn.length() == 6) {
-                StringBuilder str = new StringBuilder(ehplmn);
-                str.insert(3, "-");
-                plmnsFromSubInfo.add(str.toString());
+        int[] prioritizedPlmnTypes =
+                IwlanHelper.getConfig(
+                        CarrierConfigManager.Iwlan.KEY_EPDG_PLMN_PRIORITY_INT_ARRAY,
+                        mContext,
+                        mSlotId);
+
+        List<String> ehplmns = getEhplmns();
+        String registeredPlmn = getRegisteredPlmn();
+
+        List<String> combinedList = new ArrayList<>();
+        for (int plmnType : prioritizedPlmnTypes) {
+            switch (plmnType) {
+                case CarrierConfigManager.Iwlan.EPDG_PLMN_RPLMN:
+                    if (isInEpdgSelectionInfo(registeredPlmn)) {
+                        combinedList.add(registeredPlmn);
+                    }
+                    break;
+                case CarrierConfigManager.Iwlan.EPDG_PLMN_HPLMN:
+                    combinedList.add(plmnFromImsi);
+                    break;
+                case CarrierConfigManager.Iwlan.EPDG_PLMN_EHPLMN_ALL:
+                    combinedList.addAll(getEhplmns());
+                    break;
+                case CarrierConfigManager.Iwlan.EPDG_PLMN_EHPLMN_FIRST:
+                    if (!ehplmns.isEmpty()) {
+                        combinedList.add(ehplmns.get(0));
+                    }
+                    break;
+                default:
+                    Log.e(TAG, "Unknown PLMN type: " + plmnType);
+                    break;
             }
         }
 
-        Log.d(TAG, "plmnsFromSubInfo:" + plmnsFromSubInfo);
-
-        // To avoid double adding plmn from imsi
-        plmnsFromCarrierConfig.removeIf(i -> i.equals(plmnFromImsi));
-        plmnsFromSubInfo.removeIf(i -> i.equals(plmnFromImsi));
-
-        for (Iterator<String> iterator = plmnsFromCarrierConfig.iterator(); iterator.hasNext(); ) {
-            String plmn = iterator.next();
-            if (plmnsFromSubInfo.contains(plmn)) {
-                combinedList.add(plmn);
-                plmnsFromSubInfo.remove(plmn);
-                iterator.remove();
-            }
-        }
-
-        combinedList.addAll(plmnsFromSubInfo);
-        combinedList.addAll(plmnsFromCarrierConfig);
+        combinedList =
+                combinedList.stream()
+                        .distinct()
+                        .filter(EpdgSelector::isValidPlmn)
+                        .map(plmn -> new StringBuilder(plmn).insert(3, "-").toString())
+                        .toList();
 
         Log.d(TAG, "Final plmn list:" + combinedList);
         return combinedList.toArray(new String[combinedList.size()]);
     }
 
-    private ArrayList<InetAddress> removeDuplicateIp(ArrayList<InetAddress> validIpList) {
+    private List<String> getPlmnsFromCarrierConfig() {
+        return Arrays.asList(
+                IwlanHelper.getConfig(
+                        CarrierConfigManager.Iwlan.KEY_MCC_MNCS_STRING_ARRAY, mContext, mSlotId));
+    }
+
+    private boolean isInEpdgSelectionInfo(String plmn) {
+        if (!isValidPlmn(plmn)) {
+            return false;
+        }
+        List<String> plmnsFromCarrierConfig = getPlmnsFromCarrierConfig();
+        return plmnsFromCarrierConfig.contains(new StringBuilder(plmn).insert(3, "-").toString());
+    }
+
+    private ArrayList<InetAddress> removeDuplicateIp(List<InetAddress> validIpList) {
         ArrayList<InetAddress> resultIpList = new ArrayList<InetAddress>();
 
-        for (Iterator<InetAddress> iterator = validIpList.iterator(); iterator.hasNext(); ) {
-            InetAddress validIp = iterator.next();
-
+        for (InetAddress validIp : validIpList) {
             if (!resultIpList.contains(validIp)) {
                 resultIpList.add(validIp);
             }
@@ -284,16 +534,51 @@
         return resultIpList;
     }
 
+    private void prioritizeIp(@NonNull List<InetAddress> validIpList, @EpdgAddressOrder int order) {
+        switch (order) {
+            case IPV4_PREFERRED:
+                validIpList.sort(inetAddressComparator);
+                break;
+            case IPV6_PREFERRED:
+                validIpList.sort(inetAddressComparator.reversed());
+                break;
+            case SYSTEM_PREFERRED:
+                break;
+            default:
+                Log.w(TAG, "Invalid EpdgAddressOrder : " + order);
+        }
+    }
+
     private String[] splitMccMnc(String plmn) {
         String[] mccmnc = plmn.split("-");
         mccmnc[1] = String.format("%03d", Integer.parseInt(mccmnc[1]));
         return mccmnc;
     }
 
+    /**
+     * @return the registered PLMN, null if not registered with 3gpp or failed to get telephony
+     *     manager
+     */
+    @Nullable
+    private String getRegisteredPlmn() {
+        TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
+        if (telephonyManager == null) {
+            Log.e(TAG, "TelephonyManager is NULL");
+            return null;
+        }
+
+        telephonyManager =
+                telephonyManager.createForSubscriptionId(IwlanHelper.getSubId(mContext, mSlotId));
+
+        String registeredPlmn = telephonyManager.getNetworkOperator();
+        return registeredPlmn.isEmpty() ? null : registeredPlmn;
+    }
+
     private List<String> getEhplmns() {
         TelephonyManager mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
         mTelephonyManager =
-                mTelephonyManager.createForSubscriptionId(IwlanHelper.getSubId(mContext, mSlotId));
+                Objects.requireNonNull(mTelephonyManager)
+                        .createForSubscriptionId(IwlanHelper.getSubId(mContext, mSlotId));
 
         if (mTelephonyManager == null) {
             Log.e(TAG, "TelephonyManager is NULL");
@@ -304,14 +589,14 @@
     }
 
     private void resolutionMethodStatic(
-            int filter, ArrayList<InetAddress> validIpList, boolean isRoaming, Network network) {
+            int filter, List<InetAddress> validIpList, Network network) {
         String[] domainNames = null;
 
         Log.d(TAG, "STATIC Method");
 
         // Get the static domain names from carrier config
         // Config obtained in form of a list of domain names separated by
-        // a delimeter is only used for testing purpose.
+        // a delimiter is only used for testing purpose.
         if (!inSameCountry()) {
             domainNames =
                     getDomainNames(
@@ -327,9 +612,14 @@
         }
 
         Log.d(TAG, "Static Domain Names: " + Arrays.toString(domainNames));
-        for (String domainName : domainNames) {
-            getIP(domainName, filter, validIpList, network);
-        }
+        LinkedHashMap<String, List<InetAddress>> domainNameToIpAddr =
+                getIP(
+                        Arrays.asList(domainNames),
+                        filter,
+                        network,
+                        PARALLEL_STATIC_RESOLUTION_TIMEOUT_DURATION_SEC);
+        printParallelDnsResult(domainNameToIpAddr);
+        domainNameToIpAddr.values().forEach(validIpList::addAll);
     }
 
     private String[] getDomainNames(String key) {
@@ -345,7 +635,9 @@
         boolean inSameCountry = true;
 
         TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
-        tm = tm.createForSubscriptionId(IwlanHelper.getSubId(mContext, mSlotId));
+        tm =
+                Objects.requireNonNull(tm)
+                        .createForSubscriptionId(IwlanHelper.getSubId(mContext, mSlotId));
 
         if (tm != null) {
             String simCountry = tm.getSimCountryIso();
@@ -359,14 +651,15 @@
         return inSameCountry;
     }
 
-    private void resolutionMethodPlmn(
-            int filter, ArrayList<InetAddress> validIpList, boolean isEmergency, Network network) {
+    private Map<String, List<InetAddress>> resolutionMethodPlmn(
+            int filter, List<InetAddress> validIpList, boolean isEmergency, Network network) {
         String[] plmnList;
         StringBuilder domainName = new StringBuilder();
 
         Log.d(TAG, "PLMN Method");
 
         plmnList = getPlmnList();
+        List<String> domainNames = new ArrayList<>();
         for (String plmn : plmnList) {
             String[] mccmnc = splitMccMnc(plmn);
             /*
@@ -377,22 +670,38 @@
              * sos.epdg.epc.mnc<MNC>.mcc<MCC>.pub.3gppnetwork.org
              */
             if (isEmergency) {
-                domainName.append("sos.");
+                domainName = new StringBuilder();
+                domainName
+                        .append("sos.")
+                        .append("epdg.epc.mnc")
+                        .append(mccmnc[1])
+                        .append(".mcc")
+                        .append(mccmnc[0])
+                        .append(".pub.3gppnetwork.org");
+                domainNames.add(domainName.toString());
+                domainName.setLength(0);
             }
-
+            // For emergency PDN setup, still adding FQDN without "sos" header as second priority
+            // because some operator doesn't support hostname with "sos" prefix.
             domainName
                     .append("epdg.epc.mnc")
                     .append(mccmnc[1])
                     .append(".mcc")
                     .append(mccmnc[0])
                     .append(".pub.3gppnetwork.org");
-            getIP(domainName.toString(), filter, validIpList, network);
+            domainNames.add(domainName.toString());
             domainName.setLength(0);
         }
+
+        LinkedHashMap<String, List<InetAddress>> domainNameToIpAddr =
+                getIP(domainNames, filter, network, PARALLEL_PLMN_RESOLUTION_TIMEOUT_DURATION_SEC);
+        printParallelDnsResult(domainNameToIpAddr);
+        domainNameToIpAddr.values().forEach(validIpList::addAll);
+        return domainNameToIpAddr;
     }
 
     private void resolutionMethodCellularLoc(
-            int filter, ArrayList<InetAddress> validIpList, boolean isEmergency, Network network) {
+            int filter, List<InetAddress> validIpList, boolean isEmergency, Network network) {
         String[] plmnList;
         StringBuilder domainName = new StringBuilder();
 
@@ -400,7 +709,8 @@
 
         TelephonyManager mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
         mTelephonyManager =
-                mTelephonyManager.createForSubscriptionId(IwlanHelper.getSubId(mContext, mSlotId));
+                Objects.requireNonNull(mTelephonyManager)
+                        .createForSubscriptionId(IwlanHelper.getSubId(mContext, mSlotId));
 
         if (mTelephonyManager == null) {
             Log.e(TAG, "TelephonyManager is NULL");
@@ -466,8 +776,7 @@
                     domainName.setLength(0);
                 }
             } else if (cellInfo instanceof CellInfoNr) {
-                CellIdentityNr nrCellId =
-                        (CellIdentityNr) ((CellInfoNr) cellInfo).getCellIdentity();
+                CellIdentityNr nrCellId = (CellIdentityNr) cellInfo.getCellIdentity();
                 String tacString = String.format("%06x", nrCellId.getTac());
                 String[] tacSubString = new String[3];
                 tacSubString[0] = tacString.substring(0, 2);
@@ -514,7 +823,7 @@
 
     private void lacDomainNameResolution(
             int filter,
-            ArrayList<InetAddress> validIpList,
+            List<InetAddress> validIpList,
             String lacString,
             boolean isEmergency,
             Network network) {
@@ -548,7 +857,7 @@
         }
     }
 
-    private void resolutionMethodPco(int filter, ArrayList<InetAddress> validIpList) {
+    private void resolutionMethodPco(int filter, List<InetAddress> validIpList) {
         Log.d(TAG, "PCO Method");
 
         int PCO_ID_IPV6 =
@@ -586,7 +895,7 @@
         }
     }
 
-    private void getInetAddressWithPcoData(byte[] pcoData, ArrayList<InetAddress> validIpList) {
+    private void getInetAddressWithPcoData(byte[] pcoData, List<InetAddress> validIpList) {
         InetAddress ipAddress;
         if (pcoData != null && pcoData.length > 0) {
             try {
@@ -647,7 +956,7 @@
 
     private void processNaptrResponse(
             int filter,
-            ArrayList<InetAddress> validIpList,
+            List<InetAddress> validIpList,
             boolean isEmergency,
             Network network,
             boolean isRegisteredWith3GPP,
@@ -699,12 +1008,13 @@
     }
 
     private void resolutionMethodVisitedCountry(
-            int filter, ArrayList<InetAddress> validIpList, boolean isEmergency, Network network) {
+            int filter, List<InetAddress> validIpList, boolean isEmergency, Network network) {
         StringBuilder domainName = new StringBuilder();
 
         TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
         telephonyManager =
-                telephonyManager.createForSubscriptionId(IwlanHelper.getSubId(mContext, mSlotId));
+                Objects.requireNonNull(telephonyManager)
+                        .createForSubscriptionId(IwlanHelper.getSubId(mContext, mSlotId));
 
         if (telephonyManager == null) {
             Log.e(TAG, "TelephonyManager is NULL");
@@ -724,8 +1034,7 @@
 
         final String cellMcc = telephonyManager.getNetworkOperator().substring(0, 3);
         final String cellMnc = telephonyManager.getNetworkOperator().substring(3);
-        final String plmnFromNetwork =
-                new StringBuilder().append(cellMcc).append("-").append(cellMnc).toString();
+        final String plmnFromNetwork = cellMcc + "-" + cellMnc;
         final String registeredhostName = composeFqdnWithMccMnc(cellMcc, cellMnc, isEmergency);
 
         /*
@@ -755,7 +1064,7 @@
                 .append(cellMcc)
                 .append(".visited-country.pub.3gppnetwork.org");
 
-        Log.d(TAG, "Visited Country FQDN with " + domainName.toString());
+        Log.d(TAG, "Visited Country FQDN with " + domainName);
 
         CompletableFuture<List<NaptrTarget>> naptrDnsResult = new CompletableFuture<>();
         DnsResolver.Callback<List<NaptrTarget>> naptrDnsCb =
@@ -774,10 +1083,11 @@
                         naptrDnsResult.completeExceptionally(error);
                     }
                 };
-        NaptrDnsResolver.query(network, domainName.toString(), r -> r.run(), null, naptrDnsCb);
+        NaptrDnsResolver.query(network, domainName.toString(), Runnable::run, null, naptrDnsCb);
 
         try {
-            final List<NaptrTarget> naptrResponse = naptrDnsResult.get();
+            final List<NaptrTarget> naptrResponse =
+                    naptrDnsResult.get(DNS_RESOLVER_TIMEOUT_DURATION_SEC, TimeUnit.SECONDS);
             // Check if there is any record in the NAPTR response
             if (naptrResponse != null && naptrResponse.size() > 0) {
                 processNaptrResponse(
@@ -793,27 +1103,74 @@
         } catch (ExecutionException e) {
             Log.e(TAG, "Cause of ExecutionException: ", e.getCause());
         } catch (InterruptedException e) {
-            if (Thread.currentThread().interrupted()) {
-                Thread.currentThread().interrupt();
+            Thread thread = Thread.currentThread();
+            if (thread.interrupted()) {
+                thread.interrupt();
             }
             Log.e(TAG, "InterruptedException: ", e);
+        } catch (TimeoutException e) {
+            Log.e(TAG, "TimeoutException: ", e);
         }
     }
 
+    // Cancels duplicate prefetches a prefetch is already running. Always schedules tunnel bringup.
+    private void trySubmitEpdgSelectionExecutor(
+            Runnable runnable, boolean isPrefetch, boolean isEmergency) {
+        if (isEmergency) {
+            if (isPrefetch) {
+                if (mSosDnsPrefetchFuture == null || mSosDnsPrefetchFuture.isDone()) {
+                    mSosDnsPrefetchFuture = mSosEpdgSelectionExecutor.submit(runnable);
+                }
+            } else {
+                mSosEpdgSelectionExecutor.execute(runnable);
+            }
+        } else {
+            if (isPrefetch) {
+                if (mDnsPrefetchFuture == null || mDnsPrefetchFuture.isDone()) {
+                    mDnsPrefetchFuture = mEpdgSelectionExecutor.submit(runnable);
+                }
+            } else {
+                mEpdgSelectionExecutor.execute(runnable);
+            }
+        }
+    }
+
+    /**
+     * Asynchronously runs DNS resolution on a carrier-specific list of ePDG servers into IP
+     * addresses, and passes them to the caller via the {@link EpdgSelectorCallback}.
+     *
+     * @param transactionId A unique ID passed in to match the response with the request. If this
+     *     value is 0, the caller is not interested in the result.
+     * @param filter Allows the caller to filter for IPv4 or IPv6 servers, or both.
+     * @param isRoaming Specifies whether the subscription is currently in roaming state.
+     * @param isEmergency Specifies whether the ePDG server lookup is to make an emergency call.
+     * @param network {@link Network} The server lookups will be performed over this Network.
+     * @param selectorCallback {@link EpdgSelectorCallback} The result will be returned through this
+     *     callback. If null, the caller is not interested in the result. Typically, this means the
+     *     caller is performing DNS prefetch of the ePDG server addresses to warm the native
+     *     dnsresolver module's caches.
+     * @return {link IwlanError} denoting the status of this operation.
+     */
     public IwlanError getValidatedServerList(
             int transactionId,
             @ProtoFilter int filter,
+            @EpdgAddressOrder int order,
             boolean isRoaming,
             boolean isEmergency,
             @NonNull Network network,
             EpdgSelectorCallback selectorCallback) {
-        ArrayList<InetAddress> validIpList = new ArrayList<InetAddress>();
-        StringBuilder domainName = new StringBuilder();
 
-        Runnable doValidation =
+        final Runnable epdgSelectionRunnable =
                 () -> {
-                    Log.d(TAG, "Processing request with transactionId: " + transactionId);
-                    String[] plmnList;
+                    List<InetAddress> validIpList = new ArrayList<>();
+                    Log.d(
+                            TAG,
+                            "Processing request with transactionId: "
+                                    + transactionId
+                                    + ", for slotID: "
+                                    + mSlotId
+                                    + ", isEmergency: "
+                                    + isEmergency);
 
                     int[] addrResolutionMethods =
                             IwlanHelper.getConfig(
@@ -834,14 +1191,17 @@
                         resolutionMethodVisitedCountry(filter, validIpList, isEmergency, network);
                     }
 
+                    Map<String, List<InetAddress>> plmnDomainNamesToIpAddress = null;
                     for (int addrResolutionMethod : addrResolutionMethods) {
                         switch (addrResolutionMethod) {
                             case CarrierConfigManager.Iwlan.EPDG_ADDRESS_STATIC:
-                                resolutionMethodStatic(filter, validIpList, isRoaming, network);
+                                resolutionMethodStatic(filter, validIpList, network);
                                 break;
 
                             case CarrierConfigManager.Iwlan.EPDG_ADDRESS_PLMN:
-                                resolutionMethodPlmn(filter, validIpList, isEmergency, network);
+                                plmnDomainNamesToIpAddress =
+                                        resolutionMethodPlmn(
+                                                filter, validIpList, isEmergency, network);
                                 break;
 
                             case CarrierConfigManager.Iwlan.EPDG_ADDRESS_PCO:
@@ -862,8 +1222,29 @@
                     }
 
                     if (selectorCallback != null) {
+                        if (mErrorPolicyManager.getMostRecentDataFailCause()
+                                == DataFailCause.IWLAN_CONGESTION) {
+                            Objects.requireNonNull(plmnDomainNamesToIpAddress)
+                                    .values()
+                                    .removeIf(List::isEmpty);
+
+                            int numFqdns = plmnDomainNamesToIpAddress.size();
+                            int index = mErrorPolicyManager.getCurrentFqdnIndex(numFqdns);
+                            if (index >= 0 && index < numFqdns) {
+                                Object[] keys = plmnDomainNamesToIpAddress.keySet().toArray();
+                                validIpList = plmnDomainNamesToIpAddress.get((String) keys[index]);
+                            } else {
+                                Log.w(
+                                        TAG,
+                                        "CONGESTION error handling- invalid index: "
+                                                + index
+                                                + " number of PLMN FQDNs: "
+                                                + numFqdns);
+                            }
+                        }
+
                         if (!validIpList.isEmpty()) {
-                            Collections.sort(validIpList, inetAddressComparator);
+                            prioritizeIp(validIpList, order);
                             selectorCallback.onServerListChanged(
                                     transactionId, removeDuplicateIp(validIpList));
                         } else {
@@ -874,8 +1255,20 @@
                         }
                     }
                 };
-        Thread subThread = new Thread(doValidation);
-        subThread.start();
+
+        boolean isPrefetch = (selectorCallback == null);
+        trySubmitEpdgSelectionExecutor(epdgSelectionRunnable, isPrefetch, isEmergency);
+
         return new IwlanError(IwlanError.NO_ERROR);
     }
+
+    /**
+     * Validates a PLMN (Public Land Mobile Network) identifier string.
+     *
+     * @param plmn The PLMN identifier string to validate.
+     * @return True if the PLMN identifier is valid, false otherwise.
+     */
+    private static boolean isValidPlmn(String plmn) {
+        return plmn != null && plmn.matches("\\d{5,6}");
+    }
 }
diff --git a/src/com/google/android/iwlan/epdg/EpdgTunnelManager.java b/src/com/google/android/iwlan/epdg/EpdgTunnelManager.java
index ea78893..b8b70e4 100644
--- a/src/com/google/android/iwlan/epdg/EpdgTunnelManager.java
+++ b/src/com/google/android/iwlan/epdg/EpdgTunnelManager.java
@@ -28,6 +28,7 @@
 import android.net.IpSecManager;
 import android.net.IpSecTransform;
 import android.net.LinkAddress;
+import android.net.LinkProperties;
 import android.net.Network;
 import android.net.eap.EapAkaInfo;
 import android.net.eap.EapInfo;
@@ -50,6 +51,7 @@
 import android.net.ipsec.ike.SaProposal;
 import android.net.ipsec.ike.TunnelModeChildSessionParams;
 import android.net.ipsec.ike.exceptions.IkeException;
+import android.net.ipsec.ike.exceptions.IkeIOException;
 import android.net.ipsec.ike.exceptions.IkeProtocolException;
 import android.net.ipsec.ike.ike3gpp.Ike3gppBackoffTimer;
 import android.net.ipsec.ike.ike3gpp.Ike3gppData;
@@ -74,9 +76,12 @@
 import com.google.android.iwlan.ErrorPolicyManager;
 import com.google.android.iwlan.IwlanError;
 import com.google.android.iwlan.IwlanHelper;
+import com.google.android.iwlan.IwlanTunnelMetricsImpl;
+import com.google.android.iwlan.TunnelMetricsInterface;
+import com.google.android.iwlan.TunnelMetricsInterface.OnClosedMetrics;
+import com.google.android.iwlan.TunnelMetricsInterface.OnOpenedMetrics;
 import com.google.android.iwlan.exceptions.IwlanSimNotReadyException;
 
-import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.net.Inet4Address;
@@ -85,11 +90,10 @@
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -99,9 +103,8 @@
 
 public class EpdgTunnelManager {
 
-    private Context mContext;
+    private final Context mContext;
     private final int mSlotId;
-    private HandlerThread mHandlerThread;
     private Handler mHandler;
 
     private static final int EVENT_TUNNEL_BRINGUP_REQUEST = 0;
@@ -115,6 +118,7 @@
     private static final int EVENT_UPDATE_NETWORK = 9;
     private static final int EVENT_IKE_SESSION_OPENED = 10;
     private static final int EVENT_IKE_SESSION_CONNECTION_INFO_CHANGED = 11;
+    private static final int EVENT_IKE_3GPP_DATA_RECEIVED = 12;
     private static final int IKE_HARD_LIFETIME_SEC_MINIMUM = 300;
     private static final int IKE_HARD_LIFETIME_SEC_MAXIMUM = 86400;
     private static final int IKE_SOFT_LIFETIME_SEC_MINIMUM = 120;
@@ -143,26 +147,35 @@
     private static final String TRAFFIC_SELECTOR_IPV6_END_ADDR =
             "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff";
 
-    private static Map<Integer, EpdgTunnelManager> mTunnelManagerInstances =
+    // "192.0.2.0" is selected from RFC5737, "IPv4 Address Blocks Reserved for Documentation"
+    private static final InetAddress DUMMY_ADDR = InetAddresses.parseNumericAddress("192.0.2.0");
+
+    private static final Map<Integer, EpdgTunnelManager> mTunnelManagerInstances =
             new ConcurrentHashMap<>();
 
-    private Queue<TunnelRequestWrapper> mRequestQueue = new LinkedList<>();
+    private Queue<TunnelRequestWrapper> mPendingBringUpRequests = new LinkedList<>();
 
-    private EpdgInfo mValidEpdgInfo = new EpdgInfo();
-    private InetAddress mEpdgAddress;
-    private Network mNetwork;
+    private final EpdgInfo mValidEpdgInfo = new EpdgInfo();
+    @Nullable private InetAddress mEpdgAddress;
+
+    // The most recently updated system default network as seen by IwlanDataService.
+    @Nullable private Network mDefaultNetwork;
+    // The latest Network provided to the IKE session. Only for debugging purposes.
+    @Nullable private Network mIkeSessionNetwork;
+
     private int mTransactionId = 0;
-    private int mProtoFilter = EpdgSelector.PROTO_FILTER_IPV4V6;
-    private boolean mIsEpdgAddressSelected;
-    private IkeSessionCreator mIkeSessionCreator;
+    private boolean mHasConnectedToEpdg;
+    private final IkeSessionCreator mIkeSessionCreator;
 
     private Map<String, TunnelConfig> mApnNameToTunnelConfig = new ConcurrentHashMap<>();
+    private final Map<String, Integer> mApnNameToCurrentToken = new ConcurrentHashMap<>();
 
     private final String TAG;
 
-    private List<InetAddress> mLocalAddresses;
-
     @Nullable private byte[] mNextReauthId = null;
+    private long mEpdgServerSelectionDuration = 0;
+    private long mEpdgServerSelectionStartTime = 0;
+    private long mIkeTunnelEstablishmentStartTime = 0;
 
     private static final Set<Integer> VALID_DH_GROUPS;
     private static final Set<Integer> VALID_KEY_LENGTHS;
@@ -171,60 +184,49 @@
     private static final Set<Integer> VALID_ENCRYPTION_ALGOS;
 
     private static final String CONFIG_TYPE_DH_GROUP = "dh group";
-    private static final String CONFIG_TYPE_KEY_LEN = "alogrithm key length";
+    private static final String CONFIG_TYPE_KEY_LEN = "algorithm key length";
     private static final String CONFIG_TYPE_PRF_ALGO = "prf algorithm";
     private static final String CONFIG_TYPE_INTEGRITY_ALGO = "integrity algorithm";
     private static final String CONFIG_TYPE_ENCRYPT_ALGO = "encryption algorithm";
 
     static {
         VALID_DH_GROUPS =
-                Collections.unmodifiableSet(
-                        new HashSet<>(
-                                Arrays.asList(
-                                        SaProposal.DH_GROUP_1024_BIT_MODP,
-                                        SaProposal.DH_GROUP_1536_BIT_MODP,
-                                        SaProposal.DH_GROUP_2048_BIT_MODP)));
+                Set.of(
+                        SaProposal.DH_GROUP_1024_BIT_MODP,
+                        SaProposal.DH_GROUP_1536_BIT_MODP,
+                        SaProposal.DH_GROUP_2048_BIT_MODP);
         VALID_KEY_LENGTHS =
-                Collections.unmodifiableSet(
-                        new HashSet<>(
-                                Arrays.asList(
-                                        SaProposal.KEY_LEN_AES_128,
-                                        SaProposal.KEY_LEN_AES_192,
-                                        SaProposal.KEY_LEN_AES_256)));
+                Set.of(
+                        SaProposal.KEY_LEN_AES_128,
+                        SaProposal.KEY_LEN_AES_192,
+                        SaProposal.KEY_LEN_AES_256);
 
         VALID_ENCRYPTION_ALGOS =
-                Collections.unmodifiableSet(
-                        new HashSet<>(
-                                Arrays.asList(
-                                        SaProposal.ENCRYPTION_ALGORITHM_AES_CBC,
-                                        SaProposal.ENCRYPTION_ALGORITHM_AES_CTR)));
+                Set.of(
+                        SaProposal.ENCRYPTION_ALGORITHM_AES_CBC,
+                        SaProposal.ENCRYPTION_ALGORITHM_AES_CTR);
 
         VALID_INTEGRITY_ALGOS =
-                Collections.unmodifiableSet(
-                        new HashSet<>(
-                                Arrays.asList(
-                                        SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96,
-                                        SaProposal.INTEGRITY_ALGORITHM_AES_XCBC_96,
-                                        SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_256_128,
-                                        SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_384_192,
-                                        SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_512_256)));
+                Set.of(
+                        SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96,
+                        SaProposal.INTEGRITY_ALGORITHM_AES_XCBC_96,
+                        SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_256_128,
+                        SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_384_192,
+                        SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_512_256);
 
         VALID_PRF_ALGOS =
-                Collections.unmodifiableSet(
-                        new HashSet<>(
-                                Arrays.asList(
-                                        SaProposal.PSEUDORANDOM_FUNCTION_HMAC_SHA1,
-                                        SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC,
-                                        SaProposal.PSEUDORANDOM_FUNCTION_SHA2_256,
-                                        SaProposal.PSEUDORANDOM_FUNCTION_SHA2_384,
-                                        SaProposal.PSEUDORANDOM_FUNCTION_SHA2_512)));
+                Set.of(
+                        SaProposal.PSEUDORANDOM_FUNCTION_HMAC_SHA1,
+                        SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC,
+                        SaProposal.PSEUDORANDOM_FUNCTION_SHA2_256,
+                        SaProposal.PSEUDORANDOM_FUNCTION_SHA2_384,
+                        SaProposal.PSEUDORANDOM_FUNCTION_SHA2_512);
     }
 
     private final EpdgSelector.EpdgSelectorCallback mSelectorCallback =
             new EpdgSelector.EpdgSelectorCallback() {
                 @Override
-                public void onServerListChanged(
-                        int transactionId, ArrayList<InetAddress> validIPList) {
+                public void onServerListChanged(int transactionId, List<InetAddress> validIPList) {
                     sendSelectionRequestComplete(
                             validIPList, new IwlanError(IwlanError.NO_ERROR), transactionId);
                 }
@@ -238,17 +240,28 @@
     @VisibleForTesting
     class TunnelConfig {
         @NonNull final TunnelCallback mTunnelCallback;
+        @NonNull final TunnelMetricsInterface mTunnelMetrics;
         // TODO: Change this to TunnelLinkProperties after removing autovalue
         private List<InetAddress> mPcscfAddrList;
         private List<InetAddress> mDnsAddrList;
         private List<LinkAddress> mInternalAddrList;
 
-        private InetAddress mSrcIpv6Address;
-        private int mSrcIpv6AddressPrefixLen;
+        private final InetAddress mSrcIpv6Address;
+        private final int mSrcIpv6AddressPrefixLen;
         private NetworkSliceInfo mSliceInfo;
         private boolean mIsBackoffTimeValid = false;
         private long mBackoffTime;
 
+        private IkeSessionState mIkeSessionState;
+
+        public IkeSessionState getIkeSessionState() {
+            return mIkeSessionState;
+        }
+
+        public void setIkeSessionState(IkeSessionState ikeSessionState) {
+            mIkeSessionState = ikeSessionState;
+        }
+
         public NetworkSliceInfo getSliceInfo() {
             return mSliceInfo;
         }
@@ -277,13 +290,17 @@
         public TunnelConfig(
                 IkeSession ikeSession,
                 TunnelCallback tunnelCallback,
+                TunnelMetricsInterface tunnelMetrics,
                 InetAddress srcIpv6Addr,
                 int srcIpv6PrefixLength) {
             mTunnelCallback = tunnelCallback;
+            mTunnelMetrics = tunnelMetrics;
             mIkeSession = ikeSession;
             mError = new IwlanError(IwlanError.NO_ERROR);
             mSrcIpv6Address = srcIpv6Addr;
             mSrcIpv6AddressPrefixLen = srcIpv6PrefixLength;
+
+            setIkeSessionState(IkeSessionState.IKE_SESSION_INIT_IN_PROGRESS);
         }
 
         @NonNull
@@ -291,6 +308,11 @@
             return mTunnelCallback;
         }
 
+        @NonNull
+        TunnelMetricsInterface getTunnelMetrics() {
+            return mTunnelMetrics;
+        }
+
         List<InetAddress> getPcscfAddrList() {
             return mPcscfAddrList;
         }
@@ -315,9 +337,7 @@
             if (laddr.isIpv6() && (laddr.getPrefixLength() == mSrcIpv6AddressPrefixLen)) {
                 IpPrefix assignedPrefix = new IpPrefix(laddr.getAddress(), laddr.getPrefixLength());
                 IpPrefix srcPrefix = new IpPrefix(mSrcIpv6Address, mSrcIpv6AddressPrefixLen);
-                if (assignedPrefix.equals(srcPrefix)) {
-                    return true;
-                }
+                return assignedPrefix.equals(srcPrefix);
             }
             return false;
         }
@@ -325,7 +345,7 @@
         public void setInternalAddrList(List<LinkAddress> internalAddrList) {
             mInternalAddrList = new ArrayList<LinkAddress>(internalAddrList);
             if (getSrcIpv6Address() != null) {
-                // check if we can reuse src ipv6 address (i.e if prefix is same)
+                // check if we can reuse src ipv6 address (i.e. if prefix is same)
                 for (LinkAddress assignedAddr : internalAddrList) {
                     if (isPrefixSameAsSrcIP(assignedAddr)) {
                         // the assigned IPv6 address is same as pre-Handover IPv6
@@ -374,21 +394,6 @@
             return mSrcIpv6Address;
         }
 
-        public int getSrcIpv6AddressPrefixLen() {
-            return mSrcIpv6AddressPrefixLen;
-        }
-
-        private String addressListString(List<InetAddress> list) {
-            StringBuilder sb = new StringBuilder();
-            sb.append("{ ");
-            for (InetAddress addr : list) {
-                sb.append(addr);
-                sb.append(", ");
-            }
-            sb.append(" }");
-            return sb.toString();
-        }
-
         public boolean hasTunnelOpened() {
             return mInternalAddrList != null
                     && !mInternalAddrList.isEmpty() /* The child session is opened */
@@ -399,34 +404,13 @@
         public String toString() {
             StringBuilder sb = new StringBuilder();
             sb.append("TunnelConfig { ");
-            /*if (mPcscfAddrList != null) {
-                sb.append("mPcscfAddrList: " + addressListString(mPcscfAddrList));
-                sb.append(", ");
-            }
-            if (mDnsAddrList != null) {
-                sb.append("mDnsAddrList: " + addressListString(mDnsAddrList));
-                sb.append(", ");
-            }
-            if (mInternalAddrList != null) {
-                sb.append("mInternalAddrList: { ");
-                for (LinkAddress addr : mInternalAddrList) {
-                    sb.append(addr + ", ");
-                }
-                sb.append(" }, ");
-            }
-
-            if (mSrcIpv6Address != null) {
-                sb.append("{mSrcIpv6Address: " + mSrcIpv6Address + "}, ");
-            } else {
-                sb.append("{NULL mSrcIpv6Address}, ");
-            } */
 
             if (mSliceInfo != null) {
-                sb.append("mSliceInfo: " + mSliceInfo + ", ");
+                sb.append("mSliceInfo: ").append(mSliceInfo).append(", ");
             }
 
             if (mIsBackoffTimeValid) {
-                sb.append("mBackoffTime: " + mBackoffTime + ", ");
+                sb.append("mBackoffTime: ").append(mBackoffTime).append(", ");
             }
             sb.append(" }");
             return sb.toString();
@@ -437,38 +421,40 @@
     class TmIkeSessionCallback implements IkeSessionCallback {
 
         private final String mApnName;
+        private final int mToken;
 
-        TmIkeSessionCallback(String apnName) {
+        TmIkeSessionCallback(String apnName, int token) {
             this.mApnName = apnName;
+            this.mToken = token;
         }
 
         @Override
         public void onOpened(IkeSessionConfiguration sessionConfiguration) {
-            Log.d(TAG, "Ike session opened for apn: " + mApnName);
+            Log.d(TAG, "Ike session opened for apn: " + mApnName + " with token: " + mToken);
             mHandler.sendMessage(
                     mHandler.obtainMessage(
                             EVENT_IKE_SESSION_OPENED,
-                            new IkeSessionOpenedData(mApnName, sessionConfiguration)));
+                            new IkeSessionOpenedData(mApnName, mToken, sessionConfiguration)));
         }
 
         @Override
         public void onClosed() {
-            Log.d(TAG, "Ike session closed for apn: " + mApnName);
+            Log.d(TAG, "Ike session closed for apn: " + mApnName + " with token: " + mToken);
             mHandler.sendMessage(
                     mHandler.obtainMessage(
                             EVENT_IKE_SESSION_CLOSED,
-                            new SessionClosedData(mApnName, new IwlanError(IwlanError.NO_ERROR))));
+                            new SessionClosedData(mApnName, mToken, null /* ikeException */)));
         }
 
         @Override
-        public void onClosedExceptionally(IkeException exception) {
+        public void onClosedWithException(IkeException exception) {
             mNextReauthId = null;
-            onSessionClosedWithException(exception, mApnName, EVENT_IKE_SESSION_CLOSED);
+            onSessionClosedWithException(exception, mApnName, mToken, EVENT_IKE_SESSION_CLOSED);
         }
 
         @Override
         public void onError(IkeProtocolException exception) {
-            Log.d(TAG, "Ike session onError for apn: " + mApnName);
+            Log.d(TAG, "Ike session onError for apn: " + mApnName + " with token: " + mToken);
 
             mNextReauthId = null;
 
@@ -488,51 +474,34 @@
                     TAG,
                     "Ike session connection info changed for apn: "
                             + mApnName
+                            + " with token: "
+                            + mToken
                             + " Network: "
                             + network);
             mHandler.sendMessage(
                     mHandler.obtainMessage(
                             EVENT_IKE_SESSION_CONNECTION_INFO_CHANGED,
-                            new IkeSessionConnectionInfoData(mApnName, ikeSessionConnectionInfo)));
+                            new IkeSessionConnectionInfoData(
+                                    mApnName, mToken, ikeSessionConnectionInfo)));
         }
     }
 
     @VisibleForTesting
     class TmIke3gppCallback implements Ike3gppExtension.Ike3gppDataListener {
         private final String mApnName;
+        private final int mToken;
 
-        private TmIke3gppCallback(String apnName) {
+        private TmIke3gppCallback(String apnName, int token) {
             mApnName = apnName;
+            mToken = token;
         }
 
         @Override
         public void onIke3gppDataReceived(List<Ike3gppData> payloads) {
-            if (payloads != null && !payloads.isEmpty()) {
-                TunnelConfig tunnelConfig = mApnNameToTunnelConfig.get(mApnName);
-                for (Ike3gppData payload : payloads) {
-                    if (payload.getDataType() == DATA_TYPE_NOTIFY_N1_MODE_INFORMATION) {
-                        Log.d(TAG, "Got payload DATA_TYPE_NOTIFY_N1_MODE_INFORMATION");
-                        NetworkSliceInfo si =
-                                NetworkSliceSelectionAssistanceInformation.getSliceInfo(
-                                        ((Ike3gppN1ModeInformation) payload).getSnssai());
-                        if (si != null) {
-                            tunnelConfig.setSliceInfo(si);
-                            Log.d(TAG, "SliceInfo: " + si);
-                        }
-                    } else if (payload.getDataType() == DATA_TYPE_NOTIFY_BACKOFF_TIMER) {
-                        Log.d(TAG, "Got payload DATA_TYPE_NOTIFY_BACKOFF_TIMER");
-                        long backoffTime =
-                                decodeBackoffTime(
-                                        ((Ike3gppBackoffTimer) payload).getBackoffTimer());
-                        if (backoffTime > 0) {
-                            tunnelConfig.setBackoffTime(backoffTime);
-                            Log.d(TAG, "Backoff Timer: " + backoffTime);
-                        }
-                    }
-                }
-            } else {
-                Log.e(TAG, "Null or empty payloads received:");
-            }
+            mHandler.sendMessage(
+                    mHandler.obtainMessage(
+                            EVENT_IKE_3GPP_DATA_RECEIVED,
+                            new Ike3gppDataReceived(mApnName, mToken, payloads)));
         }
     }
 
@@ -540,67 +509,93 @@
     class TmChildSessionCallback implements ChildSessionCallback {
 
         private final String mApnName;
+        private final int mToken;
 
-        TmChildSessionCallback(String apnName) {
+        TmChildSessionCallback(String apnName, int token) {
             this.mApnName = apnName;
+            this.mToken = token;
         }
 
         @Override
         public void onOpened(ChildSessionConfiguration sessionConfiguration) {
+            Log.d(TAG, "onOpened child session for apn: " + mApnName + " with token: " + mToken);
             mHandler.sendMessage(
                     mHandler.obtainMessage(
                             EVENT_CHILD_SESSION_OPENED,
                             new TunnelOpenedData(
                                     mApnName,
+                                    mToken,
                                     sessionConfiguration.getInternalDnsServers(),
                                     sessionConfiguration.getInternalAddresses())));
         }
 
         @Override
         public void onClosed() {
-            Log.d(TAG, "onClosed child session for apn: " + mApnName);
+            Log.d(TAG, "onClosed child session for apn: " + mApnName + " with token: " + mToken);
             mHandler.sendMessage(
                     mHandler.obtainMessage(
                             EVENT_CHILD_SESSION_CLOSED,
-                            new SessionClosedData(mApnName, new IwlanError(IwlanError.NO_ERROR))));
+                            new SessionClosedData(mApnName, mToken, null /* ikeException */)));
         }
 
         @Override
-        public void onClosedExceptionally(IkeException exception) {
-            onSessionClosedWithException(exception, mApnName, EVENT_CHILD_SESSION_CLOSED);
+        public void onClosedWithException(IkeException exception) {
+            onSessionClosedWithException(exception, mApnName, mToken, EVENT_CHILD_SESSION_CLOSED);
         }
 
         @Override
         public void onIpSecTransformsMigrated(
                 IpSecTransform inIpSecTransform, IpSecTransform outIpSecTransform) {
             // migration is similar to addition
-            Log.d(TAG, "Transforms migrated for apn: + " + mApnName);
+            Log.d(TAG, "Transforms migrated for apn: " + mApnName + " with token: " + mToken);
             mHandler.sendMessage(
                     mHandler.obtainMessage(
                             EVENT_IPSEC_TRANSFORM_CREATED,
                             new IpsecTransformData(
-                                    inIpSecTransform, IpSecManager.DIRECTION_IN, mApnName)));
+                                    inIpSecTransform,
+                                    IpSecManager.DIRECTION_IN,
+                                    mApnName,
+                                    mToken)));
             mHandler.sendMessage(
                     mHandler.obtainMessage(
                             EVENT_IPSEC_TRANSFORM_CREATED,
                             new IpsecTransformData(
-                                    outIpSecTransform, IpSecManager.DIRECTION_OUT, mApnName)));
+                                    outIpSecTransform,
+                                    IpSecManager.DIRECTION_OUT,
+                                    mApnName,
+                                    mToken)));
         }
 
         @Override
         public void onIpSecTransformCreated(IpSecTransform ipSecTransform, int direction) {
-            Log.d(TAG, "Transform created, direction: " + direction + ", apn:" + mApnName);
+            Log.d(
+                    TAG,
+                    "Transform created, direction: "
+                            + direction
+                            + ", apn: "
+                            + mApnName
+                            + ", token: "
+                            + mToken);
             mHandler.sendMessage(
                     mHandler.obtainMessage(
                             EVENT_IPSEC_TRANSFORM_CREATED,
-                            new IpsecTransformData(ipSecTransform, direction, mApnName)));
+                            new IpsecTransformData(ipSecTransform, direction, mApnName, mToken)));
         }
 
         @Override
         public void onIpSecTransformDeleted(IpSecTransform ipSecTransform, int direction) {
-            Log.d(TAG, "Transform deleted, direction: " + direction + ", apn:" + mApnName);
+            Log.d(
+                    TAG,
+                    "Transform deleted, direction: "
+                            + direction
+                            + ", apn: "
+                            + mApnName
+                            + ", token: "
+                            + mToken);
             mHandler.sendMessage(
-                    mHandler.obtainMessage(EVENT_IPSEC_TRANSFORM_DELETED, ipSecTransform));
+                    mHandler.obtainMessage(
+                            EVENT_IPSEC_TRANSFORM_DELETED,
+                            new IpsecTransformData(ipSecTransform, direction, mApnName, mToken)));
         }
     }
 
@@ -619,13 +614,13 @@
 
     @VisibleForTesting
     Looper getLooper() {
-        mHandlerThread = new HandlerThread("EpdgTunnelManagerThread");
-        mHandlerThread.start();
-        return mHandlerThread.getLooper();
+        HandlerThread handlerThread = new HandlerThread("EpdgTunnelManagerThread");
+        handlerThread.start();
+        return handlerThread.getLooper();
     }
 
     /**
-     * Gets a epdg tunnel manager instance.
+     * Gets a EpdgTunnelManager instance.
      *
      * @param context application context
      * @param subId subscription ID for the tunnel
@@ -636,6 +631,11 @@
                 subId, k -> new EpdgTunnelManager(context, subId));
     }
 
+    @VisibleForTesting
+    public static void resetAllInstances() {
+        mTunnelManagerInstances.clear();
+    }
+
     public interface TunnelCallback {
         /**
          * Called when the tunnel is opened.
@@ -655,33 +655,36 @@
 
     /**
      * Close tunnel for an apn. Confirmation of closing will be delivered in TunnelCallback that was
-     * provided in {@link #bringUpTunnel}
+     * provided in {@link #bringUpTunnel}. If no tunnel was available, callback will be delivered
+     * using client-provided provided tunnelCallback and iwlanTunnelMetrics
      *
      * @param apnName apn name
      * @param forceClose if true, results in local cleanup of tunnel
-     * @return true if params are valid and tunnel exists. False otherwise.
+     * @param tunnelCallback Used if no current or pending IWLAN tunnel exists
+     * @param iwlanTunnelMetrics Used to report metrics if no current or pending IWLAN tunnel exists
      */
-    public boolean closeTunnel(@NonNull String apnName, boolean forceClose) {
+    public void closeTunnel(
+            @NonNull String apnName,
+            boolean forceClose,
+            @NonNull TunnelCallback tunnelCallback,
+            @NonNull IwlanTunnelMetricsImpl iwlanTunnelMetrics) {
         mHandler.sendMessage(
                 mHandler.obtainMessage(
                         EVENT_TUNNEL_BRINGDOWN_REQUEST,
-                        forceClose ? 1 : 0,
-                        0 /*not used*/,
-                        apnName));
-        return true;
+                        new TunnelBringdownRequest(
+                                apnName, forceClose, tunnelCallback, iwlanTunnelMetrics)));
     }
 
     /**
      * Update the local Network. This will trigger a revaluation for every tunnel for which tunnel
      * manager has state.
      *
-     * <p>Tunnels in bringup state will be for closed since IKE currently keeps retrying.
-     *
-     * <p>For rest of the tunnels, update IKE session wth new network. This will either result in
-     * MOBIKE callflow or just a rekey over new Network
+     * @param network the network to be updated
+     * @param network the linkProperties to be updated
      */
-    public void updateNetwork(@NonNull Network network, String apnName) {
-        UpdateNetworkWrapper updateNetworkWrapper = new UpdateNetworkWrapper(network, apnName);
+    public void updateNetwork(Network network, LinkProperties linkProperties) {
+        UpdateNetworkWrapper updateNetworkWrapper =
+                new UpdateNetworkWrapper(network, linkProperties);
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_UPDATE_NETWORK, updateNetworkWrapper));
     }
     /**
@@ -694,7 +697,9 @@
      * @return true if params are valid and no existing tunnel. False otherwise.
      */
     public boolean bringUpTunnel(
-            @NonNull TunnelSetupRequest setupRequest, @NonNull TunnelCallback tunnelCallback) {
+            @NonNull TunnelSetupRequest setupRequest,
+            @NonNull TunnelCallback tunnelCallback,
+            @NonNull TunnelMetricsInterface tunnelMetrics) {
         String apnName = setupRequest.apnName();
 
         if (getTunnelSetupRequestApnName(setupRequest) == null) {
@@ -719,7 +724,7 @@
         }
 
         TunnelRequestWrapper tunnelRequestWrapper =
-                new TunnelRequestWrapper(setupRequest, tunnelCallback);
+                new TunnelRequestWrapper(setupRequest, tunnelCallback, tunnelMetrics);
 
         mHandler.sendMessage(
                 mHandler.obtainMessage(EVENT_TUNNEL_BRINGUP_REQUEST, tunnelRequestWrapper));
@@ -727,27 +732,33 @@
         return true;
     }
 
-    private void onBringUpTunnel(TunnelSetupRequest setupRequest, TunnelCallback tunnelCallback) {
+    private void onBringUpTunnel(
+            TunnelSetupRequest setupRequest,
+            TunnelCallback tunnelCallback,
+            TunnelMetricsInterface tunnelMetrics) {
         String apnName = setupRequest.apnName();
-        IkeSessionParams ikeSessionParams = null;
+        IkeSessionParams ikeSessionParams;
 
         Log.d(
                 TAG,
                 "Bringing up tunnel for apn: "
                         + apnName
-                        + "ePDG : "
+                        + " ePDG: "
                         + mEpdgAddress.getHostAddress());
 
+        final int token = incrementAndGetCurrentTokenForApn(apnName);
+
         try {
-            ikeSessionParams = buildIkeSessionParams(setupRequest, apnName);
+            ikeSessionParams = buildIkeSessionParams(setupRequest, apnName, token);
         } catch (IwlanSimNotReadyException e) {
-            mRequestQueue.poll();
             IwlanError iwlanError = new IwlanError(IwlanError.SIM_NOT_READY_EXCEPTION);
             reportIwlanError(apnName, iwlanError);
             tunnelCallback.onClosed(apnName, iwlanError);
+            tunnelMetrics.onClosed(new OnClosedMetrics.Builder().setApnName(apnName).build());
             return;
         }
 
+        mIkeTunnelEstablishmentStartTime = System.currentTimeMillis();
         IkeSession ikeSession =
                 getIkeSessionCreator()
                         .createIkeSession(
@@ -755,14 +766,15 @@
                                 ikeSessionParams,
                                 buildChildSessionParams(setupRequest),
                                 Executors.newSingleThreadExecutor(),
-                                getTmIkeSessionCallback(apnName),
-                                new TmChildSessionCallback(apnName));
+                                getTmIkeSessionCallback(apnName, token),
+                                new TmChildSessionCallback(apnName, token));
 
         boolean isSrcIpv6Present = setupRequest.srcIpv6Address().isPresent();
         putApnNameToTunnelConfig(
                 apnName,
                 ikeSession,
                 tunnelCallback,
+                tunnelMetrics,
                 isSrcIpv6Present ? setupRequest.srcIpv6Address().get() : null,
                 setupRequest.srcIpv6AddressPrefixLength());
     }
@@ -795,9 +807,9 @@
     private ChildSessionParams buildChildSessionParams(TunnelSetupRequest setupRequest) {
         int proto = setupRequest.apnIpProtocol();
         int hardTimeSeconds =
-                (int) getConfig(CarrierConfigManager.Iwlan.KEY_CHILD_SA_REKEY_HARD_TIMER_SEC_INT);
+                getConfig(CarrierConfigManager.Iwlan.KEY_CHILD_SA_REKEY_HARD_TIMER_SEC_INT);
         int softTimeSeconds =
-                (int) getConfig(CarrierConfigManager.Iwlan.KEY_CHILD_SA_REKEY_SOFT_TIMER_SEC_INT);
+                getConfig(CarrierConfigManager.Iwlan.KEY_CHILD_SA_REKEY_SOFT_TIMER_SEC_INT);
         if (!isValidChildSessionLifetime(hardTimeSeconds, softTimeSeconds)) {
             if (hardTimeSeconds > CHILD_HARD_LIFETIME_SEC_MAXIMUM
                     && softTimeSeconds > CHILD_SOFT_LIFETIME_SEC_MINIMUM) {
@@ -805,15 +817,11 @@
                 softTimeSeconds = CHILD_HARD_LIFETIME_SEC_MAXIMUM - LIFETIME_MARGIN_SEC_MINIMUM;
             } else {
                 hardTimeSeconds =
-                        (int)
-                                IwlanHelper.getDefaultConfig(
-                                        CarrierConfigManager.Iwlan
-                                                .KEY_CHILD_SA_REKEY_HARD_TIMER_SEC_INT);
+                        IwlanHelper.getDefaultConfig(
+                                CarrierConfigManager.Iwlan.KEY_CHILD_SA_REKEY_HARD_TIMER_SEC_INT);
                 softTimeSeconds =
-                        (int)
-                                IwlanHelper.getDefaultConfig(
-                                        CarrierConfigManager.Iwlan
-                                                .KEY_CHILD_SA_REKEY_SOFT_TIMER_SEC_INT);
+                        IwlanHelper.getDefaultConfig(
+                                CarrierConfigManager.Iwlan.KEY_CHILD_SA_REKEY_SOFT_TIMER_SEC_INT);
             }
             Log.d(
                     TAG,
@@ -827,7 +835,7 @@
                 new TunnelModeChildSessionParams.Builder()
                         .setLifetimeSeconds(hardTimeSeconds, softTimeSeconds);
 
-        childSessionParamsBuilder.addSaProposal(buildChildSaProposal());
+        childSessionParamsBuilder.addChildSaProposal(buildChildSaProposal());
 
         boolean handoverIPv4Present = setupRequest.srcIpv4Address().isPresent();
         boolean handoverIPv6Present = setupRequest.srcIpv6Address().isPresent();
@@ -898,7 +906,8 @@
     private @Nullable String getMobileDeviceIdentity() {
         TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
         telephonyManager =
-                telephonyManager.createForSubscriptionId(IwlanHelper.getSubId(mContext, mSlotId));
+                Objects.requireNonNull(telephonyManager)
+                        .createForSubscriptionId(IwlanHelper.getSubId(mContext, mSlotId));
         if (telephonyManager == null) {
             return null;
         }
@@ -917,12 +926,13 @@
         return imei.substring(0, imei.length() - 1) + imeisv_suffix;
     }
 
-    private IkeSessionParams buildIkeSessionParams(TunnelSetupRequest setupRequest, String apnName)
+    private IkeSessionParams buildIkeSessionParams(
+            TunnelSetupRequest setupRequest, String apnName, int token)
             throws IwlanSimNotReadyException {
         int hardTimeSeconds =
-                (int) getConfig(CarrierConfigManager.Iwlan.KEY_IKE_REKEY_HARD_TIMER_SEC_INT);
+                getConfig(CarrierConfigManager.Iwlan.KEY_IKE_REKEY_HARD_TIMER_SEC_INT);
         int softTimeSeconds =
-                (int) getConfig(CarrierConfigManager.Iwlan.KEY_IKE_REKEY_SOFT_TIMER_SEC_INT);
+                getConfig(CarrierConfigManager.Iwlan.KEY_IKE_REKEY_SOFT_TIMER_SEC_INT);
         if (!isValidIkeSessionLifetime(hardTimeSeconds, softTimeSeconds)) {
             if (hardTimeSeconds > IKE_HARD_LIFETIME_SEC_MAXIMUM
                     && softTimeSeconds > IKE_SOFT_LIFETIME_SEC_MINIMUM) {
@@ -930,15 +940,11 @@
                 softTimeSeconds = IKE_HARD_LIFETIME_SEC_MAXIMUM - LIFETIME_MARGIN_SEC_MINIMUM;
             } else {
                 hardTimeSeconds =
-                        (int)
-                                IwlanHelper.getDefaultConfig(
-                                        CarrierConfigManager.Iwlan
-                                                .KEY_IKE_REKEY_HARD_TIMER_SEC_INT);
+                        IwlanHelper.getDefaultConfig(
+                                CarrierConfigManager.Iwlan.KEY_IKE_REKEY_HARD_TIMER_SEC_INT);
                 softTimeSeconds =
-                        (int)
-                                IwlanHelper.getDefaultConfig(
-                                        CarrierConfigManager.Iwlan
-                                                .KEY_IKE_REKEY_SOFT_TIMER_SEC_INT);
+                        IwlanHelper.getDefaultConfig(
+                                CarrierConfigManager.Iwlan.KEY_IKE_REKEY_SOFT_TIMER_SEC_INT);
             }
             Log.d(
                     TAG,
@@ -958,8 +964,8 @@
                         .setLocalIdentification(getLocalIdentification())
                         .setRemoteIdentification(getId(setupRequest.apnName(), false))
                         .setAuthEap(null, getEapConfig())
-                        .addSaProposal(buildIkeSaProposal())
-                        .setNetwork(mNetwork)
+                        .addIkeSaProposal(buildIkeSaProposal())
+                        .setNetwork(mDefaultNetwork)
                         .addIkeOption(IkeSessionParams.IKE_OPTION_ACCEPT_ANY_REMOTE_ID)
                         .addIkeOption(IkeSessionParams.IKE_OPTION_MOBIKE)
                         .addIkeOption(IkeSessionParams.IKE_OPTION_REKEY_MOBILITY)
@@ -987,39 +993,51 @@
             }
         }
 
-        Ike3gppParams.Builder builder3gppParams = null;
-
-        String imei = getMobileDeviceIdentity();
-        if (imei != null) {
-            if (builder3gppParams == null) {
-                builder3gppParams = new Ike3gppParams.Builder();
-            }
-            Log.d(TAG, "DEVICE_IDENTITY set in Ike3gppParams");
-            builder3gppParams.setMobileDeviceIdentity(imei);
+        // If MOBIKE is configured, ePDGs may force IPv6 UDP encapsulation- as specified by
+        // RFC 4555- which Android connectivity stack presently does not support.
+        if (mEpdgAddress instanceof Inet6Address) {
+            builder.removeIkeOption(IkeSessionParams.IKE_OPTION_MOBIKE);
         }
 
-        if (setupRequest.pduSessionId() != 0) {
-            if (builder3gppParams == null) {
-                builder3gppParams = new Ike3gppParams.Builder();
+        Ike3gppParams.Builder builder3gppParams = null;
+
+        // TODO(b/239753287): Telus carrier requests DEVICE_IDENTITY, but errors out when parsing
+        //  the response. Temporarily disabled.
+        if (false) {
+            String imei = getMobileDeviceIdentity();
+            if (imei != null) {
+                if (builder3gppParams == null) {
+                    builder3gppParams = new Ike3gppParams.Builder();
+                }
+                Log.d(TAG, "DEVICE_IDENTITY set in Ike3gppParams");
+                builder3gppParams.setMobileDeviceIdentity(imei);
             }
-            builder3gppParams.setPduSessionId((byte) setupRequest.pduSessionId());
+        }
+
+        if (isN1ModeSupported()) {
+            if (setupRequest.pduSessionId() != 0) {
+                // Configures the PduSession ID in N1_MODE_CAPABILITY payload
+                // to notify the server that UE supports N1_MODE
+                builder3gppParams = new Ike3gppParams.Builder();
+                builder3gppParams.setPduSessionId((byte) setupRequest.pduSessionId());
+            }
         }
 
         if (builder3gppParams != null) {
             Ike3gppExtension extension =
-                    new Ike3gppExtension(builder3gppParams.build(), new TmIke3gppCallback(apnName));
+                    new Ike3gppExtension(
+                            builder3gppParams.build(), new TmIke3gppCallback(apnName, token));
             builder.setIke3gppExtension(extension);
         }
 
         int nattKeepAliveTimer =
-                (int) getConfig(CarrierConfigManager.Iwlan.KEY_NATT_KEEP_ALIVE_TIMER_SEC_INT);
+                getConfig(CarrierConfigManager.Iwlan.KEY_NATT_KEEP_ALIVE_TIMER_SEC_INT);
         if (nattKeepAliveTimer < NATT_KEEPALIVE_DELAY_SEC_MIN
                 || nattKeepAliveTimer > NATT_KEEPALIVE_DELAY_SEC_MAX) {
             Log.d(TAG, "Falling back to default natt keep alive timer");
             nattKeepAliveTimer =
-                    (int)
-                            IwlanHelper.getDefaultConfig(
-                                    CarrierConfigManager.Iwlan.KEY_NATT_KEEP_ALIVE_TIMER_SEC_INT);
+                    IwlanHelper.getDefaultConfig(
+                            CarrierConfigManager.Iwlan.KEY_NATT_KEEP_ALIVE_TIMER_SEC_INT);
         }
         builder.setNattKeepAliveDelaySeconds(nattKeepAliveTimer);
 
@@ -1027,23 +1045,17 @@
     }
 
     private boolean isValidChildSessionLifetime(int hardLifetimeSeconds, int softLifetimeSeconds) {
-        if (hardLifetimeSeconds < CHILD_HARD_LIFETIME_SEC_MINIMUM
-                || hardLifetimeSeconds > CHILD_HARD_LIFETIME_SEC_MAXIMUM
-                || softLifetimeSeconds < CHILD_SOFT_LIFETIME_SEC_MINIMUM
-                || hardLifetimeSeconds - softLifetimeSeconds < LIFETIME_MARGIN_SEC_MINIMUM) {
-            return false;
-        }
-        return true;
+        return hardLifetimeSeconds >= CHILD_HARD_LIFETIME_SEC_MINIMUM
+                && hardLifetimeSeconds <= CHILD_HARD_LIFETIME_SEC_MAXIMUM
+                && softLifetimeSeconds >= CHILD_SOFT_LIFETIME_SEC_MINIMUM
+                && hardLifetimeSeconds - softLifetimeSeconds >= LIFETIME_MARGIN_SEC_MINIMUM;
     }
 
     private boolean isValidIkeSessionLifetime(int hardLifetimeSeconds, int softLifetimeSeconds) {
-        if (hardLifetimeSeconds < IKE_HARD_LIFETIME_SEC_MINIMUM
-                || hardLifetimeSeconds > IKE_HARD_LIFETIME_SEC_MAXIMUM
-                || softLifetimeSeconds < IKE_SOFT_LIFETIME_SEC_MINIMUM
-                || hardLifetimeSeconds - softLifetimeSeconds < LIFETIME_MARGIN_SEC_MINIMUM) {
-            return false;
-        }
-        return true;
+        return hardLifetimeSeconds >= IKE_HARD_LIFETIME_SEC_MINIMUM
+                && hardLifetimeSeconds <= IKE_HARD_LIFETIME_SEC_MAXIMUM
+                && softLifetimeSeconds >= IKE_SOFT_LIFETIME_SEC_MINIMUM
+                && hardLifetimeSeconds - softLifetimeSeconds >= LIFETIME_MARGIN_SEC_MINIMUM;
     }
 
     private <T> T getConfig(String configKey) {
@@ -1242,74 +1254,122 @@
     }
 
     private void onSessionClosedWithException(
-            IkeException exception, String apnName, int sessionType) {
-        IwlanError error = new IwlanError(exception);
+            IkeException exception, String apnName, int token, int sessionType) {
         Log.e(
                 TAG,
                 "Closing tunnel with exception for apn: "
                         + apnName
+                        + " with token: "
+                        + token
                         + " sessionType:"
-                        + sessionType
-                        + " error: "
-                        + error);
+                        + sessionType);
         exception.printStackTrace();
 
         mHandler.sendMessage(
-                mHandler.obtainMessage(sessionType, new SessionClosedData(apnName, error)));
+                mHandler.obtainMessage(
+                        sessionType, new SessionClosedData(apnName, token, exception)));
+    }
+
+    private boolean isEpdgSelectionOrFirstTunnelBringUpInProgress() {
+        // Tunnel config is created but not connected to an ePDG. i.e., The first bring-up request
+        // in progress.
+        // No bring-up request in progress but pending queue is not empty. i.e. ePDG selection in
+        // progress
+        return (!mHasConnectedToEpdg && !mApnNameToTunnelConfig.isEmpty())
+                || !mPendingBringUpRequests.isEmpty();
+    }
+
+    private IwlanError getErrorFromIkeException(
+            IkeException ikeException, IkeSessionState ikeSessionState) {
+        IwlanError error;
+        if (ikeException instanceof IkeIOException) {
+            error = new IwlanError(ikeSessionState.getErrorType(), ikeException);
+        } else {
+            error = new IwlanError(ikeException);
+        }
+        Log.e(TAG, "Closing tunnel: error: " + error + " state: " + ikeSessionState);
+        return error;
     }
 
     private final class TmHandler extends Handler {
-        private final String TAG = TmHandler.class.getSimpleName();
 
         @Override
         public void handleMessage(Message msg) {
-            Log.d(TAG, "msg.what = " + msg.what);
+            Log.d(TAG, "msg.what = " + eventToString(msg.what));
 
             String apnName;
             TunnelConfig tunnelConfig;
+            OnClosedMetrics.Builder onClosedMetricsBuilder;
+            switch (msg.what) {
+                case EVENT_CHILD_SESSION_OPENED:
+                case EVENT_IKE_SESSION_CLOSED:
+                case EVENT_IPSEC_TRANSFORM_CREATED:
+                case EVENT_IPSEC_TRANSFORM_DELETED:
+                case EVENT_CHILD_SESSION_CLOSED:
+                case EVENT_IKE_SESSION_OPENED:
+                case EVENT_IKE_SESSION_CONNECTION_INFO_CHANGED:
+                case EVENT_IKE_3GPP_DATA_RECEIVED:
+                    IkeEventData ikeEventData = (IkeEventData) msg.obj;
+                    if (isObsoleteToken(ikeEventData.mApnName, ikeEventData.mToken)) {
+                        Log.d(
+                                TAG,
+                                eventToString(msg.what)
+                                        + " for obsolete token "
+                                        + ikeEventData.mToken);
+                        return;
+                    }
+            }
 
+            long mIkeTunnelEstablishmentDuration;
             switch (msg.what) {
                 case EVENT_TUNNEL_BRINGUP_REQUEST:
                     TunnelRequestWrapper tunnelRequestWrapper = (TunnelRequestWrapper) msg.obj;
                     TunnelSetupRequest setupRequest = tunnelRequestWrapper.getSetupRequest();
+                    IwlanError bringUpError = null;
+
+                    onClosedMetricsBuilder =
+                            new OnClosedMetrics.Builder().setApnName(setupRequest.apnName());
 
                     if (IwlanHelper.getSubId(mContext, mSlotId)
                             == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
                         Log.e(TAG, "SIM isn't ready");
-                        IwlanError iwlanError = new IwlanError(IwlanError.SIM_NOT_READY_EXCEPTION);
-                        reportIwlanError(setupRequest.apnName(), iwlanError);
-                        tunnelRequestWrapper
-                                .getTunnelCallback()
-                                .onClosed(setupRequest.apnName(), iwlanError);
-                        return;
-                    }
-
-                    if (!canBringUpTunnel(setupRequest.apnName())) {
+                        bringUpError = new IwlanError(IwlanError.SIM_NOT_READY_EXCEPTION);
+                        reportIwlanError(setupRequest.apnName(), bringUpError);
+                    } else if (Objects.isNull(mDefaultNetwork)) {
+                        Log.e(TAG, "The default network is not ready");
+                        bringUpError = new IwlanError(IwlanError.IKE_INTERNAL_IO_EXCEPTION);
+                        reportIwlanError(setupRequest.apnName(), bringUpError);
+                    } else if (!canBringUpTunnel(setupRequest.apnName())) {
                         Log.d(TAG, "Cannot bring up tunnel as retry time has not passed");
+                        bringUpError = getLastError(setupRequest.apnName());
+                    }
+
+                    if (Objects.nonNull(bringUpError)) {
                         tunnelRequestWrapper
                                 .getTunnelCallback()
-                                .onClosed(
-                                        setupRequest.apnName(),
-                                        getLastError(setupRequest.apnName()));
+                                .onClosed(setupRequest.apnName(), bringUpError);
+                        tunnelRequestWrapper
+                                .getTunnelMetrics()
+                                .onClosed(onClosedMetricsBuilder.build());
                         return;
                     }
 
-                    // No tunnel bring up in progress and the epdg address is null
-                    if (!mIsEpdgAddressSelected
-                            && mApnNameToTunnelConfig.size() == 0
-                            && mRequestQueue.size() == 0) {
-                        mNetwork = setupRequest.network();
-                        mRequestQueue.add(tunnelRequestWrapper);
-                        selectEpdgAddress(setupRequest);
+                    if (mHasConnectedToEpdg) {
+                        // Service the request immediately when epdg address is available
+                        onBringUpTunnel(
+                                setupRequest,
+                                tunnelRequestWrapper.getTunnelCallback(),
+                                tunnelRequestWrapper.getTunnelMetrics());
                         break;
                     }
 
-                    // Service the request immediately when epdg address is available
-                    if (mIsEpdgAddressSelected) {
-                        onBringUpTunnel(setupRequest, tunnelRequestWrapper.getTunnelCallback());
-                    } else {
-                        mRequestQueue.add(tunnelRequestWrapper);
+                    if (!isEpdgSelectionOrFirstTunnelBringUpInProgress()) {
+                        // No tunnel bring-up in progress. Select the ePDG address first
+                        selectEpdgAddress(setupRequest);
                     }
+
+                    // Another bring-up or ePDG selection is in progress, pending this request.
+                    mPendingBringUpRequests.add(tunnelRequestWrapper);
                     break;
 
                 case EVENT_EPDG_ADDRESS_SELECTION_REQUEST_COMPLETE:
@@ -1321,17 +1381,19 @@
                         break;
                     }
 
-                    if ((tunnelRequestWrapper = mRequestQueue.peek()) == null) {
+                    if (mPendingBringUpRequests.isEmpty()) {
                         Log.d(TAG, "Empty request queue");
                         break;
                     }
 
                     if (selectorResult.getEpdgError().getErrorType() == IwlanError.NO_ERROR
                             && selectorResult.getValidIpList() != null) {
+                        tunnelRequestWrapper = mPendingBringUpRequests.remove();
                         validateAndSetEpdgAddress(selectorResult.getValidIpList());
                         onBringUpTunnel(
                                 tunnelRequestWrapper.getSetupRequest(),
-                                tunnelRequestWrapper.getTunnelCallback());
+                                tunnelRequestWrapper.getTunnelCallback(),
+                                tunnelRequestWrapper.getTunnelMetrics());
                     } else {
                         IwlanError error =
                                 (selectorResult.getEpdgError().getErrorType()
@@ -1372,11 +1434,28 @@
                                     .build();
                     tunnelConfig.getTunnelCallback().onOpened(apnName, linkProperties);
 
-                    setIsEpdgAddressSelected(true);
+                    reportIwlanError(apnName, new IwlanError(IwlanError.NO_ERROR));
+
+                    mIkeTunnelEstablishmentDuration =
+                            System.currentTimeMillis() - mIkeTunnelEstablishmentStartTime;
+                    mIkeTunnelEstablishmentStartTime = 0;
+                    tunnelConfig
+                            .getTunnelMetrics()
+                            .onOpened(
+                                    new OnOpenedMetrics.Builder()
+                                            .setApnName(apnName)
+                                            .setEpdgServerAddress(mEpdgAddress)
+                                            .setEpdgServerSelectionDuration(
+                                                    (int) mEpdgServerSelectionDuration)
+                                            .setIkeTunnelEstablishmentDuration(
+                                                    (int) mIkeTunnelEstablishmentDuration)
+                                            .build());
+
+                    onConnectedToEpdg(true);
                     mValidEpdgInfo.resetIndex();
-                    mRequestQueue.poll();
                     printRequestQueue("EVENT_CHILD_SESSION_OPENED");
                     serviceAllPendingRequests();
+                    tunnelConfig.setIkeSessionState(IkeSessionState.CHILD_SESSION_OPENED);
                     break;
 
                 case EVENT_IKE_SESSION_CLOSED:
@@ -1392,17 +1471,23 @@
 
                     // If IKE session closed exceptionally, we retrieve IwlanError directly from the
                     // exception; otherwise, it is still possible that we triggered an IKE session
-                    // close due to an error (eg. IwlanError.TUNNEL_TRANSFORM_FAILED), or because
+                    // close due to an error (e.g. IwlanError.TUNNEL_TRANSFORM_FAILED), or because
                     // the Child session closed exceptionally; in which case, we attempt to retrieve
                     // the stored error (if any) from TunnelConfig.
                     IwlanError iwlanError;
-                    if (sessionClosedData.mIwlanError.getErrorType() != IwlanError.NO_ERROR) {
-                        iwlanError = sessionClosedData.mIwlanError;
+                    if (sessionClosedData.mIkeException != null) {
+                        iwlanError =
+                                getErrorFromIkeException(
+                                        sessionClosedData.mIkeException,
+                                        tunnelConfig.getIkeSessionState());
                     } else {
-                        // If IKE session setup failed without error cause, Iwlan reports
-                        // NETWORK_FAILURE instead of NO_ERROR
+                        // If IKE session opened, then closed before child session (and IWLAN
+                        // tunnel) opened.
+                        // Iwlan reports IKE_SESSION_CLOSED_BEFORE_CHILD_SESSION_OPENED
+                        // instead of NO_ERROR
                         if (!tunnelConfig.hasTunnelOpened()) {
-                            iwlanError = new IwlanError(IwlanError.NETWORK_FAILURE);
+                            iwlanError = new IwlanError(
+                                    IwlanError.IKE_SESSION_CLOSED_BEFORE_CHILD_SESSION_OPENED);
                         } else {
                             iwlanError = tunnelConfig.getError();
                         }
@@ -1413,65 +1498,105 @@
                         iface.close();
                     }
 
-                    if (!mIsEpdgAddressSelected) {
-                        // fail all the requests. report back off timer, if present, to the
-                        // current request.
-                        if (tunnelConfig.isBackoffTimeValid()) {
-                            mRequestQueue.poll();
-                            reportIwlanError(apnName, iwlanError, tunnelConfig.getBackoffTime());
-                            tunnelConfig.getTunnelCallback().onClosed(apnName, iwlanError);
-                        }
-                        failAllPendingRequests(iwlanError);
-                    } else {
-                        mRequestQueue.poll();
-                        Log.d(TAG, "Tunnel Closed: " + iwlanError);
+                    if (!tunnelConfig.hasTunnelOpened()) {
                         if (tunnelConfig.isBackoffTimeValid()) {
                             reportIwlanError(apnName, iwlanError, tunnelConfig.getBackoffTime());
                         } else {
                             reportIwlanError(apnName, iwlanError);
                         }
-                        tunnelConfig.getTunnelCallback().onClosed(apnName, iwlanError);
+                    }
+
+                    Log.d(TAG, "Tunnel Closed: " + iwlanError);
+                    tunnelConfig.setIkeSessionState(IkeSessionState.NO_IKE_SESSION);
+                    tunnelConfig.getTunnelCallback().onClosed(apnName, iwlanError);
+                    onClosedMetricsBuilder = new OnClosedMetrics.Builder().setApnName(apnName);
+
+                    if (!mHasConnectedToEpdg) {
+                        failAllPendingRequests(iwlanError);
+                        tunnelConfig.getTunnelMetrics().onClosed(onClosedMetricsBuilder.build());
+                    } else {
+                        mIkeTunnelEstablishmentDuration =
+                                mIkeTunnelEstablishmentStartTime > 0
+                                        ? System.currentTimeMillis()
+                                                - mIkeTunnelEstablishmentStartTime
+                                        : 0;
+                        mIkeTunnelEstablishmentStartTime = 0;
+
+                        onClosedMetricsBuilder
+                                .setEpdgServerAddress(mEpdgAddress)
+                                .setEpdgServerSelectionDuration((int) mEpdgServerSelectionDuration)
+                                .setIkeTunnelEstablishmentDuration(
+                                        (int) mIkeTunnelEstablishmentDuration);
+                        tunnelConfig.getTunnelMetrics().onClosed(onClosedMetricsBuilder.build());
                     }
 
                     mApnNameToTunnelConfig.remove(apnName);
-                    if (mApnNameToTunnelConfig.size() == 0 && mRequestQueue.size() == 0) {
-                        resetTunnelManagerState();
+                    if (mApnNameToTunnelConfig.size() == 0 && mPendingBringUpRequests.isEmpty()) {
+                        onConnectedToEpdg(false);
                     }
+
                     break;
 
                 case EVENT_UPDATE_NETWORK:
                     UpdateNetworkWrapper updatedNetwork = (UpdateNetworkWrapper) msg.obj;
-                    apnName = updatedNetwork.getApnName();
-                    Network network = updatedNetwork.getNetwork();
-                    tunnelConfig = mApnNameToTunnelConfig.get(apnName);
+                    mDefaultNetwork = updatedNetwork.getNetwork();
+                    LinkProperties defaultLinkProperties = updatedNetwork.getLinkProperties();
+                    String paraString = "Network: " + mDefaultNetwork;
 
-                    // Update the global cache if they aren't equal
-                    if (mNetwork == null || !mNetwork.equals(network)) {
-                        Log.d(TAG, "Updating mNetwork to " + network);
-                        mNetwork = network;
-                    }
-
-                    if (tunnelConfig == null) {
-                        Log.d(TAG, "Update Network request: No tunnel exists for apn: " + apnName);
-                    } else {
-                        Log.d(TAG, "Updating Network for apn: " + apnName + " Network: " + network);
-                        tunnelConfig.getIkeSession().setNetwork(network);
+                    if (mHasConnectedToEpdg) {
+                        if (Objects.isNull(mDefaultNetwork)) {
+                            Log.w(TAG, "The default network has been removed.");
+                        } else if (Objects.isNull(defaultLinkProperties)) {
+                            Log.w(
+                                    TAG,
+                                    "The default network's LinkProperties is not ready ."
+                                            + paraString);
+                        } else if (!defaultLinkProperties.isReachable(mEpdgAddress)) {
+                            Log.w(
+                                    TAG,
+                                    "The default network link "
+                                            + defaultLinkProperties
+                                            + " doesn't have a route to the ePDG "
+                                            + mEpdgAddress
+                                            + paraString);
+                        } else if (Objects.equals(mDefaultNetwork, mIkeSessionNetwork)) {
+                            Log.w(
+                                    TAG,
+                                    "The default network has not changed from the IKE session"
+                                            + " network. "
+                                            + paraString);
+                        } else {
+                            mApnNameToTunnelConfig.forEach(
+                                    (apn, config) -> {
+                                        Log.d(
+                                                TAG,
+                                                "The Underlying Network is updating for APN (+"
+                                                        + apn
+                                                        + "). "
+                                                        + paraString);
+                                        config.getIkeSession().setNetwork(mDefaultNetwork);
+                                        config.setIkeSessionState(
+                                                IkeSessionState.IKE_MOBILITY_IN_PROGRESS);
+                                    });
+                            mIkeSessionNetwork = mDefaultNetwork;
+                        }
                     }
                     break;
 
                 case EVENT_TUNNEL_BRINGDOWN_REQUEST:
-                    apnName = (String) msg.obj;
-                    int forceClose = msg.arg1;
+                    TunnelBringdownRequest bringdownRequest = (TunnelBringdownRequest) msg.obj;
+                    apnName = bringdownRequest.mApnName;
+                    boolean forceClose = bringdownRequest.mForceClose;
                     tunnelConfig = mApnNameToTunnelConfig.get(apnName);
                     if (tunnelConfig == null) {
-                        Log.d(
+                        Log.w(
                                 TAG,
                                 "Bringdown request: No tunnel exists for apn: "
                                         + apnName
                                         + "forced: "
                                         + forceClose);
                     } else {
-                        if (forceClose == 1) {
+                        if (forceClose) {
                             tunnelConfig.getIkeSession().kill();
                         } else {
                             tunnelConfig.getIkeSession().close();
@@ -1481,6 +1606,15 @@
                     if (numClosed > 0) {
                         Log.d(TAG, "Closed " + numClosed + " pending requests for apn: " + apnName);
                     }
+                    if (tunnelConfig == null && numClosed == 0) {
+                        // IwlanDataService expected to close a (pending or up) tunnel but was not
+                        // found. Recovers state in IwlanDataService through TunnelCallback.
+                        iwlanError = new IwlanError(IwlanError.TUNNEL_NOT_FOUND);
+                        reportIwlanError(apnName, iwlanError);
+                        bringdownRequest.mTunnelCallback.onClosed(apnName, iwlanError);
+                        bringdownRequest.mIwlanTunnelMetrics.onClosed(
+                                new OnClosedMetrics.Builder().setApnName(apnName).build());
+                    }
                     break;
 
                 case EVENT_IPSEC_TRANSFORM_CREATED:
@@ -1490,30 +1624,12 @@
                     tunnelConfig = mApnNameToTunnelConfig.get(apnName);
 
                     if (tunnelConfig.getIface() == null) {
-                        if (mLocalAddresses == null
-                                || mLocalAddresses.size() == 0
-                                || ipSecManager == null) {
-                            Log.e(TAG, "No local addresses found.");
-                            closeIkeSession(
-                                    apnName, new IwlanError(IwlanError.TUNNEL_TRANSFORM_FAILED));
-                            return;
-                        }
-
                         try {
-                            if (mEpdgAddress instanceof Inet4Address
-                                    && mProtoFilter == EpdgSelector.PROTO_FILTER_IPV6) {
-                                mLocalAddresses =
-                                        IwlanHelper.getStackedAddressesForNetwork(
-                                                mNetwork, mContext);
-                            }
-                            InetAddress localAddress =
-                                    (mEpdgAddress instanceof Inet4Address)
-                                            ? IwlanHelper.getIpv4Address(mLocalAddresses)
-                                            : IwlanHelper.getIpv6Address(mLocalAddresses);
-                            Log.d(TAG, "Local address = " + localAddress);
                             tunnelConfig.setIface(
                                     ipSecManager.createIpSecTunnelInterface(
-                                            localAddress, mEpdgAddress, mNetwork));
+                                            DUMMY_ADDR /* unused */,
+                                            DUMMY_ADDR /* unused */,
+                                            mDefaultNetwork));
                         } catch (IpSecManager.ResourceUnavailableException | IOException e) {
                             Log.e(TAG, "Failed to create tunnel interface. " + e);
                             closeIkeSession(
@@ -1523,6 +1639,7 @@
                     }
 
                     try {
+                        assert ipSecManager != null;
                         ipSecManager.applyTunnelModeTransform(
                                 tunnelConfig.getIface(),
                                 transformData.getDirection(),
@@ -1534,10 +1651,15 @@
                         closeIkeSession(
                                 apnName, new IwlanError(IwlanError.TUNNEL_TRANSFORM_FAILED));
                     }
+                    if (tunnelConfig.getIkeSessionState()
+                            == IkeSessionState.IKE_MOBILITY_IN_PROGRESS) {
+                        tunnelConfig.setIkeSessionState(IkeSessionState.CHILD_SESSION_OPENED);
+                    }
                     break;
 
                 case EVENT_IPSEC_TRANSFORM_DELETED:
-                    IpSecTransform transform = (IpSecTransform) msg.obj;
+                    transformData = (IpsecTransformData) msg.obj;
+                    IpSecTransform transform = transformData.getTransform();
                     transform.close();
                     break;
 
@@ -1550,23 +1672,28 @@
                         Log.d(TAG, "No tunnel callback for apn: " + apnName);
                         return;
                     }
-                    tunnelConfig.setError(sessionClosedData.mIwlanError);
+                    if (sessionClosedData.mIkeException != null) {
+                        tunnelConfig.setError(
+                                getErrorFromIkeException(
+                                        sessionClosedData.mIkeException,
+                                        tunnelConfig.getIkeSessionState()));
+                    }
                     tunnelConfig.getIkeSession().close();
                     break;
 
                 case EVENT_IKE_SESSION_OPENED:
                     IkeSessionOpenedData ikeSessionOpenedData = (IkeSessionOpenedData) msg.obj;
+                    apnName = ikeSessionOpenedData.mApnName;
                     IkeSessionConfiguration sessionConfiguration =
                             ikeSessionOpenedData.mIkeSessionConfiguration;
 
-                    tunnelConfig = mApnNameToTunnelConfig.get(ikeSessionOpenedData.mApnName);
+                    tunnelConfig = mApnNameToTunnelConfig.get(apnName);
                     tunnelConfig.setPcscfAddrList(sessionConfiguration.getPcscfServers());
 
                     boolean enabledFastReauth =
-                            (boolean)
-                                    getConfig(
-                                            CarrierConfigManager.Iwlan
-                                                    .KEY_SUPPORTS_EAP_AKA_FAST_REAUTH_BOOL);
+                            getConfig(
+                                    CarrierConfigManager.Iwlan
+                                            .KEY_SUPPORTS_EAP_AKA_FAST_REAUTH_BOOL);
                     Log.d(
                             TAG,
                             "CarrierConfigManager.Iwlan.KEY_SUPPORTS_EAP_AKA_FAST_REAUTH_BOOL "
@@ -1574,7 +1701,7 @@
 
                     if (enabledFastReauth) {
                         EapInfo eapInfo = sessionConfiguration.getEapInfo();
-                        if (eapInfo != null && eapInfo instanceof EapAkaInfo) {
+                        if (eapInfo instanceof EapAkaInfo) {
                             mNextReauthId = ((EapAkaInfo) eapInfo).getReauthId();
                             Log.d(TAG, "Update ReauthId: " + Arrays.toString(mNextReauthId));
                         } else {
@@ -1586,12 +1713,14 @@
                 case EVENT_IKE_SESSION_CONNECTION_INFO_CHANGED:
                     IkeSessionConnectionInfoData ikeSessionConnectionInfoData =
                             (IkeSessionConnectionInfoData) msg.obj;
-                    network = ikeSessionConnectionInfoData.mIkeSessionConnectionInfo.getNetwork();
+                    Network network =
+                            ikeSessionConnectionInfoData.mIkeSessionConnectionInfo.getNetwork();
                     apnName = ikeSessionConnectionInfoData.mApnName;
 
                     ConnectivityManager connectivityManager =
                             mContext.getSystemService(ConnectivityManager.class);
-                    if (connectivityManager.getLinkProperties(network) == null) {
+                    if (Objects.requireNonNull(connectivityManager).getLinkProperties(network)
+                            == null) {
                         Log.e(TAG, "Network " + network + " has null LinkProperties!");
                         return;
                     }
@@ -1610,6 +1739,38 @@
                     }
                     break;
 
+                case EVENT_IKE_3GPP_DATA_RECEIVED:
+                    Ike3gppDataReceived ike3gppDataReceived = (Ike3gppDataReceived) msg.obj;
+                    apnName = ike3gppDataReceived.mApnName;
+                    List<Ike3gppData> ike3gppData = ike3gppDataReceived.mIke3gppData;
+                    if (ike3gppData != null && !ike3gppData.isEmpty()) {
+                        tunnelConfig = mApnNameToTunnelConfig.get(apnName);
+                        for (Ike3gppData payload : ike3gppData) {
+                            if (payload.getDataType() == DATA_TYPE_NOTIFY_N1_MODE_INFORMATION) {
+                                Log.d(TAG, "Got payload DATA_TYPE_NOTIFY_N1_MODE_INFORMATION");
+                                NetworkSliceInfo si =
+                                        NetworkSliceSelectionAssistanceInformation.getSliceInfo(
+                                                ((Ike3gppN1ModeInformation) payload).getSnssai());
+                                if (si != null) {
+                                    tunnelConfig.setSliceInfo(si);
+                                    Log.d(TAG, "SliceInfo: " + si);
+                                }
+                            } else if (payload.getDataType() == DATA_TYPE_NOTIFY_BACKOFF_TIMER) {
+                                Log.d(TAG, "Got payload DATA_TYPE_NOTIFY_BACKOFF_TIMER");
+                                long backoffTime =
+                                        decodeBackoffTime(
+                                                ((Ike3gppBackoffTimer) payload).getBackoffTimer());
+                                if (backoffTime > 0) {
+                                    tunnelConfig.setBackoffTime(backoffTime);
+                                    Log.d(TAG, "Backoff Timer: " + backoffTime);
+                                }
+                            }
+                        }
+                    } else {
+                        Log.e(TAG, "Null or empty payloads received:");
+                    }
+                    break;
+
                 default:
                     throw new IllegalStateException("Unexpected value: " + msg.what);
             }
@@ -1627,35 +1788,58 @@
     }
 
     private void selectEpdgAddress(TunnelSetupRequest setupRequest) {
-        mLocalAddresses = getAddressForNetwork(mNetwork, mContext);
-        if (mLocalAddresses == null || mLocalAddresses.size() == 0) {
-            Log.e(TAG, "No local addresses available.");
-            failAllPendingRequests(
-                    new IwlanError(IwlanError.EPDG_SELECTOR_SERVER_SELECTION_FAILED));
+        ++mTransactionId;
+        mEpdgServerSelectionStartTime = System.currentTimeMillis();
+
+        final int ipPreference =
+                IwlanHelper.getConfig(
+                        CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT,
+                        mContext,
+                        mSlotId);
+
+        IpPreferenceConflict ipPreferenceConflict =
+                isIpPreferenceConflictsWithNetwork(ipPreference);
+        if (ipPreferenceConflict.mIsConflict) {
+            sendSelectionRequestComplete(
+                    null, new IwlanError(ipPreferenceConflict.mErrorType), mTransactionId);
             return;
         }
 
-        mProtoFilter = EpdgSelector.PROTO_FILTER_IPV4V6;
-        if (!IwlanHelper.hasIpv6Address(mLocalAddresses)) {
-            mProtoFilter = EpdgSelector.PROTO_FILTER_IPV4;
-        }
-        if (!IwlanHelper.hasIpv4Address(mLocalAddresses)) {
-            mProtoFilter = EpdgSelector.PROTO_FILTER_IPV6;
+        int protoFilter = EpdgSelector.PROTO_FILTER_IPV4V6;
+        int epdgAddressOrder = EpdgSelector.SYSTEM_PREFERRED;
+        switch (ipPreference) {
+            case CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV4_PREFERRED:
+                epdgAddressOrder = EpdgSelector.IPV4_PREFERRED;
+                break;
+            case CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV6_PREFERRED:
+                epdgAddressOrder = EpdgSelector.IPV6_PREFERRED;
+                break;
+            case CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV4_ONLY:
+                protoFilter = EpdgSelector.PROTO_FILTER_IPV4;
+                break;
+            case CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV6_ONLY:
+                protoFilter = EpdgSelector.PROTO_FILTER_IPV6;
+                break;
+            case CarrierConfigManager.Iwlan.EPDG_ADDRESS_SYSTEM_PREFERRED:
+                break;
+            default:
+                Log.w(TAG, "Invalid Ip preference : " + ipPreference);
         }
 
         EpdgSelector epdgSelector = getEpdgSelector();
         IwlanError epdgError =
                 epdgSelector.getValidatedServerList(
-                        ++mTransactionId,
-                        mProtoFilter,
+                        mTransactionId,
+                        protoFilter,
+                        epdgAddressOrder,
                         setupRequest.isRoaming(),
                         setupRequest.isEmergency(),
-                        mNetwork,
+                        mDefaultNetwork,
                         mSelectorCallback);
 
         if (epdgError.getErrorType() != IwlanError.NO_ERROR) {
             Log.e(TAG, "Epdg address selection failed with error:" + epdgError);
-            failAllPendingRequests(epdgError);
+            sendSelectionRequestComplete(null, epdgError, mTransactionId);
         }
     }
 
@@ -1667,24 +1851,29 @@
     @VisibleForTesting
     int closePendingRequestsForApn(String apnName) {
         int numRequestsClosed = 0;
-        int queueSize = mRequestQueue.size();
+        int queueSize = mPendingBringUpRequests.size();
         if (queueSize == 0) {
             return numRequestsClosed;
         }
 
-        int count = 0;
-
-        while (count < queueSize) {
-            TunnelRequestWrapper requestWrapper = mRequestQueue.poll();
-            if (requestWrapper.getSetupRequest().apnName() == apnName) {
+        for (int count = 0; count < queueSize; count++) {
+            TunnelRequestWrapper requestWrapper = mPendingBringUpRequests.remove();
+            if (requestWrapper.getSetupRequest().apnName().equals(apnName)) {
                 requestWrapper
                         .getTunnelCallback()
                         .onClosed(apnName, new IwlanError(IwlanError.NO_ERROR));
+
+                requestWrapper
+                        .getTunnelMetrics()
+                        .onClosed(
+                                new OnClosedMetrics.Builder()
+                                        .setApnName(apnName)
+                                        .setEpdgServerAddress(mEpdgAddress)
+                                        .build());
                 numRequestsClosed++;
             } else {
-                mRequestQueue.add(requestWrapper);
+                mPendingBringUpRequests.add(requestWrapper);
             }
-            count++;
         }
         return numRequestsClosed;
     }
@@ -1709,69 +1898,74 @@
         mValidEpdgInfo.incrementIndex();
     }
 
-    @VisibleForTesting
-    void resetTunnelManagerState() {
-        Log.d(TAG, "resetTunnelManagerState");
-        mEpdgAddress = null;
-        setIsEpdgAddressSelected(false);
-        mNetwork = null;
-        mRequestQueue = new LinkedList<>();
-        mApnNameToTunnelConfig = new ConcurrentHashMap<>();
-        mLocalAddresses = null;
-    }
-
     private void serviceAllPendingRequests() {
-        while (mRequestQueue.size() > 0) {
+        while (!mPendingBringUpRequests.isEmpty()) {
             Log.d(TAG, "serviceAllPendingRequests");
-            TunnelRequestWrapper request = mRequestQueue.poll();
-            onBringUpTunnel(request.getSetupRequest(), request.getTunnelCallback());
+            TunnelRequestWrapper request = mPendingBringUpRequests.remove();
+            onBringUpTunnel(
+                    request.getSetupRequest(),
+                    request.getTunnelCallback(),
+                    request.getTunnelMetrics());
         }
     }
 
     private void failAllPendingRequests(IwlanError error) {
-        while (mRequestQueue.size() > 0) {
+        while (!mPendingBringUpRequests.isEmpty()) {
             Log.d(TAG, "failAllPendingRequests");
-            TunnelRequestWrapper request = mRequestQueue.poll();
+            TunnelRequestWrapper request = mPendingBringUpRequests.remove();
             TunnelSetupRequest setupRequest = request.getSetupRequest();
             reportIwlanError(setupRequest.apnName(), error);
             request.getTunnelCallback().onClosed(setupRequest.apnName(), error);
+            request.getTunnelMetrics()
+                    .onClosed(
+                            new OnClosedMetrics.Builder()
+                                    .setApnName(setupRequest.apnName())
+                                    .setEpdgServerAddress(mEpdgAddress)
+                                    .build());
         }
     }
 
-    // Prints mRequestQueue
+    // Prints mPendingBringUpRequests
     private void printRequestQueue(String info) {
         Log.d(TAG, info);
-        Log.d(TAG, "mRequestQueue: " + Arrays.toString(mRequestQueue.toArray()));
+        Log.d(
+                TAG,
+                "mPendingBringUpRequests: " + Arrays.toString(mPendingBringUpRequests.toArray()));
     }
 
     // Update Network wrapper
     private static final class UpdateNetworkWrapper {
         private final Network mNetwork;
-        private final String mApnName;
+        private final LinkProperties mLinkProperties;
 
-        private UpdateNetworkWrapper(Network network, String apnName) {
+        private UpdateNetworkWrapper(Network network, LinkProperties linkProperties) {
             mNetwork = network;
-            mApnName = apnName;
-        }
-
-        public String getApnName() {
-            return mApnName;
+            mLinkProperties = linkProperties;
         }
 
         public Network getNetwork() {
             return mNetwork;
         }
+
+        public LinkProperties getLinkProperties() {
+            return mLinkProperties;
+        }
     }
+
     // Tunnel request + tunnel callback
     private static final class TunnelRequestWrapper {
         private final TunnelSetupRequest mSetupRequest;
 
         private final TunnelCallback mTunnelCallback;
+        private final TunnelMetricsInterface mTunnelMetrics;
 
         private TunnelRequestWrapper(
-                TunnelSetupRequest setupRequest, TunnelCallback tunnelCallback) {
+                TunnelSetupRequest setupRequest,
+                TunnelCallback tunnelCallback,
+                TunnelMetricsInterface tunnelMetrics) {
             mTunnelCallback = tunnelCallback;
             mSetupRequest = setupRequest;
+            mTunnelMetrics = tunnelMetrics;
         }
 
         public TunnelSetupRequest getSetupRequest() {
@@ -1781,6 +1975,28 @@
         public TunnelCallback getTunnelCallback() {
             return mTunnelCallback;
         }
+
+        public TunnelMetricsInterface getTunnelMetrics() {
+            return mTunnelMetrics;
+        }
+    }
+
+    private static final class TunnelBringdownRequest {
+        final String mApnName;
+        final boolean mForceClose;
+        final TunnelCallback mTunnelCallback;
+        final IwlanTunnelMetricsImpl mIwlanTunnelMetrics;
+
+        private TunnelBringdownRequest(
+                String apnName,
+                boolean forceClose,
+                TunnelCallback tunnelCallback,
+                IwlanTunnelMetricsImpl iwlanTunnelMetrics) {
+            mApnName = apnName;
+            mForceClose = forceClose;
+            mTunnelCallback = tunnelCallback;
+            mIwlanTunnelMetrics = iwlanTunnelMetrics;
+        }
     }
 
     private static final class EpdgSelectorResult {
@@ -1810,70 +2026,71 @@
     }
 
     // Data received from IkeSessionStateMachine on successful EVENT_CHILD_SESSION_OPENED.
-    private static final class TunnelOpenedData {
-        final String mApnName;
+    private static final class TunnelOpenedData extends IkeEventData {
         final List<InetAddress> mInternalDnsServers;
         final List<LinkAddress> mInternalAddresses;
 
         private TunnelOpenedData(
                 String apnName,
+                int token,
                 List<InetAddress> internalDnsServers,
                 List<LinkAddress> internalAddresses) {
-            mApnName = apnName;
+            super(apnName, token);
             mInternalDnsServers = internalDnsServers;
             mInternalAddresses = internalAddresses;
         }
     }
 
     // Data received from IkeSessionStateMachine on successful EVENT_IKE_SESSION_OPENED.
-    private static final class IkeSessionOpenedData {
-        final String mApnName;
+    private static final class IkeSessionOpenedData extends IkeEventData {
         final IkeSessionConfiguration mIkeSessionConfiguration;
 
         private IkeSessionOpenedData(
-                String apnName, IkeSessionConfiguration ikeSessionConfiguration) {
-            mApnName = apnName;
+                String apnName, int token, IkeSessionConfiguration ikeSessionConfiguration) {
+            super(apnName, token);
             mIkeSessionConfiguration = ikeSessionConfiguration;
         }
     }
 
-    private static final class IkeSessionConnectionInfoData {
-        final String mApnName;
+    private static final class IkeSessionConnectionInfoData extends IkeEventData {
         final IkeSessionConnectionInfo mIkeSessionConnectionInfo;
 
         private IkeSessionConnectionInfoData(
-                String apnName, IkeSessionConnectionInfo ikeSessionConnectionInfo) {
-            mApnName = apnName;
+                String apnName, int token, IkeSessionConnectionInfo ikeSessionConnectionInfo) {
+            super(apnName, token);
             mIkeSessionConnectionInfo = ikeSessionConnectionInfo;
         }
     }
 
+    private static final class Ike3gppDataReceived extends IkeEventData {
+        final List<Ike3gppData> mIke3gppData;
+
+        private Ike3gppDataReceived(String apnName, int token, List<Ike3gppData> ike3gppData) {
+            super(apnName, token);
+            mIke3gppData = ike3gppData;
+        }
+    }
+
     // Data received from IkeSessionStateMachine if either IKE session or Child session have been
     // closed, normally or exceptionally.
-    private static final class SessionClosedData {
-        final String mApnName;
-        final IwlanError mIwlanError;
+    private static final class SessionClosedData extends IkeEventData {
+        final IkeException mIkeException;
 
-        private SessionClosedData(String apnName, IwlanError iwlanError) {
-            mApnName = apnName;
-            mIwlanError = iwlanError;
+        private SessionClosedData(String apnName, int token, IkeException ikeException) {
+            super(apnName, token);
+            mIkeException = ikeException;
         }
     }
 
-    public void releaseInstance() {
-        mHandlerThread.quit();
-        mTunnelManagerInstances.remove(mSlotId);
-    }
-
-    private static final class IpsecTransformData {
+    private static final class IpsecTransformData extends IkeEventData {
         private final IpSecTransform mTransform;
         private final int mDirection;
-        private final String mApnName;
 
-        private IpsecTransformData(IpSecTransform transform, int direction, String apnName) {
+        private IpsecTransformData(
+                IpSecTransform transform, int direction, String apnName, int token) {
+            super(apnName, token);
             mTransform = transform;
             mDirection = direction;
-            mApnName = apnName;
         }
 
         public IpSecTransform getTransform() {
@@ -1885,7 +2102,17 @@
         }
 
         public String getApnName() {
-            return mApnName;
+            return super.mApnName;
+        }
+    }
+
+    private abstract static class IkeEventData {
+        final String mApnName;
+        final int mToken;
+
+        private IkeEventData(String apnName, int token) {
+            mApnName = apnName;
+            mToken = token;
         }
     }
 
@@ -1924,16 +2151,28 @@
         }
     }
 
-    private int[] getRetransmissionTimeoutsFromConfig() {
-        int[] timeList =
-                (int[]) getConfig(CarrierConfigManager.Iwlan.KEY_RETRANSMIT_TIMER_MSEC_INT_ARRAY);
-        boolean isValid = true;
-        if (timeList == null
-                || timeList.length == 0
-                || timeList.length > IKE_RETRANS_MAX_ATTEMPTS_MAX) {
-            isValid = false;
+    private static class IpPreferenceConflict {
+        final boolean mIsConflict;
+        final int mErrorType;
+
+        private IpPreferenceConflict(boolean isConflict, int errorType) {
+            mIsConflict = isConflict;
+            mErrorType = errorType;
         }
-        for (int time : timeList) {
+
+        private IpPreferenceConflict() {
+            mIsConflict = false;
+            mErrorType = IwlanError.NO_ERROR;
+        }
+    }
+
+    private int[] getRetransmissionTimeoutsFromConfig() {
+        int[] timeList = getConfig(CarrierConfigManager.Iwlan.KEY_RETRANSMIT_TIMER_MSEC_INT_ARRAY);
+        boolean isValid =
+                timeList != null
+                        && timeList.length != 0
+                        && timeList.length <= IKE_RETRANS_MAX_ATTEMPTS_MAX;
+        for (int time : Objects.requireNonNull(timeList)) {
             if (time < IKE_RETRANS_TIMEOUT_MS_MIN || time > IKE_RETRANS_TIMEOUT_MS_MAX) {
                 isValid = false;
                 break;
@@ -1941,21 +2180,18 @@
         }
         if (!isValid) {
             timeList =
-                    (int[])
-                            IwlanHelper.getDefaultConfig(
-                                    CarrierConfigManager.Iwlan.KEY_RETRANSMIT_TIMER_MSEC_INT_ARRAY);
+                    IwlanHelper.getDefaultConfig(
+                            CarrierConfigManager.Iwlan.KEY_RETRANSMIT_TIMER_MSEC_INT_ARRAY);
         }
         Log.d(TAG, "getRetransmissionTimeoutsFromConfig: " + Arrays.toString(timeList));
         return timeList;
     }
 
     private int getDpdDelayFromConfig() {
-        int dpdDelay = (int) getConfig(CarrierConfigManager.Iwlan.KEY_DPD_TIMER_SEC_INT);
+        int dpdDelay = getConfig(CarrierConfigManager.Iwlan.KEY_DPD_TIMER_SEC_INT);
         if (dpdDelay < IKE_DPD_DELAY_SEC_MIN || dpdDelay > IKE_DPD_DELAY_SEC_MAX) {
             dpdDelay =
-                    (int)
-                            IwlanHelper.getDefaultConfig(
-                                    CarrierConfigManager.Iwlan.KEY_DPD_TIMER_SEC_INT);
+                    IwlanHelper.getDefaultConfig(CarrierConfigManager.Iwlan.KEY_DPD_TIMER_SEC_INT);
         }
         return dpdDelay;
     }
@@ -1971,7 +2207,7 @@
      * incremented in multiples of 1 minute 1 1 0 value is incremented in multiples of 1 hour 1 1 1
      * value indicates that the timer is deactivated.
      *
-     * @param backoffTimerByte Byte value obtained from ike
+     * @param backoffTimeByte Byte value obtained from ike
      * @return long time value in seconds. -1 if the timer needs to be deactivated.
      */
     private static long decodeBackoffTime(byte backoffTimeByte) {
@@ -1979,12 +2215,12 @@
         final int BACKOFF_TIMER_UNIT_MASK = 0xE0;
         final Long[] BACKOFF_TIMER_UNIT_INCREMENT_SECS = {
             10L * 60L, // 10 mins
-            1L * 60L * 60L, // 1 hour
+            60L * 60L, // 1 hour
             10L * 60L * 60L, // 10 hours
             2L, // 2 seconds
             30L, // 30 seconds
-            1L * 60L, // 1 minute
-            1L * 60L * 60L, // 1 hour
+            60L, // 1 minute
+            60L * 60L, // 1 hour
         };
 
         long time = backoffTimeByte & BACKOFF_TIME_VALUE_MASK;
@@ -1998,8 +2234,7 @@
 
     @VisibleForTesting
     String getTunnelSetupRequestApnName(TunnelSetupRequest setupRequest) {
-        String apnName = setupRequest.apnName();
-        return apnName;
+        return setupRequest.apnName();
     }
 
     @VisibleForTesting
@@ -2007,29 +2242,48 @@
             String apnName,
             IkeSession ikeSession,
             TunnelCallback tunnelCallback,
+            TunnelMetricsInterface tunnelMetrics,
             InetAddress srcIpv6Addr,
             int srcIPv6AddrPrefixLen) {
         mApnNameToTunnelConfig.put(
                 apnName,
-                new TunnelConfig(ikeSession, tunnelCallback, srcIpv6Addr, srcIPv6AddrPrefixLen));
+                new TunnelConfig(
+                        ikeSession,
+                        tunnelCallback,
+                        tunnelMetrics,
+                        srcIpv6Addr,
+                        srcIPv6AddrPrefixLen));
         Log.d(TAG, "Added apn: " + apnName + " to TunnelConfig");
     }
 
     @VisibleForTesting
+    int incrementAndGetCurrentTokenForApn(String apnName) {
+        final int currentToken =
+                mApnNameToCurrentToken.compute(
+                        apnName, (apn, token) -> token == null ? 0 : token + 1);
+        Log.d(TAG, "Added token: " + currentToken + " for apn: " + apnName);
+        return currentToken;
+    }
+
+    @VisibleForTesting
+    boolean isN1ModeSupported() {
+        int[] nrCarrierCaps =
+                getConfig(CarrierConfigManager.KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY);
+        Log.d(TAG, "KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY : " + Arrays.toString(nrCarrierCaps));
+        if (Arrays.stream(nrCarrierCaps)
+                .anyMatch(cap -> cap == CarrierConfigManager.CARRIER_NR_AVAILABILITY_SA)) {
+            return true;
+        } else return false;
+    }
+
+    @VisibleForTesting
     boolean isTunnelConfigContainExistApn(String apnName) {
-        boolean ret = mApnNameToTunnelConfig.containsKey(apnName);
-        return ret;
+        return mApnNameToTunnelConfig.containsKey(apnName);
     }
 
     @VisibleForTesting
     List<InetAddress> getAddressForNetwork(Network network, Context context) {
-        List<InetAddress> ret = IwlanHelper.getAddressesForNetwork(network, context);
-        return ret;
-    }
-
-    @VisibleForTesting
-    EpdgSelector.EpdgSelectorCallback getSelectorCallback() {
-        return mSelectorCallback;
+        return IwlanHelper.getAllAddressesForNetwork(network, context);
     }
 
     @VisibleForTesting
@@ -2039,7 +2293,9 @@
 
     @VisibleForTesting
     void sendSelectionRequestComplete(
-            ArrayList<InetAddress> validIPList, IwlanError result, int transactionId) {
+            List<InetAddress> validIPList, IwlanError result, int transactionId) {
+        mEpdgServerSelectionDuration = System.currentTimeMillis() - mEpdgServerSelectionStartTime;
+        mEpdgServerSelectionStartTime = 0;
         EpdgSelectorResult epdgSelectorResult =
                 new EpdgSelectorResult(validIPList, result, transactionId);
         mHandler.sendMessage(
@@ -2053,14 +2309,58 @@
                 || proto == ApnSetting.PROTOCOL_IPV6);
     }
 
-    @VisibleForTesting
-    TmIkeSessionCallback getTmIkeSessionCallback(String apnName) {
-        return new TmIkeSessionCallback(apnName);
+    boolean isObsoleteToken(String apnName, int token) {
+        if (!mApnNameToCurrentToken.containsKey(apnName)) {
+            return true;
+        }
+        return token != mApnNameToCurrentToken.get(apnName);
+    }
+
+    private static String eventToString(int event) {
+        switch (event) {
+            case EVENT_TUNNEL_BRINGUP_REQUEST:
+                return "EVENT_TUNNEL_BRINGUP_REQUEST";
+            case EVENT_TUNNEL_BRINGDOWN_REQUEST:
+                return "EVENT_TUNNEL_BRINGDOWN_REQUEST";
+            case EVENT_CHILD_SESSION_OPENED:
+                return "EVENT_CHILD_SESSION_OPENED";
+            case EVENT_CHILD_SESSION_CLOSED:
+                return "EVENT_CHILD_SESSION_CLOSED";
+            case EVENT_IKE_SESSION_CLOSED:
+                return "EVENT_IKE_SESSION_CLOSED";
+            case EVENT_EPDG_ADDRESS_SELECTION_REQUEST_COMPLETE:
+                return "EVENT_EPDG_ADDRESS_SELECTION_REQUEST_COMPLETE";
+            case EVENT_IPSEC_TRANSFORM_CREATED:
+                return "EVENT_IPSEC_TRANSFORM_CREATED";
+            case EVENT_IPSEC_TRANSFORM_DELETED:
+                return "EVENT_IPSEC_TRANSFORM_DELETED";
+            case EVENT_UPDATE_NETWORK:
+                return "EVENT_UPDATE_NETWORK";
+            case EVENT_IKE_SESSION_OPENED:
+                return "EVENT_IKE_SESSION_OPENED";
+            case EVENT_IKE_SESSION_CONNECTION_INFO_CHANGED:
+                return "EVENT_IKE_SESSION_CONNECTION_INFO_CHANGED";
+            case EVENT_IKE_3GPP_DATA_RECEIVED:
+                return "EVENT_IKE_3GPP_DATA_RECEIVED";
+            default:
+                return "Unknown(" + event + ")";
+        }
     }
 
     @VisibleForTesting
-    void setIsEpdgAddressSelected(boolean value) {
-        mIsEpdgAddressSelected = value;
+    TmIkeSessionCallback getTmIkeSessionCallback(String apnName, int token) {
+        return new TmIkeSessionCallback(apnName, token);
+    }
+
+    @VisibleForTesting
+    void onConnectedToEpdg(boolean hasConnected) {
+        mHasConnectedToEpdg = hasConnected;
+        if (mHasConnectedToEpdg) {
+            mIkeSessionNetwork = mDefaultNetwork;
+        } else {
+            mIkeSessionNetwork = null;
+            mEpdgAddress = null;
+        }
     }
 
     @VisibleForTesting
@@ -2069,6 +2369,14 @@
     }
 
     @VisibleForTesting
+    int getCurrentTokenForApn(String apnName) {
+        if (!mApnNameToCurrentToken.containsKey(apnName)) {
+            throw new IllegalArgumentException("There is no token for apn: " + apnName);
+        }
+        return mApnNameToCurrentToken.get(apnName);
+    }
+
+    @VisibleForTesting
     long reportIwlanError(String apnName, IwlanError error) {
         return ErrorPolicyManager.getInstance(mContext, mSlotId).reportIwlanError(apnName, error);
     }
@@ -2089,9 +2397,49 @@
         return ErrorPolicyManager.getInstance(mContext, mSlotId).canBringUpTunnel(apnName);
     }
 
-    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+    @VisibleForTesting
+    void setEpdgAddress(InetAddress inetAddress) {
+        mEpdgAddress = inetAddress;
+    }
+
+    @VisibleForTesting
+    IpPreferenceConflict isIpPreferenceConflictsWithNetwork(
+            @CarrierConfigManager.Iwlan.EpdgAddressIpPreference int ipPreference) {
+        List<InetAddress> localAddresses = getAddressForNetwork(mDefaultNetwork, mContext);
+        if (localAddresses == null || localAddresses.size() == 0) {
+            Log.e(TAG, "No local addresses available for Network " + mDefaultNetwork);
+            return new IpPreferenceConflict(true, IwlanError.EPDG_SELECTOR_SERVER_SELECTION_FAILED);
+        } else if (!IwlanHelper.hasIpv6Address(localAddresses)
+                && ipPreference == CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV6_ONLY) {
+            Log.e(
+                    TAG,
+                    "ePDG IP preference: "
+                            + ipPreference
+                            + " conflicts with source IP type: "
+                            + EpdgSelector.PROTO_FILTER_IPV4);
+            return new IpPreferenceConflict(true, IwlanError.EPDG_ADDRESS_ONLY_IPV6_ALLOWED);
+        } else if (!IwlanHelper.hasIpv4Address(localAddresses)
+                && ipPreference == CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV4_ONLY) {
+            // b/209938719 allows Iwlan to support VoWiFi for IPv4 ePDG server while on IPv6 WiFi.
+            // Iwlan will receive a IPv4 address which is embedded in stacked IPv6 address. By using
+            // this IPv4 address, UE will connect to IPv4 ePDG server through XLAT. However, there
+            // are issues on connecting ePDG server through XLAT. Will allow IPV4_ONLY on IPv6 WiFi
+            // after the issues are resolved.
+            Log.e(
+                    TAG,
+                    "ePDG IP preference: "
+                            + ipPreference
+                            + " conflicts with source IP type: "
+                            + EpdgSelector.PROTO_FILTER_IPV6);
+            return new IpPreferenceConflict(true, IwlanError.EPDG_ADDRESS_ONLY_IPV4_ALLOWED);
+        }
+        return new IpPreferenceConflict();
+    }
+
+    public void dump(PrintWriter pw) {
         pw.println("---- EpdgTunnelManager ----");
-        pw.println("mIsEpdgAddressSelected: " + mIsEpdgAddressSelected);
+        pw.println("mHasConnectedToEpdg: " + mHasConnectedToEpdg);
+        pw.println("mIkeSessionNetwork: " + mIkeSessionNetwork);
         if (mEpdgAddress != null) {
             pw.println("mEpdgAddress: " + mEpdgAddress);
         }
diff --git a/src/com/google/android/iwlan/epdg/IkeSessionState.java b/src/com/google/android/iwlan/epdg/IkeSessionState.java
new file mode 100644
index 0000000..ad3d5e3
--- /dev/null
+++ b/src/com/google/android/iwlan/epdg/IkeSessionState.java
@@ -0,0 +1,55 @@
+/*
+ * 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.google.android.iwlan.epdg;
+
+import com.google.android.iwlan.IwlanError;
+
+/** A state machine that infers the current IkeSession state. */
+enum IkeSessionState {
+    NO_IKE_SESSION {
+        @Override
+        public int getErrorType() {
+            return IwlanError.NO_ERROR;
+        }
+    },
+    IKE_SESSION_INIT_IN_PROGRESS {
+        @Override
+        public int getErrorType() {
+            return IwlanError.IKE_INIT_TIMEOUT;
+        }
+    },
+    IKE_MOBILITY_IN_PROGRESS {
+        @Override
+        public int getErrorType() {
+            return IwlanError.IKE_MOBILITY_TIMEOUT;
+        }
+    },
+    CHILD_SESSION_OPENED {
+        @Override
+        public int getErrorType() {
+            return IwlanError.IKE_DPD_TIMEOUT;
+        }
+    };
+
+    /**
+     * Called when IkeSession report error with IkeIOException, check current IkeSession state and
+     * return corresponding time out error.
+     *
+     * @return NO_ERROR or IWLAN IKE time out error
+     */
+    public abstract int getErrorType();
+}
diff --git a/src/com/google/android/iwlan/epdg/SrvDnsResolver.java b/src/com/google/android/iwlan/epdg/SrvDnsResolver.java
index 1ae9cba..192fbb8 100644
--- a/src/com/google/android/iwlan/epdg/SrvDnsResolver.java
+++ b/src/com/google/android/iwlan/epdg/SrvDnsResolver.java
@@ -36,6 +36,7 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
@@ -146,7 +147,7 @@
         // Parses the Answers section of a DnsPacket to construct and return a mapping
         // of Domain Name strings to their corresponding SRV record.
         public @NonNull Map<String, SrvRecord> parseSrvRecords() throws ParseException {
-            final HashMap<String, SrvRecord> targetNameToSrvRecord = new HashMap<>();
+            final HashMap<String, SrvRecord> targetNameToSrvRecord = new LinkedHashMap<>();
             if (mHeader.getRecordCount(ANSECTION) == 0) return targetNameToSrvRecord;
 
             for (final DnsRecord ansSec : mRecords[ANSECTION]) {
diff --git a/src/com/google/android/iwlan/epdg/TunnelSetupRequest.java b/src/com/google/android/iwlan/epdg/TunnelSetupRequest.java
index 4086c22..d1ed9dd 100644
--- a/src/com/google/android/iwlan/epdg/TunnelSetupRequest.java
+++ b/src/com/google/android/iwlan/epdg/TunnelSetupRequest.java
@@ -29,8 +29,6 @@
 
     abstract int apnIpProtocol();
 
-    abstract Network network();
-
     abstract Optional<InetAddress> srcIpv4Address();
 
     abstract Optional<InetAddress> srcIpv6Address();
@@ -58,8 +56,6 @@
 
         public abstract Builder setApnIpProtocol(int protocol);
 
-        public abstract Builder setNetwork(Network network);
-
         public Builder setSrcIpv4Address(InetAddress srcIpv4Address) {
             return setSrcIpv4Address(Optional.ofNullable(srcIpv4Address));
         }
diff --git a/src/com/google/android/iwlan/proto/MetricsAtom.java b/src/com/google/android/iwlan/proto/MetricsAtom.java
new file mode 100644
index 0000000..7ecf464
--- /dev/null
+++ b/src/com/google/android/iwlan/proto/MetricsAtom.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2020 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.google.android.iwlan.proto;
+
+import android.net.ipsec.ike.exceptions.IkeIOException;
+import android.net.ipsec.ike.exceptions.IkeInternalException;
+
+import com.google.android.iwlan.IwlanError;
+import com.google.android.iwlan.IwlanStatsLog;
+
+public class MetricsAtom {
+    private int mMessageId;
+    private int mApnType;
+    private boolean mIsHandover;
+    private String mEpdgServerAddress;
+    private int mSourceRat;
+    private boolean mIsCellularRoaming;
+    private boolean mIsNetworkConnected;
+    private int mTransportType;
+    private int mSetupRequestResult;
+    private int mIwlanError;
+    private int mDataCallFailCause;
+    private int mProcessingDurationMillis;
+    private int mEpdgServerSelectionDurationMillis;
+    private int mIkeTunnelEstablishmentDurationMillis;
+    private int mTunnelState;
+    private int mHandoverFailureMode;
+    private int mRetryDurationMillis;
+    private int mWifiSignalValue;
+    private String mIwlanErrorWrappedClassname;
+    private String mIwlanErrorWrappedStackFirstFrame;
+
+    public void setMessageId(int messageId) {
+        this.mMessageId = messageId;
+    }
+
+    public void setApnType(int apnType) {
+        this.mApnType = apnType;
+    }
+
+    public void setIsHandover(boolean isHandover) {
+        this.mIsHandover = isHandover;
+    }
+
+    public void setEpdgServerAddress(String epdgServerAddress) {
+        this.mEpdgServerAddress = epdgServerAddress;
+    }
+
+    public void setSourceRat(int sourceRat) {
+        this.mSourceRat = sourceRat;
+    }
+
+    public void setIsCellularRoaming(boolean isCellularRoaming) {
+        this.mIsCellularRoaming = isCellularRoaming;
+    }
+
+    public void setIsNetworkConnected(boolean isNetworkConnected) {
+        this.mIsNetworkConnected = isNetworkConnected;
+    }
+
+    public void setTransportType(int transportType) {
+        this.mTransportType = transportType;
+    }
+
+    public void setSetupRequestResult(int setupRequestResult) {
+        this.mSetupRequestResult = setupRequestResult;
+    }
+
+    public void setIwlanError(int iwlanError) {
+        this.mIwlanError = iwlanError;
+    }
+
+    public void setDataCallFailCause(int dataCallFailCause) {
+        this.mDataCallFailCause = dataCallFailCause;
+    }
+
+    public void setProcessingDurationMillis(int processingDurationMillis) {
+        this.mProcessingDurationMillis = processingDurationMillis;
+    }
+
+    public void setEpdgServerSelectionDurationMillis(int epdgServerSelectionDurationMillis) {
+        this.mEpdgServerSelectionDurationMillis = epdgServerSelectionDurationMillis;
+    }
+
+    public void setIkeTunnelEstablishmentDurationMillis(int ikeTunnelEstablishmentDurationMillis) {
+        this.mIkeTunnelEstablishmentDurationMillis = ikeTunnelEstablishmentDurationMillis;
+    }
+
+    public void setTunnelState(int tunnelState) {
+        this.mTunnelState = tunnelState;
+    }
+
+    public void setHandoverFailureMode(int handoverFailureMode) {
+        this.mHandoverFailureMode = handoverFailureMode;
+    }
+
+    public void setRetryDurationMillis(int retryDurationMillis) {
+        this.mRetryDurationMillis = retryDurationMillis;
+    }
+
+    public void setWifiSignalValue(int wifiSignalValue) {
+        this.mWifiSignalValue = wifiSignalValue;
+    }
+
+    public void setIwlanErrorWrappedClassnameAndStack(IwlanError iwlanError) {
+        Throwable iwlanErrorWrapped = iwlanError.getException();
+        if (iwlanErrorWrapped instanceof IkeInternalException
+                || iwlanErrorWrapped instanceof IkeIOException) {
+            iwlanErrorWrapped = iwlanErrorWrapped.getCause();
+        }
+
+        if (iwlanErrorWrapped == null) {
+            this.mIwlanErrorWrappedClassname = null;
+            this.mIwlanErrorWrappedStackFirstFrame = null;
+            return;
+        }
+
+        this.mIwlanErrorWrappedClassname = iwlanErrorWrapped.getClass().getCanonicalName();
+
+        StackTraceElement[] iwlanErrorWrappedStackTraceElements = iwlanErrorWrapped.getStackTrace();
+        this.mIwlanErrorWrappedStackFirstFrame =
+                iwlanErrorWrappedStackTraceElements.length != 0
+                        ? iwlanErrorWrappedStackTraceElements[0].toString()
+                        : null;
+    }
+
+    public String getIwlanErrorWrappedClassname() {
+        return mIwlanErrorWrappedClassname;
+    }
+
+    public String getIwlanErrorWrappedStackFirstFrame() {
+        return mIwlanErrorWrappedStackFirstFrame;
+    }
+
+    public void sendMetricsData() {
+        if (mMessageId == IwlanStatsLog.IWLAN_SETUP_DATA_CALL_RESULT_REPORTED) {
+            IwlanStatsLog.write(
+                    mMessageId,
+                    mApnType,
+                    mIsHandover,
+                    mEpdgServerAddress,
+                    mSourceRat,
+                    mIsCellularRoaming,
+                    mIsNetworkConnected,
+                    mTransportType,
+                    mSetupRequestResult,
+                    mIwlanError,
+                    mDataCallFailCause,
+                    mProcessingDurationMillis,
+                    mEpdgServerSelectionDurationMillis,
+                    mIkeTunnelEstablishmentDurationMillis,
+                    mTunnelState,
+                    mHandoverFailureMode,
+                    mRetryDurationMillis,
+                    mIwlanErrorWrappedClassname,
+                    mIwlanErrorWrappedStackFirstFrame);
+            return;
+        } else if (mMessageId == IwlanStatsLog.IWLAN_PDN_DISCONNECTED_REASON_REPORTED) {
+            IwlanStatsLog.write(
+                    mMessageId,
+                    mDataCallFailCause,
+                    mIsNetworkConnected,
+                    mTransportType,
+                    mWifiSignalValue);
+            return;
+        }
+    }
+}
diff --git a/test/com/google/android/iwlan/ErrorPolicyManagerTest.java b/test/com/google/android/iwlan/ErrorPolicyManagerTest.java
index 3bc624b..1c15e41 100644
--- a/test/com/google/android/iwlan/ErrorPolicyManagerTest.java
+++ b/test/com/google/android/iwlan/ErrorPolicyManagerTest.java
@@ -20,6 +20,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.*;
 import static org.mockito.Mockito.mock;
@@ -30,6 +31,7 @@
 import android.content.res.AssetManager;
 import android.net.ipsec.ike.exceptions.IkeProtocolException;
 import android.os.PersistableBundle;
+import android.os.test.TestLooper;
 import android.telephony.CarrierConfigManager;
 import android.telephony.DataFailCause;
 import android.telephony.SubscriptionInfo;
@@ -39,6 +41,9 @@
 
 import androidx.test.InstrumentationRegistry;
 
+import com.google.auto.value.AutoValue;
+
+import org.json.JSONArray;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -48,9 +53,80 @@
 import org.mockito.quality.Strictness;
 
 import java.io.InputStream;
+import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 public class ErrorPolicyManagerTest {
+    @AutoValue
+    abstract static class ErrorPolicyString {
+        abstract String errorType();
+
+        abstract List<String> errorDetails();
+
+        abstract List<String> retryArray();
+
+        abstract List<String> unthrottlingEvents();
+
+        abstract Optional<String> numAttemptsPerFqdn();
+
+        abstract Optional<String> handoverAttemptCount();
+
+        static Builder builder() {
+            return new AutoValue_ErrorPolicyManagerTest_ErrorPolicyString.Builder();
+        }
+
+        @AutoValue.Builder
+        abstract static class Builder {
+            abstract Builder setErrorType(String errorType);
+
+            abstract Builder setErrorDetails(List<String> errorDetails);
+
+            abstract Builder setRetryArray(List<String> retryArray);
+
+            abstract Builder setUnthrottlingEvents(List<String> unthrottlingEvents);
+
+            abstract Builder setNumAttemptsPerFqdn(String numAttemptsPerFqdn);
+
+            abstract Builder setHandoverAttemptCount(String handoverAttemptCount);
+
+            abstract ErrorPolicyString build();
+        }
+
+        String getErrorPolicyInString() {
+            StringBuilder errorPolicy =
+                    new StringBuilder(
+                            "\"ErrorType\": \""
+                                    + errorType()
+                                    + "\","
+                                    + "\"ErrorDetails\": [\""
+                                    + String.join("\", \"", errorDetails())
+                                    + "\"],"
+                                    + "\"RetryArray\": [\""
+                                    + String.join("\", \"", retryArray())
+                                    + "\"],"
+                                    + "\"UnthrottlingEvents\": [\""
+                                    + String.join("\", \"", unthrottlingEvents())
+                                    + "\"]");
+
+            numAttemptsPerFqdn()
+                    .ifPresent(
+                            numAttemptsPerFqdn ->
+                                    errorPolicy
+                                            .append(",\"NumAttemptsPerFqdn\": \"")
+                                            .append(numAttemptsPerFqdn)
+                                            .append("\""));
+            handoverAttemptCount()
+                    .ifPresent(
+                            handoverAttemptCount ->
+                                    errorPolicy
+                                            .append(",\"HandoverAttemptCount\": \"")
+                                            .append(handoverAttemptCount)
+                                            .append("\""));
+            return errorPolicy.toString();
+        }
+    }
+
     private static final String TAG = "ErrorPolicyManagerTest";
 
     // @Rule public final MockitoRule mockito = MockitoJUnit.rule();
@@ -60,6 +136,9 @@
     private static final int DEFAULT_SUBID = 0;
     private static final int TEST_CARRIER_ID = 1;
 
+    private TestLooper mTestLooper = new TestLooper();
+    private long mMockedClockTime = 0;
+
     @Mock private Context mMockContext;
     @Mock CarrierConfigManager mMockCarrierConfigManager;
     @Mock SubscriptionManager mMockSubscriptionManager;
@@ -75,17 +154,22 @@
         mStaticMockSession =
                 mockitoSession()
                         .mockStatic(IwlanDataService.class)
+                        .spyStatic(IwlanHelper.class)
                         .strictness(Strictness.LENIENT)
                         .startMocking();
         when(IwlanDataService.getDataServiceProvider(anyInt()))
                 .thenReturn(mMockDataServiceProvider);
+        when(IwlanHelper.elapsedRealtime()).thenAnswer(i -> mMockedClockTime);
         AssetManager mockAssetManager = mock(AssetManager.class);
         Context context = InstrumentationRegistry.getTargetContext();
         InputStream is = context.getResources().getAssets().open("defaultiwlanerrorconfig.json");
         doReturn(mockAssetManager).when(mMockContext).getAssets();
         doReturn(is).when(mockAssetManager).open(any());
         setupMockForCarrierConfig(null);
+        ErrorPolicyManager.resetAllInstances();
         mErrorPolicyManager = spy(ErrorPolicyManager.getInstance(mMockContext, DEFAULT_SLOT_INDEX));
+        doReturn(mTestLooper.getLooper()).when(mErrorPolicyManager).getLooper();
+        mErrorPolicyManager.initHandler();
     }
 
     @After
@@ -113,6 +197,10 @@
         return buildIwlanIkeProtocolError(IkeProtocolException.ERROR_TYPE_CHILD_SA_NOT_FOUND);
     }
 
+    private static IwlanError buildIwlanIkeInternalAddressFailure() {
+        return buildIwlanIkeProtocolError(IkeProtocolException.ERROR_TYPE_INTERNAL_ADDRESS_FAILURE);
+    }
+
     @Test
     public void testValidCarrierConfig() throws Exception {
         String apn = "ims";
@@ -122,17 +210,22 @@
                         + apn
                         + "\","
                         + "\"ErrorTypes\": [{"
-                        + getErrorTypeInJSON(
-                                "IKE_PROTOCOL_ERROR_TYPE",
-                                new String[] {"24", "34", "9000-9050"},
-                                new String[] {"4", "8", "16"},
-                                new String[] {"APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("IKE_PROTOCOL_ERROR_TYPE")
+                                .setErrorDetails(List.of("24", "34", "9000-9050"))
+                                .setRetryArray(List.of("4", "8", "16"))
+                                .setUnthrottlingEvents(
+                                        List.of("APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}, {"
-                        + getErrorTypeInJSON(
-                                "GENERIC_ERROR_TYPE",
-                                new String[] {"SERVER_SELECTION_FAILED"},
-                                new String[] {"0"},
-                                new String[] {"APM_ENABLE_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("GENERIC_ERROR_TYPE")
+                                .setErrorDetails(List.of("SERVER_SELECTION_FAILED"))
+                                .setRetryArray(List.of("0"))
+                                .setUnthrottlingEvents(List.of("APM_ENABLE_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}]"
                         + "}]";
 
@@ -143,8 +236,7 @@
                 .mHandler
                 .obtainMessage(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT)
                 .sendToTarget();
-
-        sleep(1000);
+        mTestLooper.dispatchAll();
 
         // IKE_PROTOCOL_ERROR_TYPE(24) and retryArray = 4,8,16
         IwlanError iwlanError = buildIwlanIkeAuthFailedError();
@@ -187,7 +279,7 @@
         time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
         assertEquals(10, time);
         time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
-        assertEquals(10, time);
+        assertEquals(20, time);
     }
 
     @Test
@@ -199,11 +291,14 @@
                         + apn
                         + "\","
                         + "\"ErrorTypes\": [{"
-                        + getErrorTypeInJSON(
-                                "IKE_PROTOCOL_ERROR_TYPE",
-                                new String[] {"WRONG_ERROR_DETAIL"},
-                                new String[] {"4", "8", "16"},
-                                new String[] {"APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("IKE_PROTOCOL_ERROR_TYPE")
+                                .setErrorDetails(List.of("WRONG_ERROR_DETAIL"))
+                                .setRetryArray(List.of("4", "8", "16"))
+                                .setUnthrottlingEvents(
+                                        List.of("APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}]"
                         + "}]";
 
@@ -214,13 +309,16 @@
                 .mHandler
                 .obtainMessage(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT)
                 .sendToTarget();
-
-        sleep(1000);
+        mTestLooper.dispatchAll();
 
         // Fallback to default Iwlan error policy for IKE_PROTOCOL_ERROR_TYPE(24) because of failed
         // parsing (or lack of explicit carrier-defined policy).
         IwlanError iwlanError = buildIwlanIkeAuthFailedError();
         long time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(5, time);
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(10, time);
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
         assertEquals(10, time);
         time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
         assertEquals(20, time);
@@ -228,10 +326,30 @@
         assertEquals(40, time);
         time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
         assertEquals(80, time);
+
+        iwlanError = buildIwlanIkeProtocolError(9002);
         time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
-        assertEquals(160, time);
+        assertEquals(5, time);
         time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
-        assertEquals(86400, time);
+        assertEquals(10, time);
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(10, time);
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(20, time);
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(40, time);
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(80, time);
+
+        iwlanError = buildIwlanIkeInternalAddressFailure();
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(0, time);
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(0, time);
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(0, time);
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(10, time);
     }
 
     @Test
@@ -243,17 +361,22 @@
                         + apn
                         + "\","
                         + "\"ErrorTypes\": [{"
-                        + getErrorTypeInJSON(
-                                "IKE_PROTOCOL_ERROR_TYPE",
-                                new String[] {"24", "34"},
-                                new String[] {"4", "8", "16"},
-                                new String[] {"APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("IKE_PROTOCOL_ERROR_TYPE")
+                                .setErrorDetails(List.of("24", "34"))
+                                .setRetryArray(List.of("4", "8", "16"))
+                                .setUnthrottlingEvents(
+                                        List.of("APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}, {"
-                        + getErrorTypeInJSON(
-                                "IKE_PROTOCOL_ERROR_TYPE",
-                                new String[] {"*"},
-                                new String[] {"0"},
-                                new String[] {"APM_ENABLE_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("IKE_PROTOCOL_ERROR_TYPE")
+                                .setErrorDetails(List.of("*"))
+                                .setRetryArray(List.of("0"))
+                                .setUnthrottlingEvents(List.of("APM_ENABLE_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}]"
                         + "}]";
         PersistableBundle bundle = new PersistableBundle();
@@ -263,8 +386,7 @@
                 .mHandler
                 .obtainMessage(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT)
                 .sendToTarget();
-
-        sleep(1000);
+        mTestLooper.dispatchAll();
 
         mErrorPolicyManager.logErrorPolicies();
 
@@ -297,17 +419,22 @@
                         + apn
                         + "\","
                         + "\"ErrorTypes\": [{"
-                        + getErrorTypeInJSON(
-                                "IKE_PROTOCOL_ERROR_TYPE",
-                                new String[] {"24", "34"},
-                                new String[] {"4", "8", "16"},
-                                new String[] {"APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("IKE_PROTOCOL_ERROR_TYPE")
+                                .setErrorDetails(List.of("24", "34"))
+                                .setRetryArray(List.of("4", "8", "16"))
+                                .setUnthrottlingEvents(
+                                        List.of("APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}, {"
-                        + getErrorTypeInJSON(
-                                "GENERIC_ERROR_TYPE",
-                                new String[] {"SERVER_SELECTION_FAILED"},
-                                new String[] {"0"},
-                                new String[] {"APM_ENABLE_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("GENERIC_ERROR_TYPE")
+                                .setErrorDetails(List.of("SERVER_SELECTION_FAILED"))
+                                .setRetryArray(List.of("0"))
+                                .setUnthrottlingEvents(List.of("APM_ENABLE_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}]"
                         + "}]";
         PersistableBundle bundle = new PersistableBundle();
@@ -317,8 +444,7 @@
                 .mHandler
                 .obtainMessage(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT)
                 .sendToTarget();
-
-        sleep(1000);
+        mTestLooper.dispatchAll();
 
         // IKE_PROTOCOL_ERROR_TYPE(24) and retryArray = 4,8,16
         IwlanError iwlanError = buildIwlanIkeAuthFailedError();
@@ -328,7 +454,7 @@
         boolean bringUpTunnel = mErrorPolicyManager.canBringUpTunnel(apn);
         assertFalse(bringUpTunnel);
 
-        sleep(4000);
+        advanceClockByTimeMs(4000);
 
         bringUpTunnel = mErrorPolicyManager.canBringUpTunnel(apn);
         assertTrue(bringUpTunnel);
@@ -349,17 +475,22 @@
                         + apn
                         + "\","
                         + "\"ErrorTypes\": [{"
-                        + getErrorTypeInJSON(
-                                "IKE_PROTOCOL_ERROR_TYPE",
-                                new String[] {"24", "34"},
-                                new String[] {"4", "8", "16"},
-                                new String[] {"APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("IKE_PROTOCOL_ERROR_TYPE")
+                                .setErrorDetails(List.of("24", "34"))
+                                .setRetryArray(List.of("4", "8", "16"))
+                                .setUnthrottlingEvents(
+                                        List.of("APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}, {"
-                        + getErrorTypeInJSON(
-                                "GENERIC_ERROR_TYPE",
-                                new String[] {"SERVER_SELECTION_FAILED"},
-                                new String[] {"0"},
-                                new String[] {"APM_ENABLE_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("GENERIC_ERROR_TYPE")
+                                .setErrorDetails(List.of("SERVER_SELECTION_FAILED"))
+                                .setRetryArray(List.of("0"))
+                                .setUnthrottlingEvents(List.of("APM_ENABLE_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}]"
                         + "}]";
         PersistableBundle bundle = new PersistableBundle();
@@ -369,8 +500,7 @@
                 .mHandler
                 .obtainMessage(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT)
                 .sendToTarget();
-
-        sleep(1000);
+        mTestLooper.dispatchAll();
 
         // IKE_PROTOCOL_ERROR_TYPE(24) and retryArray = 4,8,16
         IwlanError iwlanError = buildIwlanIkeAuthFailedError();
@@ -396,17 +526,22 @@
                         + apn
                         + "\","
                         + "\"ErrorTypes\": [{"
-                        + getErrorTypeInJSON(
-                                "IKE_PROTOCOL_ERROR_TYPE",
-                                new String[] {"24", "34"},
-                                new String[] {"6", "12", "24"},
-                                new String[] {"APM_ENABLE_EVENT", "WIFI_DISABLE_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("IKE_PROTOCOL_ERROR_TYPE")
+                                .setErrorDetails(List.of("24", "34"))
+                                .setRetryArray(List.of("6", "12", "24"))
+                                .setUnthrottlingEvents(
+                                        List.of("APM_ENABLE_EVENT", "WIFI_DISABLE_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}, {"
-                        + getErrorTypeInJSON(
-                                "GENERIC_ERROR_TYPE",
-                                new String[] {"SERVER_SELECTION_FAILED"},
-                                new String[] {"0"},
-                                new String[] {"APM_DISABLE_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("GENERIC_ERROR_TYPE")
+                                .setErrorDetails(List.of("SERVER_SELECTION_FAILED"))
+                                .setRetryArray(List.of("0"))
+                                .setUnthrottlingEvents(List.of("APM_ENABLE_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}]"
                         + "}]";
         PersistableBundle bundle = new PersistableBundle();
@@ -416,7 +551,7 @@
                 .mHandler
                 .obtainMessage(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT)
                 .sendToTarget();
-        sleep(1000);
+        mTestLooper.dispatchAll();
 
         // IKE_PROTOCOL_ERROR_TYPE(24) and retryArray = 6, 12, 24
         IwlanError iwlanError = buildIwlanIkeAuthFailedError();
@@ -427,7 +562,7 @@
                 .mHandler
                 .obtainMessage(IwlanEventListener.WIFI_DISABLE_EVENT)
                 .sendToTarget();
-        sleep(500);
+        advanceClockByTimeMs(500);
         verify(mMockDataServiceProvider, times(1)).notifyApnUnthrottled(eq(apn));
 
         boolean bringUpTunnel = mErrorPolicyManager.canBringUpTunnel(apn);
@@ -447,17 +582,22 @@
                         + apn
                         + "\","
                         + "\"ErrorTypes\": [{"
-                        + getErrorTypeInJSON(
-                                "IKE_PROTOCOL_ERROR_TYPE",
-                                new String[] {"24", "34"},
-                                new String[] {"6", "12", "24"},
-                                new String[] {"WIFI_CALLING_DISABLE_EVENT", "WIFI_DISABLE_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("IKE_PROTOCOL_ERROR_TYPE")
+                                .setErrorDetails(List.of("24", "34"))
+                                .setRetryArray(List.of("6", "12", "24"))
+                                .setUnthrottlingEvents(
+                                        List.of("WIFI_CALLING_DISABLE_EVENT", "WIFI_DISABLE_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}, {"
-                        + getErrorTypeInJSON(
-                                "GENERIC_ERROR_TYPE",
-                                new String[] {"SERVER_SELECTION_FAILED"},
-                                new String[] {"0"},
-                                new String[] {"APM_DISABLE_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("GENERIC_ERROR_TYPE")
+                                .setErrorDetails(List.of("SERVER_SELECTION_FAILED"))
+                                .setRetryArray(List.of("0"))
+                                .setUnthrottlingEvents(List.of("APM_ENABLE_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}]"
                         + "}]";
         PersistableBundle bundle = new PersistableBundle();
@@ -467,7 +607,7 @@
                 .mHandler
                 .obtainMessage(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT)
                 .sendToTarget();
-        sleep(1000);
+        mTestLooper.dispatchAll();
 
         // IKE_PROTOCOL_ERROR_TYPE(24) and retryArray = 6, 12, 24
         IwlanError iwlanError = buildIwlanIkeAuthFailedError();
@@ -478,7 +618,7 @@
                 .mHandler
                 .obtainMessage(IwlanEventListener.WIFI_CALLING_DISABLE_EVENT)
                 .sendToTarget();
-        sleep(500);
+        advanceClockByTimeMs(500);
         verify(mMockDataServiceProvider, times(1)).notifyApnUnthrottled(eq(apn));
 
         boolean bringUpTunnel = mErrorPolicyManager.canBringUpTunnel(apn);
@@ -498,17 +638,22 @@
                         + apn
                         + "\","
                         + "\"ErrorTypes\": [{"
-                        + getErrorTypeInJSON(
-                                "IKE_PROTOCOL_ERROR_TYPE",
-                                new String[] {"24", "34"},
-                                new String[] {"4", "8", "16"},
-                                new String[] {"APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("IKE_PROTOCOL_ERROR_TYPE")
+                                .setErrorDetails(List.of("24", "34"))
+                                .setRetryArray(List.of("4", "8", "16"))
+                                .setUnthrottlingEvents(
+                                        List.of("APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}, {"
-                        + getErrorTypeInJSON(
-                                "GENERIC_ERROR_TYPE",
-                                new String[] {"SERVER_SELECTION_FAILED"},
-                                new String[] {"0"},
-                                new String[] {"APM_DISABLE_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("GENERIC_ERROR_TYPE")
+                                .setErrorDetails(List.of("SERVER_SELECTION_FAILED"))
+                                .setRetryArray(List.of("0"))
+                                .setUnthrottlingEvents(List.of("APM_ENABLE_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}]"
                         + "}]";
         PersistableBundle bundle = new PersistableBundle();
@@ -518,7 +663,7 @@
                 .mHandler
                 .obtainMessage(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT)
                 .sendToTarget();
-        sleep(1000);
+        mTestLooper.dispatchAll();
 
         // IKE_PROTOCOL_ERROR_TYPE(24) and retryArray = 4,8,16
         IwlanError iwlanError = buildIwlanIkeAuthFailedError();
@@ -529,7 +674,7 @@
                 .mHandler
                 .obtainMessage(IwlanEventListener.APM_ENABLE_EVENT)
                 .sendToTarget();
-        sleep(500);
+        advanceClockByTimeMs(500);
         verify(mMockDataServiceProvider, times(1)).notifyApnUnthrottled(eq(apn));
 
         boolean bringUpTunnel = mErrorPolicyManager.canBringUpTunnel(apn);
@@ -550,17 +695,22 @@
                         + apn1
                         + "\","
                         + "\"ErrorTypes\": [{"
-                        + getErrorTypeInJSON(
-                                "IKE_PROTOCOL_ERROR_TYPE",
-                                new String[] {"24", "34"},
-                                new String[] {"4", "8", "16"},
-                                new String[] {"APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("IKE_PROTOCOL_ERROR_TYPE")
+                                .setErrorDetails(List.of("24", "34"))
+                                .setRetryArray(List.of("4", "8", "16"))
+                                .setUnthrottlingEvents(
+                                        List.of("APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}, {"
-                        + getErrorTypeInJSON(
-                                "GENERIC_ERROR_TYPE",
-                                new String[] {"SERVER_SELECTION_FAILED"},
-                                new String[] {"0"},
-                                new String[] {"APM_ENABLE_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("GENERIC_ERROR_TYPE")
+                                .setErrorDetails(List.of("SERVER_SELECTION_FAILED"))
+                                .setRetryArray(List.of("0"))
+                                .setUnthrottlingEvents(List.of("APM_ENABLE_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}]"
                         + "}]";
         PersistableBundle bundle = new PersistableBundle();
@@ -570,8 +720,7 @@
                 .mHandler
                 .obtainMessage(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT)
                 .sendToTarget();
-
-        sleep(1000);
+        mTestLooper.dispatchAll();
 
         // IKE_PROTOCOL_ERROR_TYPE(24) and retryArray = 4,8,16
         IwlanError iwlanError = buildIwlanIkeAuthFailedError();
@@ -607,17 +756,22 @@
                         + apn
                         + "\","
                         + "\"ErrorTypes\": [{"
-                        + getErrorTypeInJSON(
-                                "IKE_PROTOCOL_ERROR_TYPE",
-                                new String[] {"24", "34"},
-                                new String[] {"10", "15", "20"},
-                                new String[] {"APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("IKE_PROTOCOL_ERROR_TYPE")
+                                .setErrorDetails(List.of("24", "34"))
+                                .setRetryArray(List.of("10", "15", "20"))
+                                .setUnthrottlingEvents(
+                                        List.of("APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}, {"
-                        + getErrorTypeInJSON(
-                                "GENERIC_ERROR_TYPE",
-                                new String[] {"SERVER_SELECTION_FAILED"},
-                                new String[] {"0"},
-                                new String[] {"APM_ENABLE_EVENT"})
+                        + ErrorPolicyString.builder()
+                                .setErrorType("GENERIC_ERROR_TYPE")
+                                .setErrorDetails(List.of("SERVER_SELECTION_FAILED"))
+                                .setRetryArray(List.of("0"))
+                                .setUnthrottlingEvents(List.of("APM_ENABLE_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
                         + "}]"
                         + "}]";
 
@@ -628,8 +782,7 @@
                 .mHandler
                 .obtainMessage(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT)
                 .sendToTarget();
-
-        sleep(1000);
+        mTestLooper.dispatchAll();
 
         // IKE_PROTOCOL_ERROR_TYPE(24) and retryArray = 4,8,16
         IwlanError iwlanError = buildIwlanIkeAuthFailedError();
@@ -638,9 +791,9 @@
         time = Math.round((double) mErrorPolicyManager.getCurrentRetryTimeMs(apn) / 1000);
         assertEquals(time, 2);
 
-        // sleep for 2 seconds and make sure that we can bring up tunnel after 2 secs
+        // advanceClockByTimeMs for 2 seconds and make sure that we can bring up tunnel after 2 secs
         // as back off time - 2 secs should override the retry time in policy - 10 secs
-        sleep(2000);
+        advanceClockByTimeMs(2000);
         boolean bringUpTunnel = mErrorPolicyManager.canBringUpTunnel(apn);
         assertTrue(bringUpTunnel);
 
@@ -661,6 +814,207 @@
     }
 
     @Test
+    public void testErrorPolicyWithNumAttemptsPerFqdn() throws Exception {
+        // ErrorPolicyManager#getCurrentFqdnIndex() is tested when the ErrorType
+        // parameter "NumAttemptsPerFqdn" is configured.
+        String apn = "ims";
+        String config =
+                "[{"
+                        + "\"ApnName\": \""
+                        + apn
+                        + "\","
+                        + "\"ErrorTypes\": [{"
+                        + ErrorPolicyString.builder()
+                                .setErrorType("IKE_PROTOCOL_ERROR_TYPE")
+                                .setErrorDetails(List.of("15500")) /* CONGESTION */
+                                .setRetryArray(
+                                        List.of(
+                                                "0", "0", "300", "600", "1200", "0", "0", "0",
+                                                "300", "600", "1200", "-1"))
+                                .setUnthrottlingEvents(
+                                        List.of(
+                                                "APM_ENABLE_EVENT",
+                                                "WIFI_DISABLE_EVENT",
+                                                "WIFI_CALLING_DISABLE_EVENT"))
+                                .setNumAttemptsPerFqdn("6")
+                                .build()
+                                .getErrorPolicyInString()
+                        + "}]"
+                        + "}]";
+
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putString(ErrorPolicyManager.KEY_ERROR_POLICY_CONFIG_STRING, config);
+        setupMockForCarrierConfig(bundle);
+        mErrorPolicyManager
+                .mHandler
+                .obtainMessage(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT)
+                .sendToTarget();
+        mTestLooper.dispatchAll();
+        assertEquals(DataFailCause.NONE, mErrorPolicyManager.getMostRecentDataFailCause());
+
+        // IKE_PROTOCOL_ERROR_TYPE(15500)
+        // UE constructs 2 PLMN FQDNs.
+        IwlanError iwlanError = buildIwlanIkeProtocolError(15500 /* CONGESTION */);
+
+        long time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(0, time);
+        assertEquals(
+                DataFailCause.IWLAN_CONGESTION, mErrorPolicyManager.getMostRecentDataFailCause());
+
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(0, time);
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(300, time);
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(600, time);
+
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(1200, time);
+        assertEquals(0, mErrorPolicyManager.getCurrentFqdnIndex(2));
+
+        // Cycles to next FQDN
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(0, time);
+        assertEquals(1, mErrorPolicyManager.getCurrentFqdnIndex(2));
+
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(0, time);
+        assertEquals(1, mErrorPolicyManager.getCurrentFqdnIndex(2));
+
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(0, time);
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(300, time);
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(600, time);
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(1200, time);
+
+        // Steady state retry duration, cycles back to 1st FQDN.
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(1200, time);
+        assertEquals(0, mErrorPolicyManager.getCurrentFqdnIndex(2));
+    }
+
+    @Test
+    public void testShouldRetryWithInitialAttach() throws Exception {
+        String apn = "ims";
+        String config =
+                "[{"
+                        + "\"ApnName\": \""
+                        + apn
+                        + "\","
+                        + "\"ErrorTypes\": [{"
+                        + ErrorPolicyString.builder()
+                                .setErrorType("IKE_PROTOCOL_ERROR_TYPE")
+                                .setErrorDetails(List.of("24", "34"))
+                                .setRetryArray(List.of("4", "8", "16"))
+                                .setUnthrottlingEvents(
+                                        List.of("APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"))
+                                .setHandoverAttemptCount("2")
+                                .build()
+                                .getErrorPolicyInString()
+                        + "}]"
+                        + "}]";
+
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putString(ErrorPolicyManager.KEY_ERROR_POLICY_CONFIG_STRING, config);
+        setupMockForCarrierConfig(bundle);
+        mErrorPolicyManager
+                .mHandler
+                .obtainMessage(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT)
+                .sendToTarget();
+        mTestLooper.dispatchAll();
+
+        // IKE_PROTOCOL_ERROR_TYPE(24) and retryArray = 4,8,16
+        IwlanError iwlanError = buildIwlanIkeAuthFailedError();
+        long time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(4, time);
+        assertFalse(mErrorPolicyManager.shouldRetryWithInitialAttach(apn));
+
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(8, time);
+        // Reached handover attempt count and error is IKE protocol error
+        assertTrue(mErrorPolicyManager.shouldRetryWithInitialAttach(apn));
+    }
+
+    @Test
+    public void testShouldRetryWithInitialAttachForInternalError() throws Exception {
+        String apn = "ims";
+        String config =
+                "[{"
+                        + "\"ApnName\": \""
+                        + apn
+                        + "\","
+                        + "\"ErrorTypes\": [{"
+                        + ErrorPolicyString.builder()
+                                .setErrorType("IKE_PROTOCOL_ERROR_TYPE")
+                                .setErrorDetails(List.of("24", "34"))
+                                .setRetryArray(List.of("4", "8", "16"))
+                                .setUnthrottlingEvents(
+                                        List.of("APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"))
+                                .setHandoverAttemptCount("2")
+                                .build()
+                                .getErrorPolicyInString()
+                        + "}, {"
+                        + ErrorPolicyString.builder()
+                                .setErrorType("GENERIC_ERROR_TYPE")
+                                .setErrorDetails(List.of("SERVER_SELECTION_FAILED"))
+                                .setRetryArray(List.of("0", "0"))
+                                .setUnthrottlingEvents(List.of("APM_ENABLE_EVENT"))
+                                .build()
+                                .getErrorPolicyInString()
+                        + "}]"
+                        + "}]";
+
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putString(ErrorPolicyManager.KEY_ERROR_POLICY_CONFIG_STRING, config);
+        setupMockForCarrierConfig(bundle);
+        mErrorPolicyManager
+                .mHandler
+                .obtainMessage(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT)
+                .sendToTarget();
+        mTestLooper.dispatchAll();
+
+        // GENERIC_PROTOCOL_ERROR_TYPE - SERVER_SELECTION_FAILED and retryArray = 0, 0
+        IwlanError iwlanError = new IwlanError(IwlanError.EPDG_SELECTOR_SERVER_SELECTION_FAILED);
+        long time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(0, time);
+        assertFalse(mErrorPolicyManager.shouldRetryWithInitialAttach(apn));
+
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(0, time);
+        // Should not retry with initial attach as the errors are not IKE_PROTOCOL_ERROR_TYPE
+        assertFalse(mErrorPolicyManager.shouldRetryWithInitialAttach(apn));
+    }
+
+    @Test
+    public void testHandoverAttemptCountInvalidErrorType() throws Exception {
+        String apn = "ims";
+        String config =
+                "[{"
+                        + "\"ApnName\": \""
+                        + apn
+                        + "\","
+                        + "\"ErrorTypes\": [{"
+                        + ErrorPolicyString.builder()
+                                .setErrorType("GENERIC_ERROR_TYPE")
+                                .setErrorDetails(List.of("*"))
+                                .setRetryArray(List.of("4", "8", "16"))
+                                .setUnthrottlingEvents(
+                                        List.of("APM_ENABLE_EVENT", "WIFI_AP_CHANGED_EVENT"))
+                                .setHandoverAttemptCount("2")
+                                .build()
+                                .getErrorPolicyInString()
+                        + "}]"
+                        + "}]";
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> mErrorPolicyManager.readErrorPolicies(new JSONArray(config)));
+    }
+
+    @Test
     public void testErrorStats() throws Exception {
         String apn1 = "ims";
         String apn2 = "mms";
@@ -701,31 +1055,9 @@
         assertEquals(resultServerApn2, serverSelectionCountApn2);
     }
 
-    private String getErrorTypeInJSON(
-            String ErrorType,
-            String[] errorDetails,
-            String[] retryArray,
-            String[] unthrottlingEvents) {
-        return "\"ErrorType\": \""
-                + ErrorType
-                + "\","
-                + "\"ErrorDetails\": [\""
-                + String.join("\", \"", errorDetails)
-                + "\"],"
-                + "\"RetryArray\": [\""
-                + String.join("\", \"", retryArray)
-                + "\"],"
-                + "\"UnthrottlingEvents\": [\""
-                + String.join("\", \"", unthrottlingEvents)
-                + "\"]";
-    }
-
-    private void sleep(long time) {
-        try {
-            Thread.sleep(time);
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
+    private void advanceClockByTimeMs(long time) {
+        mMockedClockTime += time;
+        mTestLooper.dispatchAll();
     }
 
     private void setupMockForCarrierConfig(PersistableBundle bundle) {
diff --git a/test/com/google/android/iwlan/IwlanDataServiceTest.java b/test/com/google/android/iwlan/IwlanDataServiceTest.java
index a0414de..2f8f2c3 100644
--- a/test/com/google/android/iwlan/IwlanDataServiceTest.java
+++ b/test/com/google/android/iwlan/IwlanDataServiceTest.java
@@ -17,21 +17,46 @@
 package com.google.android.iwlan;
 
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.isNull;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+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.content.ContentResolver;
 import android.content.Context;
 import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.ipsec.ike.exceptions.IkeInternalException;
+import android.net.vcn.VcnTransportInfo;
+import android.os.test.TestLooper;
 import android.telephony.AccessNetworkConstants.AccessNetworkType;
 import android.telephony.DataFailCause;
 import android.telephony.SubscriptionInfo;
@@ -49,12 +74,12 @@
 import com.google.android.iwlan.IwlanDataService.IwlanDataServiceProvider;
 import com.google.android.iwlan.IwlanDataService.IwlanDataServiceProvider.IwlanTunnelCallback;
 import com.google.android.iwlan.IwlanDataService.IwlanDataServiceProvider.TunnelState;
-import com.google.android.iwlan.IwlanDataService.IwlanNetworkMonitorCallback;
 import com.google.android.iwlan.epdg.EpdgSelector;
 import com.google.android.iwlan.epdg.EpdgTunnelManager;
 import com.google.android.iwlan.epdg.TunnelLinkProperties;
 import com.google.android.iwlan.epdg.TunnelLinkPropertiesTest;
 import com.google.android.iwlan.epdg.TunnelSetupRequest;
+import com.google.android.iwlan.proto.MetricsAtom;
 
 import org.junit.After;
 import org.junit.Before;
@@ -65,11 +90,13 @@
 import org.mockito.MockitoSession;
 import org.mockito.quality.Strictness;
 
+import java.lang.reflect.Method;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.util.ArrayList;
 import java.util.Calendar;
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.LongSummaryStatistics;
@@ -79,6 +106,7 @@
 public class IwlanDataServiceTest {
     private static final int DEFAULT_SLOT_INDEX = 0;
     private static final int DEFAULT_SUB_INDEX = 0;
+    private static final int INVALID_SUB_INDEX = -1;
     private static final int LINK_MTU = 1280;
     private static final String TEST_APN_NAME = "ims";
     private static final String IP_ADDRESS = "192.0.2.1";
@@ -96,26 +124,29 @@
     @Mock private EpdgTunnelManager mMockEpdgTunnelManager;
     @Mock private IwlanDataServiceProvider mMockIwlanDataServiceProvider;
     @Mock private Network mMockNetwork;
-    @Mock private NetworkCapabilities mMockNetworkCapabilities;
     @Mock private TunnelLinkProperties mMockTunnelLinkProperties;
     @Mock private ErrorPolicyManager mMockErrorPolicyManager;
     @Mock private ImsManager mMockImsManager;
     @Mock private ImsMmTelManager mMockImsMmTelManager;
     @Mock private TelephonyManager mMockTelephonyManager;
     @Mock private EpdgSelector mMockEpdgSelector;
-    @Mock private LinkProperties mMockLinkProperties;
     @Mock private LinkAddress mMockIPv4LinkAddress;
     @Mock private LinkAddress mMockIPv6LinkAddress;
     @Mock private Inet4Address mMockInet4Address;
     @Mock private Inet6Address mMockInet6Address;
+
     MockitoSession mStaticMockSession;
 
+    private LinkProperties mLinkProperties;
     private List<DataCallResponse> mResultDataCallList;
     private @DataServiceCallback.ResultCode int mResultCode;
     private CountDownLatch latch;
     private IwlanDataService mIwlanDataService;
-    private IwlanDataServiceProvider mIwlanDataServiceProvider;
     private IwlanDataServiceProvider mSpyIwlanDataServiceProvider;
+    private TestLooper mTestLooper = new TestLooper();
+    private long mMockedCalendarTime;
+    private ArgumentCaptor<NetworkCallback> mNetworkCallbackCaptor =
+            ArgumentCaptor.forClass(NetworkCallback.class);
 
     private final class IwlanDataServiceCallback extends IDataServiceCallback.Stub {
 
@@ -170,24 +201,32 @@
         mStaticMockSession =
                 mockitoSession()
                         .mockStatic(EpdgSelector.class)
+                        .mockStatic(EpdgTunnelManager.class)
                         .mockStatic(ErrorPolicyManager.class)
                         .mockStatic(IwlanBroadcastReceiver.class)
-                        .mockStatic(IwlanHelper.class)
+                        .mockStatic(SubscriptionManager.class)
                         .strictness(Strictness.LENIENT)
                         .startMocking();
 
         when(mMockContext.getSystemService(eq(ConnectivityManager.class)))
                 .thenReturn(mMockConnectivityManager);
-        when(mMockConnectivityManager.getNetworkCapabilities(eq(mMockNetwork)))
-                .thenReturn(mMockNetworkCapabilities);
-        when(mMockNetworkCapabilities.hasTransport(eq(TRANSPORT_CELLULAR))).thenReturn(false);
-        when(mMockNetworkCapabilities.hasTransport(eq(TRANSPORT_WIFI))).thenReturn(true);
 
         when(mMockContext.getSystemService(eq(SubscriptionManager.class)))
                 .thenReturn(mMockSubscriptionManager);
 
+        doNothing()
+                .when(mMockConnectivityManager)
+                .registerSystemDefaultNetworkCallback(mNetworkCallbackCaptor.capture(), any());
+
+        when(EpdgTunnelManager.getInstance(mMockContext, DEFAULT_SLOT_INDEX))
+                .thenReturn(mMockEpdgTunnelManager);
         when(mMockSubscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(anyInt()))
                 .thenReturn(mMockSubscriptionInfo);
+        when(mMockSubscriptionManager.getDefaultDataSubscriptionId()).thenReturn(DEFAULT_SUB_INDEX);
+        when(mMockSubscriptionManager.getSlotIndex(DEFAULT_SUB_INDEX))
+                .thenReturn(DEFAULT_SLOT_INDEX);
+        when(mMockSubscriptionManager.getSlotIndex(DEFAULT_SUB_INDEX + 1))
+                .thenReturn(DEFAULT_SLOT_INDEX + 1);
 
         when(mMockSubscriptionInfo.getSubscriptionId()).thenReturn(DEFAULT_SUB_INDEX);
 
@@ -214,50 +253,335 @@
         when(mMockIPv6LinkAddress.getAddress()).thenReturn(mMockInet6Address);
 
         mIwlanDataService = spy(new IwlanDataService());
+        // Injects the test looper into the IwlanDataServiceHandler
+        doReturn(mTestLooper.getLooper()).when(mIwlanDataService).getLooper();
         mIwlanDataService.setAppContext(mMockContext);
-        mIwlanDataServiceProvider =
-                (IwlanDataServiceProvider)
-                        mIwlanDataService.onCreateDataServiceProvider(DEFAULT_SLOT_INDEX);
-        mSpyIwlanDataServiceProvider = spy(mIwlanDataServiceProvider);
+        mSpyIwlanDataServiceProvider =
+                spy(
+                        (IwlanDataServiceProvider)
+                                mIwlanDataService.onCreateDataServiceProvider(DEFAULT_SLOT_INDEX));
+        mTestLooper.dispatchAll();
+
+        when(Calendar.getInstance().getTime()).thenAnswer(i -> mMockedCalendarTime);
+
+        mLinkProperties = new LinkProperties();
+        mLinkProperties.setInterfaceName("wlan0");
+        mLinkProperties.addLinkAddress(mMockIPv4LinkAddress);
+
+        when(mMockConnectivityManager.getLinkProperties(eq(mMockNetwork)))
+                .thenReturn(mLinkProperties);
     }
 
     @After
     public void cleanUp() throws Exception {
         mStaticMockSession.finishMocking();
-        mIwlanDataServiceProvider.close();
+        mSpyIwlanDataServiceProvider.close();
+        mTestLooper.dispatchAll();
         if (mIwlanDataService != null) {
             mIwlanDataService.onDestroy();
         }
     }
 
+    public Network createMockNetwork(LinkProperties linkProperties) {
+        Network network = mock(Network.class);
+        when(mMockConnectivityManager.getLinkProperties(eq(network))).thenReturn(linkProperties);
+        return network;
+    }
+
+    private NetworkCallback getNetworkMonitorCallback() {
+        return mNetworkCallbackCaptor.getValue();
+    }
+
+    private void onSystemDefaultNetworkConnected(
+            Network network, LinkProperties linkProperties, int transportType, int subId) {
+        NetworkCapabilities nc =
+                prepareNetworkCapabilitiesForTest(
+                        transportType,
+                        subId /* unused if transportType is TRANSPORT_WIFI */,
+                        false /* isVcn */);
+        NetworkCallback networkMonitorCallback = getNetworkMonitorCallback();
+        networkMonitorCallback.onCapabilitiesChanged(network, nc);
+        networkMonitorCallback.onLinkPropertiesChanged(network, linkProperties);
+        mTestLooper.dispatchAll();
+    }
+
+    private void onSystemDefaultNetworkConnected(int transportType) {
+        Network newNetwork = createMockNetwork(mLinkProperties);
+        onSystemDefaultNetworkConnected(
+                newNetwork, mLinkProperties, transportType, DEFAULT_SUB_INDEX);
+    }
+
+    private void onSystemDefaultNetworkLost() {
+        NetworkCallback networkMonitorCallback = getNetworkMonitorCallback();
+        networkMonitorCallback.onLost(mMockNetwork);
+        mTestLooper.dispatchAll();
+    }
+
     @Test
-    public void testWifiOnAvailable() {
-        IwlanNetworkMonitorCallback mNetworkMonitorCallback =
-                mIwlanDataService.getNetworkMonitorCallback();
-
-        mNetworkMonitorCallback.onAvailable(mMockNetwork);
-        boolean ret = mIwlanDataService.isNetworkConnected(true, false);
-
-        assertTrue(ret);
+    public void testWifiOnConnected() {
+        onSystemDefaultNetworkConnected(TRANSPORT_WIFI);
+        assertTrue(
+                mIwlanDataService.isNetworkConnected(
+                        false /* isActiveDataOnOtherSub */, false /* isCstEnabled */));
     }
 
     @Test
     public void testWifiOnLost() {
+        when(mMockIwlanDataServiceProvider.getSlotIndex()).thenReturn(DEFAULT_SLOT_INDEX + 1);
         mIwlanDataService.addIwlanDataServiceProvider(mMockIwlanDataServiceProvider);
-        IwlanNetworkMonitorCallback mNetworkMonitorCallback =
-                mIwlanDataService.getNetworkMonitorCallback();
 
-        mNetworkMonitorCallback.onLost(mMockNetwork);
-        boolean ret = mIwlanDataService.isNetworkConnected(true, false);
-
-        assertFalse(ret);
+        onSystemDefaultNetworkLost();
+        assertFalse(
+                mIwlanDataService.isNetworkConnected(
+                        false /* isActiveDataOnOtherSub */, false /* isCstEnabled */));
         verify(mMockIwlanDataServiceProvider).forceCloseTunnelsInDeactivatingState();
         mIwlanDataService.removeDataServiceProvider(mMockIwlanDataServiceProvider);
+        mTestLooper.dispatchAll();
+    }
+
+    @Test
+    public void testWifiOnReconnected() {
+        Network newNetwork = createMockNetwork(mLinkProperties);
+        onSystemDefaultNetworkConnected(
+                newNetwork, mLinkProperties, TRANSPORT_WIFI, INVALID_SUB_INDEX);
+        verify(mMockEpdgTunnelManager, times(1)).updateNetwork(eq(newNetwork), eq(mLinkProperties));
+
+        onSystemDefaultNetworkLost();
+        onSystemDefaultNetworkConnected(
+                newNetwork, mLinkProperties, TRANSPORT_WIFI, INVALID_SUB_INDEX);
+        verify(mMockEpdgTunnelManager, times(2)).updateNetwork(eq(newNetwork), eq(mLinkProperties));
+    }
+
+    @Test
+    public void testOnLinkPropertiesChangedForConnectedNetwork() {
+        NetworkCallback networkCallback = getNetworkMonitorCallback();
+        onSystemDefaultNetworkConnected(
+                mMockNetwork, mLinkProperties, TRANSPORT_WIFI, INVALID_SUB_INDEX);
+
+        clearInvocations(mMockEpdgTunnelManager);
+
+        LinkProperties newLinkProperties = new LinkProperties(mLinkProperties);
+        newLinkProperties.setInterfaceName("wlan0");
+        newLinkProperties.addLinkAddress(mMockIPv6LinkAddress);
+
+        networkCallback.onLinkPropertiesChanged(mMockNetwork, newLinkProperties);
+        verify(mMockEpdgTunnelManager, times(1))
+                .updateNetwork(eq(mMockNetwork), eq(newLinkProperties));
+    }
+
+    @Test
+    public void testOnLinkPropertiesChangedForNonConnectedNetwork() {
+        NetworkCallback networkCallback = getNetworkMonitorCallback();
+        onSystemDefaultNetworkConnected(
+                mMockNetwork, mLinkProperties, TRANSPORT_WIFI, INVALID_SUB_INDEX);
+
+        clearInvocations(mMockEpdgTunnelManager);
+
+        LinkProperties newLinkProperties = new LinkProperties();
+        newLinkProperties.setInterfaceName("wlan0");
+        newLinkProperties.addLinkAddress(mMockIPv6LinkAddress);
+        Network newNetwork = createMockNetwork(newLinkProperties);
+
+        networkCallback.onLinkPropertiesChanged(newNetwork, newLinkProperties);
+        verify(mMockEpdgTunnelManager, never())
+                .updateNetwork(eq(newNetwork), any(LinkProperties.class));
+    }
+
+    @Test
+    public void testOnLinkPropertiesChangedWithClatInstalled() throws Exception {
+        NetworkCallback networkCallback = getNetworkMonitorCallback();
+        mLinkProperties.setLinkAddresses(
+                new ArrayList<>(Collections.singletonList(mMockIPv6LinkAddress)));
+        onSystemDefaultNetworkConnected(
+                mMockNetwork, mLinkProperties, TRANSPORT_WIFI, INVALID_SUB_INDEX);
+
+        clearInvocations(mMockEpdgTunnelManager);
+
+        // LinkProperties#addStackedLink() is marked with @UnsupportedAppUsage
+        LinkProperties newLinkProperties = new LinkProperties(mLinkProperties);
+        newLinkProperties.setInterfaceName("wlan0");
+        LinkProperties stackedLink = new LinkProperties();
+        stackedLink.setInterfaceName("v4-wlan0");
+        stackedLink.addLinkAddress(mMockIPv4LinkAddress);
+        Class<?>[] parameterTypes = new Class<?>[] {LinkProperties.class};
+        Object[] args = new Object[] {stackedLink};
+        callUnsupportedAppUsageMethod(newLinkProperties, "addStackedLink", parameterTypes, args);
+        assertNotEquals(mLinkProperties, newLinkProperties);
+
+        networkCallback.onLinkPropertiesChanged(mMockNetwork, newLinkProperties);
+        verify(mMockEpdgTunnelManager, times(1))
+                .updateNetwork(eq(mMockNetwork), eq(newLinkProperties));
+    }
+
+    @Test
+    public void testOnLinkPropertiesChangedForBringingUpIkeSession() {
+        DataProfile dp = buildImsDataProfile();
+
+        NetworkCallback networkCallback = getNetworkMonitorCallback();
+        onSystemDefaultNetworkConnected(
+                mMockNetwork, mLinkProperties, TRANSPORT_WIFI, INVALID_SUB_INDEX);
+
+        clearInvocations(mMockEpdgTunnelManager);
+
+        mSpyIwlanDataServiceProvider.setTunnelState(
+                dp,
+                mMockDataServiceCallback,
+                TunnelState.TUNNEL_IN_BRINGUP,
+                null, /* linkProperties */
+                false /* isHandover */,
+                1 /* pduSessionId */,
+                true /* isImsOrEmergency */);
+
+        LinkProperties newLinkProperties = new LinkProperties(mLinkProperties);
+        newLinkProperties.setInterfaceName("wlan0");
+        newLinkProperties.addLinkAddress(mMockIPv6LinkAddress);
+
+        networkCallback.onLinkPropertiesChanged(mMockNetwork, newLinkProperties);
+        verify(mMockEpdgTunnelManager, times(1))
+                .updateNetwork(eq(mMockNetwork), eq(newLinkProperties));
+        verify(mMockEpdgTunnelManager, never()).closeTunnel(any(), anyBoolean(), any(), any());
+    }
+
+    @Test
+    public void testNetworkNotConnectedWithCellularOnSameSubAndCrossSimEnabled()
+            throws InterruptedException {
+        NetworkCapabilities nc =
+                prepareNetworkCapabilitiesForTest(
+                        TRANSPORT_CELLULAR, DEFAULT_SUB_INDEX, false /* isVcn */);
+        getNetworkMonitorCallback().onCapabilitiesChanged(mMockNetwork, nc);
+
+        boolean isActiveDataOnOtherSub =
+                mIwlanDataService.isActiveDataOnOtherSub(DEFAULT_SLOT_INDEX);
+
+        assertFalse(isActiveDataOnOtherSub);
+        assertFalse(
+                mIwlanDataService.isNetworkConnected(
+                        isActiveDataOnOtherSub, true /* isCstEnabled */));
+    }
+
+    @Test
+    public void testCrossSimNetworkConnectedWithCellularOnDifferentSub()
+            throws InterruptedException {
+        NetworkCapabilities nc =
+                prepareNetworkCapabilitiesForTest(
+                        TRANSPORT_CELLULAR, DEFAULT_SUB_INDEX + 1, false /* isVcn */);
+        getNetworkMonitorCallback().onCapabilitiesChanged(mMockNetwork, nc);
+
+        boolean isActiveDataOnOtherSub =
+                mIwlanDataService.isActiveDataOnOtherSub(DEFAULT_SLOT_INDEX);
+
+        assertTrue(isActiveDataOnOtherSub);
+        assertTrue(
+                mIwlanDataService.isNetworkConnected(
+                        isActiveDataOnOtherSub, true /* isCstEnabled */));
+    }
+
+    @Test
+    public void testCrossSimNetworkConnectedWithVcnCellularOnDifferentSub()
+            throws InterruptedException {
+        NetworkCapabilities nc =
+                prepareNetworkCapabilitiesForTest(
+                        TRANSPORT_CELLULAR, DEFAULT_SUB_INDEX + 1, true /* isVcn */);
+        getNetworkMonitorCallback().onCapabilitiesChanged(mMockNetwork, nc);
+
+        boolean isActiveDataOnOtherSub =
+                mIwlanDataService.isActiveDataOnOtherSub(DEFAULT_SLOT_INDEX);
+
+        assertTrue(isActiveDataOnOtherSub);
+        assertTrue(
+                mIwlanDataService.isNetworkConnected(
+                        isActiveDataOnOtherSub, true /* isCstEnabled */));
+    }
+
+    @Test
+    public void testOnCrossSimCallingEnable_doNotUpdateTunnelManagerIfCellularDataOnSameSub()
+            throws Exception {
+        when(mMockImsMmTelManager.isCrossSimCallingEnabled()).thenReturn(true);
+
+        Network newNetwork = createMockNetwork(mLinkProperties);
+        onSystemDefaultNetworkConnected(
+                newNetwork, mLinkProperties, TRANSPORT_CELLULAR, DEFAULT_SUB_INDEX);
+
+        mIwlanDataService
+                .mIwlanDataServiceHandler
+                .obtainMessage(
+                        IwlanEventListener.CROSS_SIM_CALLING_ENABLE_EVENT,
+                        DEFAULT_SLOT_INDEX,
+                        0 /* unused */)
+                .sendToTarget();
+        mTestLooper.dispatchAll();
+        verify(mMockEpdgTunnelManager, never())
+                .updateNetwork(eq(newNetwork), any(LinkProperties.class));
+    }
+
+    @Test
+    public void testOnCrossSimCallingEnable_updateTunnelManagerIfCellularDataOnDifferentSub()
+            throws Exception {
+        when(mMockImsMmTelManager.isCrossSimCallingEnabled()).thenReturn(true);
+
+        Network newNetwork = createMockNetwork(mLinkProperties);
+        onSystemDefaultNetworkConnected(
+                newNetwork, mLinkProperties, TRANSPORT_CELLULAR, DEFAULT_SUB_INDEX + 1);
+        verify(mMockEpdgTunnelManager, times(1)).updateNetwork(eq(newNetwork), eq(mLinkProperties));
+
+        mIwlanDataService
+                .mIwlanDataServiceHandler
+                .obtainMessage(
+                        IwlanEventListener.CROSS_SIM_CALLING_ENABLE_EVENT,
+                        DEFAULT_SLOT_INDEX,
+                        0 /* unused */)
+                .sendToTarget();
+        mTestLooper.dispatchAll();
+        verify(mMockEpdgTunnelManager, times(2)).updateNetwork(eq(newNetwork), eq(mLinkProperties));
+    }
+
+    @Test
+    public void testOnCrossSimCallingEnable_doNotUpdateTunnelManagerIfNoNetwork() throws Exception {
+        when(mMockImsMmTelManager.isCrossSimCallingEnabled()).thenReturn(true);
+        onSystemDefaultNetworkLost();
+
+        mIwlanDataService
+                .mIwlanDataServiceHandler
+                .obtainMessage(
+                        IwlanEventListener.CROSS_SIM_CALLING_ENABLE_EVENT,
+                        DEFAULT_SLOT_INDEX,
+                        0 /* unused */)
+                .sendToTarget();
+        mTestLooper.dispatchAll();
+        verify(mMockEpdgTunnelManager, never())
+                .updateNetwork(any(Network.class), any(LinkProperties.class));
+    }
+
+    @Test
+    public void testOnEthernetConnection_doNotUpdateTunnelManager() throws Exception {
+        Network newNetwork = createMockNetwork(mLinkProperties);
+        onSystemDefaultNetworkConnected(
+                newNetwork, mLinkProperties, TRANSPORT_ETHERNET, DEFAULT_SUB_INDEX);
+        verify(mMockEpdgTunnelManager, never())
+                .updateNetwork(eq(newNetwork), any(LinkProperties.class));
+    }
+
+    @Test
+    public void testAddDuplicateDataServiceProviderThrows() throws Exception {
+        when(mMockIwlanDataServiceProvider.getSlotIndex()).thenReturn(DEFAULT_SLOT_INDEX);
+        assertThrows(
+                IllegalStateException.class,
+                () -> mIwlanDataService.addIwlanDataServiceProvider(mMockIwlanDataServiceProvider));
+    }
+
+    @Test
+    public void testRemoveDataServiceProvider() {
+        when(mMockIwlanDataServiceProvider.getSlotIndex()).thenReturn(DEFAULT_SLOT_INDEX);
+        mIwlanDataService.removeDataServiceProvider(mMockIwlanDataServiceProvider);
+        mTestLooper.dispatchAll();
+        verify(mIwlanDataService, times(1)).deinitNetworkCallback();
+        mIwlanDataService.onCreateDataServiceProvider(DEFAULT_SLOT_INDEX);
+        mTestLooper.dispatchAll();
     }
 
     @Test
     public void testRequestDataCallListPass() throws Exception {
-        DataProfile dp = buildDataProfile();
+        DataProfile dp = buildImsDataProfile();
         List<LinkAddress> mInternalAddressList;
         List<InetAddress> mDNSAddressList;
         List<InetAddress> mGatewayAddressList;
@@ -267,14 +591,16 @@
         IwlanDataServiceCallback callback = new IwlanDataServiceCallback("requestDataCallList");
         TunnelLinkProperties mLinkProperties =
                 TunnelLinkPropertiesTest.createTestTunnelLinkProperties();
-        mIwlanDataServiceProvider.setTunnelState(
+        mSpyIwlanDataServiceProvider.setTunnelState(
                 dp,
                 new DataServiceCallback(callback),
                 TunnelState.TUNNEL_UP,
                 mLinkProperties,
-                false,
-                1);
-        mIwlanDataServiceProvider.requestDataCallList(new DataServiceCallback(callback));
+                false, /* isHandover */
+                1, /* pduSessionId */
+                true /* isImsOrEmergency */);
+        mSpyIwlanDataServiceProvider.requestDataCallList(new DataServiceCallback(callback));
+        mTestLooper.dispatchAll();
         latch.await(1, TimeUnit.SECONDS);
 
         assertEquals(mResultCode, DataServiceCallback.RESULT_SUCCESS);
@@ -318,7 +644,8 @@
     public void testRequestDataCallListEmpty() throws Exception {
         latch = new CountDownLatch(1);
         IwlanDataServiceCallback callback = new IwlanDataServiceCallback("requestDataCallList");
-        mIwlanDataServiceProvider.requestDataCallList(new DataServiceCallback(callback));
+        mSpyIwlanDataServiceProvider.requestDataCallList(new DataServiceCallback(callback));
+        mTestLooper.dispatchAll();
         latch.await(1, TimeUnit.SECONDS);
 
         assertEquals(mResultCode, DataServiceCallback.RESULT_SUCCESS);
@@ -327,7 +654,7 @@
 
     @Test
     public void testIwlanSetupDataCallWithInvalidArg() {
-        mIwlanDataServiceProvider.setupDataCall(
+        mSpyIwlanDataServiceProvider.setupDataCall(
                 AccessNetworkType.UNKNOWN, /* AccessNetworkType */
                 null, /* dataProfile */
                 false, /* isRoaming */
@@ -339,6 +666,7 @@
                 null, /* trafficDescriptor */
                 true, /* matchAllRuleAllowed */
                 mMockDataServiceCallback);
+        mTestLooper.dispatchAll();
 
         verify(mMockDataServiceCallback, timeout(1000).times(1))
                 .onSetupDataCallComplete(
@@ -347,13 +675,12 @@
 
     @Test
     public void testIwlanSetupDataCallWithIllegalState() {
-        DataProfile dp = buildDataProfile();
+        DataProfile dp = buildImsDataProfile();
 
         /* Wifi is not connected */
-        mIwlanDataService.setNetworkConnected(
-                false, mMockNetwork, IwlanDataService.Transport.UNSPECIFIED_NETWORK);
+        onSystemDefaultNetworkLost();
 
-        mIwlanDataServiceProvider.setupDataCall(
+        mSpyIwlanDataServiceProvider.setupDataCall(
                 AccessNetworkType.IWLAN, /* AccessNetworkType */
                 dp, /* dataProfile */
                 false, /* isRoaming */
@@ -365,18 +692,21 @@
                 null, /* trafficDescriptor */
                 true, /* matchAllRuleAllowed */
                 mMockDataServiceCallback);
+        mTestLooper.dispatchAll();
 
         verify(mMockDataServiceCallback, timeout(1000).times(1))
                 .onSetupDataCallComplete(
-                        eq(5 /*DataServiceCallback.RESULT_ERROR_TEMPORARILY_UNAVAILABLE */), isNull());
+                        eq(5 /*DataServiceCallback.RESULT_ERROR_TEMPORARILY_UNAVAILABLE */),
+                        isNull());
     }
 
     @Test
     public void testIwlanDeactivateDataCallWithInvalidArg() {
-        mIwlanDataServiceProvider.deactivateDataCall(
+        mSpyIwlanDataServiceProvider.deactivateDataCall(
                 0, /* cid */
                 DataService.REQUEST_REASON_NORMAL, /* DataService.REQUEST_REASON_NORMAL */
                 mMockDataServiceCallback);
+        mTestLooper.dispatchAll();
 
         verify(mMockDataServiceCallback, timeout(1000).times(1))
                 .onDeactivateDataCallComplete(eq(DataServiceCallback.RESULT_ERROR_INVALID_ARG));
@@ -384,12 +714,11 @@
 
     @Test
     public void testIwlanSetupDataCallWithBringUpTunnel() {
-        DataProfile dp = buildDataProfile();
+        DataProfile dp = buildImsDataProfile();
 
         /* Wifi is connected */
-        mIwlanDataService.setNetworkConnected(true, mMockNetwork, IwlanDataService.Transport.WIFI);
-
-        doReturn(mMockEpdgTunnelManager).when(mSpyIwlanDataServiceProvider).getTunnelManager();
+        onSystemDefaultNetworkConnected(
+                mMockNetwork, mLinkProperties, TRANSPORT_WIFI, INVALID_SUB_INDEX);
 
         mSpyIwlanDataServiceProvider.setupDataCall(
                 AccessNetworkType.IWLAN, /* AccessNetworkType */
@@ -403,15 +732,20 @@
                 null, /* trafficDescriptor */
                 true, /* matchAllRuleAllowed */
                 mMockDataServiceCallback);
+        mTestLooper.dispatchAll();
 
         /* Check bringUpTunnel() is called. */
         verify(mMockEpdgTunnelManager, times(1))
-                .bringUpTunnel(any(TunnelSetupRequest.class), any(IwlanTunnelCallback.class));
+                .bringUpTunnel(
+                        any(TunnelSetupRequest.class),
+                        any(IwlanTunnelCallback.class),
+                        any(IwlanTunnelMetricsImpl.class));
 
         /* Check callback result is RESULT_SUCCESS when onOpened() is called. */
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onOpened(TEST_APN_NAME, mMockTunnelLinkProperties);
+        mTestLooper.dispatchAll();
         verify(mMockDataServiceCallback, times(1))
                 .onSetupDataCallComplete(
                         eq(DataServiceCallback.RESULT_SUCCESS), any(DataCallResponse.class));
@@ -419,12 +753,11 @@
 
     @Test
     public void testSliceInfoInclusionInDataCallResponse() throws Exception {
-        DataProfile dp = buildDataProfile();
+        DataProfile dp = buildImsDataProfile();
 
         /* Wifi is connected */
-        mIwlanDataService.setNetworkConnected(true, mMockNetwork, IwlanDataService.Transport.WIFI);
-
-        doReturn(mMockEpdgTunnelManager).when(mSpyIwlanDataServiceProvider).getTunnelManager();
+        onSystemDefaultNetworkConnected(
+                mMockNetwork, mLinkProperties, TRANSPORT_WIFI, INVALID_SUB_INDEX);
 
         mSpyIwlanDataServiceProvider.setupDataCall(
                 AccessNetworkType.IWLAN, /* AccessNetworkType */
@@ -438,10 +771,14 @@
                 null, /* trafficDescriptor */
                 true, /* matchAllRuleAllowed */
                 mMockDataServiceCallback);
+        mTestLooper.dispatchAll();
 
         /* Check bringUpTunnel() is called. */
         verify(mMockEpdgTunnelManager, times(1))
-                .bringUpTunnel(any(TunnelSetupRequest.class), any(IwlanTunnelCallback.class));
+                .bringUpTunnel(
+                        any(TunnelSetupRequest.class),
+                        any(IwlanTunnelCallback.class),
+                        any(IwlanTunnelMetricsImpl.class));
 
         /* Check callback result is RESULT_SUCCESS when onOpened() is called. */
         TunnelLinkProperties tp = TunnelLinkPropertiesTest.createTestTunnelLinkProperties();
@@ -450,6 +787,7 @@
                 ArgumentCaptor.forClass(DataCallResponse.class);
 
         mSpyIwlanDataServiceProvider.getIwlanTunnelCallback().onOpened(TEST_APN_NAME, tp);
+        mTestLooper.dispatchAll();
         verify(mMockDataServiceCallback, times(1))
                 .onSetupDataCallComplete(
                         eq(DataServiceCallback.RESULT_SUCCESS), dataCallResponseCaptor.capture());
@@ -462,32 +800,81 @@
 
     @Test
     public void testIwlanDeactivateDataCallWithCloseTunnel() {
-        DataProfile dp = buildDataProfile();
+        DataProfile dp = buildImsDataProfile();
 
-        doReturn(mMockEpdgTunnelManager).when(mSpyIwlanDataServiceProvider).getTunnelManager();
+        onSystemDefaultNetworkConnected(TRANSPORT_WIFI);
 
         mSpyIwlanDataServiceProvider.setTunnelState(
-                dp, mMockDataServiceCallback, TunnelState.TUNNEL_IN_BRINGUP, null, false, 1);
+                dp,
+                mMockDataServiceCallback,
+                TunnelState.TUNNEL_IN_BRINGUP,
+                null, /* linkProperties */
+                false, /* isHandover */
+                1, /* pduSessionId */
+                true /* isImsOrEmergency */);
 
         mSpyIwlanDataServiceProvider.deactivateDataCall(
                 TEST_APN_NAME.hashCode() /* cid: hashcode() of "ims" */,
-                DataService.REQUEST_REASON_NORMAL /* DataService.REQUEST_REASON_NORMAL */,
+                DataService.REQUEST_REASON_NORMAL,
                 mMockDataServiceCallback);
-
+        mTestLooper.dispatchAll();
         /* Check closeTunnel() is called. */
-        verify(mMockEpdgTunnelManager, times(1)).closeTunnel(eq(TEST_APN_NAME), anyBoolean());
+        verify(mMockEpdgTunnelManager, times(1))
+                .closeTunnel(
+                        eq(TEST_APN_NAME),
+                        eq(false),
+                        any(IwlanTunnelCallback.class),
+                        any(IwlanTunnelMetricsImpl.class));
 
         /* Check callback result is RESULT_SUCCESS when onClosed() is called. */
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.NO_ERROR));
+        mTestLooper.dispatchAll();
         verify(mMockDataServiceCallback, times(1))
                 .onDeactivateDataCallComplete(eq(DataServiceCallback.RESULT_SUCCESS));
     }
 
     @Test
-    public void testHandoverFailureModeNormal() {
-        DataProfile dp = buildDataProfile();
+    public void testIwlanDeactivateDataCallAfterSuccessHandover() {
+        DataProfile dp = buildImsDataProfile();
+
+        onSystemDefaultNetworkConnected(TRANSPORT_WIFI);
+
+        mSpyIwlanDataServiceProvider.setTunnelState(
+                dp,
+                mMockDataServiceCallback,
+                TunnelState.TUNNEL_IN_BRINGUP,
+                null, /* linkProperties */
+                false, /* isHandover */
+                1, /* pduSessionId */
+                true /* isImsOrEmergency */);
+
+        mSpyIwlanDataServiceProvider.deactivateDataCall(
+                TEST_APN_NAME.hashCode() /* cid: hashcode() of "ims" */,
+                DataService.REQUEST_REASON_HANDOVER,
+                mMockDataServiceCallback);
+        mTestLooper.dispatchAll();
+        /* Check closeTunnel() is called. */
+        verify(mMockEpdgTunnelManager, times(1))
+                .closeTunnel(
+                        eq(TEST_APN_NAME),
+                        eq(true),
+                        any(IwlanTunnelCallback.class),
+                        any(IwlanTunnelMetricsImpl.class));
+
+        /* Check callback result is RESULT_SUCCESS when onClosed() is called. */
+        mSpyIwlanDataServiceProvider
+                .getIwlanTunnelCallback()
+                .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.NO_ERROR));
+        mTestLooper.dispatchAll();
+        verify(mMockDataServiceCallback, times(1))
+                .onDeactivateDataCallComplete(eq(DataServiceCallback.RESULT_SUCCESS));
+    }
+
+    @Test
+    public void testHandoverFailureModeDefault() {
+        DataProfile dp = buildImsDataProfile();
         int setupDataReason = DataService.REQUEST_REASON_NORMAL;
 
         when(ErrorPolicyManager.getInstance(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
@@ -500,13 +887,25 @@
                 dp,
                 mMockDataServiceCallback,
                 TunnelState.TUNNEL_IN_BRINGUP,
-                null,
+                null, /* linkProperties */
                 (setupDataReason == DataService.REQUEST_REASON_HANDOVER),
-                1);
+                1 /* pduSessionId */,
+                true /* isImsOrEmergency */);
+
+        mSpyIwlanDataServiceProvider.setMetricsAtom(
+                TEST_APN_NAME,
+                64, // type IMS
+                true,
+                13, // LTE
+                false,
+                true,
+                1 // Transport Wi-Fi
+                );
 
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.NO_ERROR));
+        mTestLooper.dispatchAll();
 
         ArgumentCaptor<DataCallResponse> dataCallResponseCaptor =
                 ArgumentCaptor.forClass(DataCallResponse.class);
@@ -518,14 +917,14 @@
         DataCallResponse dataCallResponse = dataCallResponseCaptor.getValue();
         assertEquals(
                 dataCallResponse.getHandoverFailureMode(),
-                DataCallResponse.HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_SETUP_NORMAL);
+                DataCallResponse.HANDOVER_FAILURE_MODE_LEGACY);
         assertEquals(dataCallResponse.getCause(), DataFailCause.USER_AUTHENTICATION);
         assertEquals(dataCallResponse.getRetryDurationMillis(), 5L);
     }
 
     @Test
     public void testHandoverFailureModeHandover() {
-        DataProfile dp = buildDataProfile();
+        DataProfile dp = buildImsDataProfile();
         int setupDataReason = DataService.REQUEST_REASON_HANDOVER;
 
         when(ErrorPolicyManager.getInstance(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
@@ -533,18 +932,32 @@
         when(mMockErrorPolicyManager.getCurrentRetryTimeMs(eq(TEST_APN_NAME))).thenReturn(-1L);
         when(mMockErrorPolicyManager.getDataFailCause(eq(TEST_APN_NAME)))
                 .thenReturn(DataFailCause.ERROR_UNSPECIFIED);
+        when(mMockErrorPolicyManager.shouldRetryWithInitialAttach(eq(TEST_APN_NAME)))
+                .thenReturn(false);
 
         mSpyIwlanDataServiceProvider.setTunnelState(
                 dp,
                 mMockDataServiceCallback,
                 TunnelState.TUNNEL_IN_BRINGUP,
-                null,
+                null, /* linkProperties */
                 (setupDataReason == DataService.REQUEST_REASON_HANDOVER),
-                1);
+                1 /* pduSessionId */,
+                true /* isImsOrEmergency */);
+
+        mSpyIwlanDataServiceProvider.setMetricsAtom(
+                TEST_APN_NAME,
+                64, // type IMS
+                true,
+                13, // LTE
+                false,
+                true,
+                1 // Transport Wi-Fi
+                );
 
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.NO_ERROR));
+        mTestLooper.dispatchAll();
 
         ArgumentCaptor<DataCallResponse> dataCallResponseCaptor =
                 ArgumentCaptor.forClass(DataCallResponse.class);
@@ -562,34 +975,268 @@
     }
 
     @Test
+    public void testSupportInitialAttachSuccessOnIms() {
+        DataProfile dp = buildImsDataProfile();
+        int setupDataReason = DataService.REQUEST_REASON_HANDOVER;
+
+        when(ErrorPolicyManager.getInstance(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
+                .thenReturn(mMockErrorPolicyManager);
+        when(mMockErrorPolicyManager.getCurrentRetryTimeMs(eq(TEST_APN_NAME))).thenReturn(-1L);
+        when(mMockErrorPolicyManager.getDataFailCause(eq(TEST_APN_NAME)))
+                .thenReturn(DataFailCause.ERROR_UNSPECIFIED);
+        when(mMockErrorPolicyManager.shouldRetryWithInitialAttach(eq(TEST_APN_NAME)))
+                .thenReturn(true);
+
+        // APN = IMS, in idle call state
+        mIwlanDataService
+                .mIwlanDataServiceHandler
+                .obtainMessage(
+                        IwlanEventListener.CALL_STATE_CHANGED_EVENT,
+                        DEFAULT_SLOT_INDEX,
+                        TelephonyManager.CALL_STATE_IDLE)
+                .sendToTarget();
+
+        mSpyIwlanDataServiceProvider.setTunnelState(
+                dp,
+                mMockDataServiceCallback,
+                TunnelState.TUNNEL_IN_BRINGUP,
+                null, /* linkProperties */
+                (setupDataReason == DataService.REQUEST_REASON_HANDOVER),
+                1 /* pduSessionId */,
+                true /* isImsOrEmergency */);
+
+        mSpyIwlanDataServiceProvider.setMetricsAtom(
+                TEST_APN_NAME,
+                64, // type IMS
+                true,
+                13, // LTE
+                false,
+                true,
+                1 // Transport Wi-Fi
+                );
+
+        mSpyIwlanDataServiceProvider
+                .getIwlanTunnelCallback()
+                .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.NO_ERROR));
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<DataCallResponse> dataCallResponseCaptor =
+                ArgumentCaptor.forClass(DataCallResponse.class);
+        verify(mMockDataServiceCallback, times(1))
+                .onSetupDataCallComplete(
+                        eq(DataServiceCallback.RESULT_SUCCESS), dataCallResponseCaptor.capture());
+        DataCallResponse dataCallResponse = dataCallResponseCaptor.getValue();
+        // Not on video or voice call
+        assertEquals(
+                DataCallResponse.HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_SETUP_NORMAL,
+                dataCallResponse.getHandoverFailureMode());
+    }
+
+    @Test
+    public void testSupportInitialAttachSuccessOnEmergency() {
+        DataProfile dp = buildDataProfile(ApnSetting.TYPE_EMERGENCY);
+        int setupDataReason = DataService.REQUEST_REASON_HANDOVER;
+
+        when(ErrorPolicyManager.getInstance(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
+                .thenReturn(mMockErrorPolicyManager);
+        when(mMockErrorPolicyManager.getCurrentRetryTimeMs(eq(TEST_APN_NAME))).thenReturn(-1L);
+        when(mMockErrorPolicyManager.getDataFailCause(eq(TEST_APN_NAME)))
+                .thenReturn(DataFailCause.ERROR_UNSPECIFIED);
+        when(mMockErrorPolicyManager.shouldRetryWithInitialAttach(eq(TEST_APN_NAME)))
+                .thenReturn(true);
+
+        // APN = Emergency, in idle call state
+        mIwlanDataService
+                .mIwlanDataServiceHandler
+                .obtainMessage(
+                        IwlanEventListener.CALL_STATE_CHANGED_EVENT,
+                        DEFAULT_SLOT_INDEX,
+                        TelephonyManager.CALL_STATE_IDLE)
+                .sendToTarget();
+
+        mSpyIwlanDataServiceProvider.setTunnelState(
+                dp,
+                mMockDataServiceCallback,
+                TunnelState.TUNNEL_IN_BRINGUP,
+                null, /* linkProperties */
+                (setupDataReason == DataService.REQUEST_REASON_HANDOVER),
+                1 /* pduSessionId */,
+                true /* isImsOrEmergency */);
+
+        mSpyIwlanDataServiceProvider.setMetricsAtom(
+                TEST_APN_NAME,
+                512, // type Emergency
+                true,
+                13, // LTE
+                false,
+                true,
+                1 // Transport Wi-Fi
+                );
+
+        mSpyIwlanDataServiceProvider
+                .getIwlanTunnelCallback()
+                .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.NO_ERROR));
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<DataCallResponse> dataCallResponseCaptor =
+                ArgumentCaptor.forClass(DataCallResponse.class);
+        verify(mMockDataServiceCallback, times(1))
+                .onSetupDataCallComplete(
+                        eq(DataServiceCallback.RESULT_SUCCESS), dataCallResponseCaptor.capture());
+        DataCallResponse dataCallResponse = dataCallResponseCaptor.getValue();
+        // Not on video or voice call
+        assertEquals(
+                DataCallResponse.HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_SETUP_NORMAL,
+                dataCallResponse.getHandoverFailureMode());
+    }
+
+    @Test
+    public void testSupportInitialAttachOnImsCall() {
+        DataProfile dp = buildImsDataProfile();
+        int setupDataReason = DataService.REQUEST_REASON_HANDOVER;
+
+        when(ErrorPolicyManager.getInstance(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
+                .thenReturn(mMockErrorPolicyManager);
+        when(mMockErrorPolicyManager.getCurrentRetryTimeMs(eq(TEST_APN_NAME))).thenReturn(-1L);
+        when(mMockErrorPolicyManager.getDataFailCause(eq(TEST_APN_NAME)))
+                .thenReturn(DataFailCause.ERROR_UNSPECIFIED);
+        when(mMockErrorPolicyManager.shouldRetryWithInitialAttach(eq(TEST_APN_NAME)))
+                .thenReturn(true);
+
+        // APN = IMS, in call
+        mIwlanDataService
+                .mIwlanDataServiceHandler
+                .obtainMessage(
+                        IwlanEventListener.CALL_STATE_CHANGED_EVENT,
+                        DEFAULT_SLOT_INDEX,
+                        TelephonyManager.CALL_STATE_OFFHOOK)
+                .sendToTarget();
+
+        mSpyIwlanDataServiceProvider.setTunnelState(
+                dp,
+                mMockDataServiceCallback,
+                TunnelState.TUNNEL_IN_BRINGUP,
+                null /* linkProperties */,
+                (setupDataReason == DataService.REQUEST_REASON_HANDOVER),
+                1 /* pduSessionId */,
+                true /* isImsOrEmergency */);
+
+        mSpyIwlanDataServiceProvider.setMetricsAtom(
+                TEST_APN_NAME,
+                64, // type IMS
+                true,
+                13, // LTE
+                false,
+                true,
+                1 // Transport Wi-Fi
+                );
+
+        mSpyIwlanDataServiceProvider
+                .getIwlanTunnelCallback()
+                .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.NO_ERROR));
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<DataCallResponse> dataCallResponseCaptor =
+                ArgumentCaptor.forClass(DataCallResponse.class);
+        verify(mMockDataServiceCallback, times(1))
+                .onSetupDataCallComplete(
+                        eq(DataServiceCallback.RESULT_SUCCESS), dataCallResponseCaptor.capture());
+        DataCallResponse dataCallResponse = dataCallResponseCaptor.getValue();
+        // In call state
+        assertEquals(
+                DataCallResponse.HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_HANDOVER,
+                dataCallResponse.getHandoverFailureMode());
+    }
+
+    @Test
+    public void testSupportInitialAttachOnEmergencyCall() {
+        DataProfile dp = buildDataProfile(ApnSetting.TYPE_EMERGENCY);
+        int setupDataReason = DataService.REQUEST_REASON_HANDOVER;
+
+        when(ErrorPolicyManager.getInstance(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
+                .thenReturn(mMockErrorPolicyManager);
+        when(mMockErrorPolicyManager.getCurrentRetryTimeMs(eq(TEST_APN_NAME))).thenReturn(-1L);
+        when(mMockErrorPolicyManager.getDataFailCause(eq(TEST_APN_NAME)))
+                .thenReturn(DataFailCause.ERROR_UNSPECIFIED);
+        when(mMockErrorPolicyManager.shouldRetryWithInitialAttach(eq(TEST_APN_NAME)))
+                .thenReturn(true);
+
+        // APN = Emergency, in call
+        mIwlanDataService
+                .mIwlanDataServiceHandler
+                .obtainMessage(
+                        IwlanEventListener.CALL_STATE_CHANGED_EVENT,
+                        DEFAULT_SLOT_INDEX,
+                        TelephonyManager.CALL_STATE_OFFHOOK)
+                .sendToTarget();
+
+        mSpyIwlanDataServiceProvider.setTunnelState(
+                dp,
+                mMockDataServiceCallback,
+                TunnelState.TUNNEL_IN_BRINGUP,
+                null /* linkProperties */,
+                (setupDataReason == DataService.REQUEST_REASON_HANDOVER),
+                1 /* pduSessionId */,
+                true /* isImsOrEmergency */);
+
+        mSpyIwlanDataServiceProvider.setMetricsAtom(
+                TEST_APN_NAME,
+                512, // type Emergency
+                true,
+                13, // LTE
+                false,
+                true,
+                1 // Transport Wi-Fi
+                );
+
+        mSpyIwlanDataServiceProvider
+                .getIwlanTunnelCallback()
+                .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.NO_ERROR));
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<DataCallResponse> dataCallResponseCaptor =
+                ArgumentCaptor.forClass(DataCallResponse.class);
+        verify(mMockDataServiceCallback, times(1))
+                .onSetupDataCallComplete(
+                        eq(DataServiceCallback.RESULT_SUCCESS), dataCallResponseCaptor.capture());
+        DataCallResponse dataCallResponse = dataCallResponseCaptor.getValue();
+        // In call state
+        assertEquals(
+                DataCallResponse.HANDOVER_FAILURE_MODE_NO_FALLBACK_RETRY_HANDOVER,
+                dataCallResponse.getHandoverFailureMode());
+    }
+
+    @Test
     public void testDnsPrefetching() throws Exception {
-        IwlanNetworkMonitorCallback mNetworkMonitorCallback =
-                mIwlanDataService.getNetworkMonitorCallback();
+        NetworkCallback networkCallback = getNetworkMonitorCallback();
         /* Wifi is connected */
-        mIwlanDataService.setNetworkConnected(true, mMockNetwork, IwlanDataService.Transport.WIFI);
+        onSystemDefaultNetworkConnected(
+                mMockNetwork, mLinkProperties, TRANSPORT_WIFI, INVALID_SUB_INDEX);
+        networkCallback.onLinkPropertiesChanged(mMockNetwork, mLinkProperties);
 
-        List<LinkAddress> linkAddresses = new ArrayList<>();
-        linkAddresses.add(mMockIPv4LinkAddress);
-
-        when(mMockLinkProperties.getLinkAddresses()).thenReturn(linkAddresses);
-        mNetworkMonitorCallback.onLinkPropertiesChanged(mMockNetwork, mMockLinkProperties);
-
-        mIwlanDataServiceProvider
-                .mHandler
-                .obtainMessage(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT)
+        mIwlanDataService
+                .mIwlanDataServiceHandler
+                .obtainMessage(
+                        IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT,
+                        DEFAULT_SLOT_INDEX,
+                        0 /* unused */)
                 .sendToTarget();
-        sleep(1000);
 
-        mIwlanDataServiceProvider
-                .mHandler
-                .obtainMessage(IwlanEventListener.WIFI_CALLING_ENABLE_EVENT)
+        mIwlanDataService
+                .mIwlanDataServiceHandler
+                .obtainMessage(
+                        IwlanEventListener.WIFI_CALLING_ENABLE_EVENT,
+                        DEFAULT_SLOT_INDEX,
+                        0 /* unused */)
                 .sendToTarget();
-        sleep(1000);
+        mTestLooper.dispatchAll();
 
-        linkAddresses.add(mMockIPv6LinkAddress);
+        LinkProperties newLinkProperties = new LinkProperties();
+        newLinkProperties.setInterfaceName("wlan0");
+        newLinkProperties.addLinkAddress(mMockIPv4LinkAddress);
+        newLinkProperties.addLinkAddress(mMockIPv6LinkAddress);
 
-        when(mMockLinkProperties.getLinkAddresses()).thenReturn(linkAddresses);
-        mNetworkMonitorCallback.onLinkPropertiesChanged(mMockNetwork, mMockLinkProperties);
+        networkCallback.onLinkPropertiesChanged(mMockNetwork, newLinkProperties);
 
         /* Prefetching will be triggered twice.
            1. Network connected, CarrierConfig ready, WifiCallingSetting enabled
@@ -599,6 +1246,7 @@
                 .getValidatedServerList(
                         eq(0),
                         eq(EpdgSelector.PROTO_FILTER_IPV4V6),
+                        eq(EpdgSelector.SYSTEM_PREFERRED),
                         eq(false),
                         eq(false),
                         eq(mMockNetwork),
@@ -607,21 +1255,26 @@
                 .getValidatedServerList(
                         eq(0),
                         eq(EpdgSelector.PROTO_FILTER_IPV4V6),
+                        eq(EpdgSelector.SYSTEM_PREFERRED),
                         eq(false),
                         eq(true),
                         eq(mMockNetwork),
                         isNull());
     }
 
-    private void sleep(long time) {
-        try {
-            Thread.sleep(time);
-        } catch (Exception e) {
-            e.printStackTrace();
+    private void advanceCalendarByTimeMs(long time, Calendar calendar) {
+        mMockedCalendarTime += time;
+        if (calendar != null) {
+            calendar.setTimeInMillis(mMockedCalendarTime);
         }
+        mTestLooper.dispatchAll();
     }
 
-    private DataProfile buildDataProfile() {
+    private DataProfile buildImsDataProfile() {
+        return buildDataProfile(ApnSetting.TYPE_IMS);
+    }
+
+    private DataProfile buildDataProfile(int supportedApnTypesBitmask) {
         DataProfile dp =
                 new DataProfile.Builder()
                         .setProfileId(1)
@@ -635,7 +1288,7 @@
                         // .setMaxConnections(3)
                         // .setWaitTime(10)
                         .enable(true)
-                        .setSupportedApnTypesBitmask(ApnSetting.TYPE_IMS)
+                        .setSupportedApnTypesBitmask(supportedApnTypesBitmask)
                         .setRoamingProtocolType(ApnSetting.PROTOCOL_IPV4V6) // IPv4v6
                         .setBearerBitmask((int) TelephonyManager.NETWORK_TYPE_BITMASK_IWLAN)
                         .setPersistent(true)
@@ -644,24 +1297,30 @@
         return dp;
     }
 
+    private NetworkCapabilities prepareNetworkCapabilitiesForTest(
+            int transportType, int subId, boolean isVcn) {
+        NetworkCapabilities.Builder builder =
+                new NetworkCapabilities.Builder().addTransportType(transportType);
+        if (isVcn) {
+            builder.setTransportInfo(new VcnTransportInfo(subId));
+        } else {
+            builder.setNetworkSpecifier(new TelephonyNetworkSpecifier(subId));
+        }
+        return builder.build();
+    }
+
     @Test
-    public void testIwlanSetupDataCallWithCellularAndCstDisabled() {
-        DataProfile dp = buildDataProfile();
+    public void testIwlanSetupDataCallFailsWithCellularAndCstDisabled() throws Exception {
+        DataProfile dp = buildImsDataProfile();
+        /* CST is disabled, and data is on the same sub as the data service provider */
+        when(mMockImsMmTelManager.isCrossSimCallingEnabled()).thenReturn(false);
 
-        /* Mobile is connected */
-        mIwlanDataService.setNetworkConnected(
-                true, mMockNetwork, IwlanDataService.Transport.MOBILE);
+        NetworkCapabilities nc =
+                prepareNetworkCapabilitiesForTest(
+                        TRANSPORT_CELLULAR, DEFAULT_SUB_INDEX, false /* isVcn */);
+        getNetworkMonitorCallback().onCapabilitiesChanged(mMockNetwork, nc);
 
-        lenient()
-                .when(
-                        IwlanHelper.isCrossSimCallingEnabled(
-                                eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
-                .thenReturn(false);
-        lenient()
-                .when(IwlanHelper.isDefaultDataSlot(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
-                .thenReturn(true);
-
-        mIwlanDataServiceProvider.setupDataCall(
+        mSpyIwlanDataServiceProvider.setupDataCall(
                 AccessNetworkType.IWLAN, /* AccessNetworkType */
                 dp, /* dataProfile */
                 false, /* isRoaming */
@@ -673,34 +1332,25 @@
                 null, /* trafficDescriptor */
                 true, /* matchAllRuleAllowed */
                 mMockDataServiceCallback);
+        mTestLooper.dispatchAll();
 
         verify(mMockDataServiceCallback, timeout(1000).times(1))
                 .onSetupDataCallComplete(
-                        eq(5 /* DataServiceCallback.RESULT_ERROR_TEMPORARILY_UNAVAILABLE */), isNull());
+                        eq(5 /* DataServiceCallback.RESULT_ERROR_TEMPORARILY_UNAVAILABLE */),
+                        isNull());
     }
 
     @Test
-    public void testIwlanSetupDataCallWithCellularAndCstEnabled() {
-        DataProfile dp = buildDataProfile();
+    public void testIwlanSetupDataCallFailsWithCellularOnSameSubAndCstEnabled() throws Exception {
+        DataProfile dp = buildImsDataProfile();
 
-        /* Clear state */
-        mIwlanDataService.setNetworkConnected(
-                false, mMockNetwork, IwlanDataService.Transport.UNSPECIFIED_NETWORK);
+        /* CST is enabled, but data is on the same sub as the DataServiceProvider */
+        when(mMockImsMmTelManager.isCrossSimCallingEnabled()).thenReturn(true);
 
-        /* Mobile is connected */
-        mIwlanDataService.setNetworkConnected(
-                true, mMockNetwork, IwlanDataService.Transport.MOBILE);
-
-        doReturn(mMockEpdgTunnelManager).when(mSpyIwlanDataServiceProvider).getTunnelManager();
-
-        lenient()
-                .when(
-                        IwlanHelper.isCrossSimCallingEnabled(
-                                eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
-                .thenReturn(true);
-        lenient()
-                .when(IwlanHelper.isDefaultDataSlot(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
-                .thenReturn(false);
+        NetworkCapabilities nc =
+                prepareNetworkCapabilitiesForTest(
+                        TRANSPORT_CELLULAR, DEFAULT_SUB_INDEX, false /* isVcn */);
+        getNetworkMonitorCallback().onCapabilitiesChanged(mMockNetwork, nc);
 
         mSpyIwlanDataServiceProvider.setupDataCall(
                 AccessNetworkType.IWLAN, /* AccessNetworkType */
@@ -714,15 +1364,53 @@
                 null, /* trafficDescriptor */
                 true, /* matchAllRuleAllowed */
                 mMockDataServiceCallback);
+        mTestLooper.dispatchAll();
+
+        verify(mMockDataServiceCallback, timeout(1000).times(1))
+                .onSetupDataCallComplete(
+                        eq(5 /* DataServiceCallback.RESULT_ERROR_TEMPORARILY_UNAVAILABLE */),
+                        isNull());
+    }
+
+    @Test
+    public void testIwlanSetupDataCallSucceedsWithCellularOnDifferentSubAndCstEnabled()
+            throws Exception {
+        DataProfile dp = buildImsDataProfile();
+
+        /* CST is enabled, but data is on the same sub as the DataServiceProvider */
+        when(mMockImsMmTelManager.isCrossSimCallingEnabled()).thenReturn(true);
+
+        NetworkCapabilities nc =
+                prepareNetworkCapabilitiesForTest(
+                        TRANSPORT_CELLULAR, DEFAULT_SUB_INDEX + 1, false /* isVcn */);
+        getNetworkMonitorCallback().onCapabilitiesChanged(mMockNetwork, nc);
+
+        mSpyIwlanDataServiceProvider.setupDataCall(
+                AccessNetworkType.IWLAN, /* AccessNetworkType */
+                dp, /* dataProfile */
+                false, /* isRoaming */
+                true, /* allowRoaming */
+                DataService.REQUEST_REASON_NORMAL, /* DataService.REQUEST_REASON_NORMAL */
+                null, /* LinkProperties */
+                1, /* pduSessionId */
+                null, /* sliceInfo */
+                null, /* trafficDescriptor */
+                true, /* matchAllRuleAllowed */
+                mMockDataServiceCallback);
+        mTestLooper.dispatchAll();
 
         /* Check bringUpTunnel() is called. */
         verify(mMockEpdgTunnelManager, times(1))
-                .bringUpTunnel(any(TunnelSetupRequest.class), any(IwlanTunnelCallback.class));
+                .bringUpTunnel(
+                        any(TunnelSetupRequest.class),
+                        any(IwlanTunnelCallback.class),
+                        any(IwlanTunnelMetricsImpl.class));
 
         /* Check callback result is RESULT_SUCCESS when onOpened() is called. */
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onOpened(TEST_APN_NAME, mMockTunnelLinkProperties);
+        mTestLooper.dispatchAll();
         verify(mMockDataServiceCallback, times(1))
                 .onSetupDataCallComplete(
                         eq(DataServiceCallback.RESULT_SUCCESS), any(DataCallResponse.class));
@@ -730,17 +1418,18 @@
 
     @Test
     public void testIwlanTunnelStatsFailureCounts() {
-        DataProfile dp = buildDataProfile();
+        DataProfile dp = buildImsDataProfile();
 
-        mIwlanDataService.setNetworkConnected(true, mMockNetwork, IwlanDataService.Transport.WIFI);
-        doReturn(mMockEpdgTunnelManager).when(mSpyIwlanDataServiceProvider).getTunnelManager();
+        onSystemDefaultNetworkConnected(
+                mMockNetwork, mLinkProperties, TRANSPORT_WIFI, INVALID_SUB_INDEX);
+
         when(ErrorPolicyManager.getInstance(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
                 .thenReturn(mMockErrorPolicyManager);
 
         long count = 3L;
         for (int i = 0; i < count; i++) {
             mockTunnelSetupFail(dp);
-            sleep(1000);
+            mTestLooper.dispatchAll();
         }
 
         IwlanDataServiceProvider.IwlanDataTunnelStats stats =
@@ -751,14 +1440,19 @@
 
     @Test
     public void testIwlanTunnelStatsUnsolDownCounts() {
-        DataProfile dp = buildDataProfile();
+        DataProfile dp = buildImsDataProfile();
 
-        mIwlanDataService.setNetworkConnected(true, mMockNetwork, IwlanDataService.Transport.WIFI);
-        doReturn(mMockEpdgTunnelManager).when(mSpyIwlanDataServiceProvider).getTunnelManager();
+        when(ErrorPolicyManager.getInstance(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
+                .thenReturn(mMockErrorPolicyManager);
+        when(mMockErrorPolicyManager.getDataFailCause(eq(TEST_APN_NAME)))
+                .thenReturn(DataFailCause.ERROR_UNSPECIFIED);
+
+        onSystemDefaultNetworkConnected(
+                mMockNetwork, mLinkProperties, TRANSPORT_WIFI, INVALID_SUB_INDEX);
 
         long count = 3L;
         for (int i = 0; i < count; i++) {
-            mockTunnelSetupSuccess(dp, 0);
+            mockTunnelSetupSuccess(dp, 0, null);
             mockUnsolTunnelDown();
         }
 
@@ -770,35 +1464,38 @@
 
     @Test
     public void testIwlanTunnelStats() {
-        DataProfile dp = buildDataProfile();
+        DataProfile dp = buildImsDataProfile();
+        Calendar calendar = mock(Calendar.class);
+        when(calendar.getTime()).thenAnswer(i -> new Date(mMockedCalendarTime));
 
-        mIwlanDataService.setNetworkConnected(true, mMockNetwork, IwlanDataService.Transport.WIFI);
-        doReturn(mMockEpdgTunnelManager).when(mSpyIwlanDataServiceProvider).getTunnelManager();
+        mSpyIwlanDataServiceProvider.setCalendar(calendar);
+        onSystemDefaultNetworkConnected(
+                mMockNetwork, mLinkProperties, TRANSPORT_WIFI, INVALID_SUB_INDEX);
 
         LongSummaryStatistics tunnelSetupSuccessStats = new LongSummaryStatistics();
         LongSummaryStatistics tunnelUpStats = new LongSummaryStatistics();
 
-        Date beforeSetup = Calendar.getInstance().getTime();
-        mockTunnelSetupSuccess(dp, 0);
-        Date tunnelUp = Calendar.getInstance().getTime();
-        mockDeactivateTunnelDown(0);
-        Date tunnelDown = Calendar.getInstance().getTime();
+        Date beforeSetup = calendar.getTime();
+        mockTunnelSetupSuccess(dp, 0, calendar);
+        Date tunnelUp = calendar.getTime();
+        mockDeactivateTunnel(0, calendar);
+        Date tunnelDown = calendar.getTime();
         tunnelSetupSuccessStats.accept(tunnelUp.getTime() - beforeSetup.getTime());
         tunnelUpStats.accept(tunnelDown.getTime() - tunnelUp.getTime());
 
-        beforeSetup = Calendar.getInstance().getTime();
-        mockTunnelSetupSuccess(dp, 1000);
-        tunnelUp = Calendar.getInstance().getTime();
-        mockDeactivateTunnelDown(3000);
-        tunnelDown = Calendar.getInstance().getTime();
+        beforeSetup = calendar.getTime();
+        mockTunnelSetupSuccess(dp, 1000, calendar);
+        tunnelUp = calendar.getTime();
+        mockDeactivateTunnel(3000, calendar);
+        tunnelDown = calendar.getTime();
         tunnelSetupSuccessStats.accept(tunnelUp.getTime() - beforeSetup.getTime());
         tunnelUpStats.accept(tunnelDown.getTime() - tunnelUp.getTime());
 
-        beforeSetup = Calendar.getInstance().getTime();
-        mockTunnelSetupSuccess(dp, 600);
-        tunnelUp = Calendar.getInstance().getTime();
-        mockDeactivateTunnelDown(500);
-        tunnelDown = Calendar.getInstance().getTime();
+        beforeSetup = calendar.getTime();
+        mockTunnelSetupSuccess(dp, 600, calendar);
+        tunnelUp = calendar.getTime();
+        mockDeactivateTunnel(500, calendar);
+        tunnelDown = calendar.getTime();
         tunnelSetupSuccessStats.accept(tunnelUp.getTime() - beforeSetup.getTime());
         tunnelUpStats.accept(tunnelDown.getTime() - tunnelUp.getTime());
 
@@ -807,13 +1504,182 @@
         LongSummaryStatistics finalSetupStats = stats.mTunnelSetupSuccessStats.get(TEST_APN_NAME);
         LongSummaryStatistics finalUpStats = stats.mTunnelUpStats.get(TEST_APN_NAME);
 
-        assertEquals(finalSetupStats.getAverage(), tunnelSetupSuccessStats.getAverage(), 100);
-        assertEquals(finalSetupStats.getCount(), tunnelSetupSuccessStats.getCount());
-        assertEquals(finalSetupStats.getMax(), tunnelSetupSuccessStats.getMax(), 100);
+        assertEquals(tunnelSetupSuccessStats.getAverage(), finalSetupStats.getAverage(), 0);
+        assertEquals(tunnelSetupSuccessStats.getCount(), finalSetupStats.getCount());
+        assertEquals(tunnelSetupSuccessStats.getMax(), finalSetupStats.getMax(), 0);
 
-        assertEquals(finalUpStats.getAverage(), tunnelUpStats.getAverage(), 100);
-        assertEquals(finalUpStats.getCount(), tunnelUpStats.getCount());
-        assertEquals(finalUpStats.getMax(), tunnelUpStats.getMax(), 100);
+        assertEquals(tunnelUpStats.getAverage(), finalUpStats.getAverage(), 0);
+        assertEquals(tunnelUpStats.getCount(), finalUpStats.getCount());
+        assertEquals(tunnelUpStats.getMax(), finalUpStats.getMax(), 0);
+    }
+
+    @Test
+    public void testUnexpectedTunnelClosedIsSuppressed() {
+        mockUnsolTunnelDown();
+    }
+
+    @Test
+    public void testIwlanDataServiceHandlerOnUnbind() {
+        DataProfile dp = buildImsDataProfile();
+
+        mSpyIwlanDataServiceProvider.setTunnelState(
+                dp,
+                mMockDataServiceCallback,
+                TunnelState.TUNNEL_UP,
+                null /* linkProperties */,
+                false /* isHandover */,
+                1 /* pduSessionId */,
+                true /* isImsOrEmergency */);
+
+        mSpyIwlanDataServiceProvider.setMetricsAtom(
+                TEST_APN_NAME,
+                64, // type IMS
+                true,
+                13, // LTE
+                false,
+                true,
+                1 // Transport Wi-Fi
+                );
+
+        when(ErrorPolicyManager.getInstance(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
+                .thenReturn(mMockErrorPolicyManager);
+        when(mMockErrorPolicyManager.getDataFailCause(eq(TEST_APN_NAME)))
+                .thenReturn(DataFailCause.ERROR_UNSPECIFIED);
+
+        // Simulate IwlanDataService.onUnbind() which force close all tunnels
+        mSpyIwlanDataServiceProvider.forceCloseTunnels();
+        // Simulate DataService.onUnbind() which remove all IwlanDataServiceProviders
+        mSpyIwlanDataServiceProvider.close();
+        mTestLooper.dispatchAll();
+
+        verify(mMockEpdgTunnelManager, atLeastOnce())
+                .closeTunnel(
+                        eq(TEST_APN_NAME),
+                        eq(true),
+                        any(IwlanTunnelCallback.class),
+                        any(IwlanTunnelMetricsImpl.class));
+        assertNotNull(mIwlanDataService.mIwlanDataServiceHandler);
+        // Should not raise NullPointerException
+        mSpyIwlanDataServiceProvider
+                .getIwlanTunnelCallback()
+                .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.NO_ERROR));
+        mTestLooper.dispatchAll();
+    }
+
+    @Test
+    public void testBackToBackOnBindAndOnUnbindDoesNotThrow() {
+        mIwlanDataService.onBind(null);
+        mIwlanDataService.onUnbind(null);
+    }
+
+    @Test
+    public void testMetricsWhenTunnelClosedWithWrappedException() {
+        DataProfile dp = buildImsDataProfile();
+
+        mSpyIwlanDataServiceProvider.setTunnelState(
+                dp,
+                mMockDataServiceCallback,
+                TunnelState.TUNNEL_IN_BRINGUP,
+                null, /* linkProperties */
+                false /* isHandover */,
+                1 /* pduSessionId */,
+                true /* isImsOrEmergency */);
+
+        mSpyIwlanDataServiceProvider.setMetricsAtom(
+                TEST_APN_NAME,
+                64, // type IMS
+                true,
+                13, // LTE
+                false,
+                true,
+                1 // Transport Wi-Fi
+                );
+
+        MetricsAtom metricsAtom = mSpyIwlanDataServiceProvider.getMetricsAtomByApn(TEST_APN_NAME);
+        assertNotNull(metricsAtom);
+
+        String exceptionMessage = "Some exception message";
+        Exception mockException = spy(new IllegalStateException(exceptionMessage));
+        String firstDeclaringClassName = "test.test.TestClass";
+        String firstMethodName = "someMethod";
+        String firstFileName = "TestClass.java";
+        int firstLineNumber = 12345;
+        StackTraceElement[] stackTraceElements = {
+            new StackTraceElement(
+                    firstDeclaringClassName, firstMethodName, firstFileName, firstLineNumber),
+            new StackTraceElement("test", "test", "test.java", 123)
+        };
+        doReturn(stackTraceElements).when(mockException).getStackTrace();
+
+        when(ErrorPolicyManager.getInstance(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
+                .thenReturn(mMockErrorPolicyManager);
+        when(mMockErrorPolicyManager.getDataFailCause(eq(TEST_APN_NAME)))
+                .thenReturn(DataFailCause.ERROR_UNSPECIFIED);
+
+        mSpyIwlanDataServiceProvider
+                .getIwlanTunnelCallback()
+                .onClosed(TEST_APN_NAME, new IwlanError(new IkeInternalException(mockException)));
+
+        mTestLooper.dispatchAll();
+
+        var expectedStackFirstFrame =
+                firstDeclaringClassName
+                        + "."
+                        + firstMethodName
+                        + "("
+                        + firstFileName
+                        + ":"
+                        + firstLineNumber
+                        + ")";
+
+        assertEquals(
+                mockException.getClass().getCanonicalName(),
+                metricsAtom.getIwlanErrorWrappedClassname());
+
+        assertEquals(expectedStackFirstFrame, metricsAtom.getIwlanErrorWrappedStackFirstFrame());
+    }
+
+    @Test
+    public void testMetricsWhenTunnelClosedWithoutWrappedException() {
+        DataProfile dp = buildImsDataProfile();
+
+        mSpyIwlanDataServiceProvider.setTunnelState(
+                dp,
+                mMockDataServiceCallback,
+                TunnelState.TUNNEL_IN_BRINGUP,
+                null, /* linkProperties */
+                false /* isHandover */,
+                1 /* pduSessionId */,
+                true /* isImsOrEmergency */);
+
+        mSpyIwlanDataServiceProvider.setMetricsAtom(
+                TEST_APN_NAME,
+                64, // type IMS
+                true,
+                13, // LTE
+                false,
+                true,
+                1 // Transport Wi-Fi
+                );
+
+        MetricsAtom metricsAtom = mSpyIwlanDataServiceProvider.getMetricsAtomByApn(TEST_APN_NAME);
+        assertNotNull(metricsAtom);
+
+        when(ErrorPolicyManager.getInstance(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
+                .thenReturn(mMockErrorPolicyManager);
+        when(mMockErrorPolicyManager.getDataFailCause(eq(TEST_APN_NAME)))
+                .thenReturn(DataFailCause.ERROR_UNSPECIFIED);
+
+        mSpyIwlanDataServiceProvider
+                .getIwlanTunnelCallback()
+                .onClosed(
+                        TEST_APN_NAME,
+                        new IwlanError(IwlanError.EPDG_SELECTOR_SERVER_SELECTION_FAILED));
+
+        mTestLooper.dispatchAll();
+
+        assertEquals(null, metricsAtom.getIwlanErrorWrappedClassname());
+        assertEquals(null, metricsAtom.getIwlanErrorWrappedStackFirstFrame());
     }
 
     private void mockTunnelSetupFail(DataProfile dp) {
@@ -831,17 +1697,21 @@
                 mMockDataServiceCallback);
         doReturn(true)
                 .when(mMockEpdgTunnelManager)
-                .bringUpTunnel(any(TunnelSetupRequest.class), any(IwlanTunnelCallback.class));
+                .bringUpTunnel(
+                        any(TunnelSetupRequest.class),
+                        any(IwlanTunnelCallback.class),
+                        any(IwlanTunnelMetricsImpl.class));
 
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.IKE_INTERNAL_IO_EXCEPTION));
+        mTestLooper.dispatchAll();
         verify(mMockDataServiceCallback, atLeastOnce())
                 .onSetupDataCallComplete(
                         eq(DataServiceCallback.RESULT_SUCCESS), any(DataCallResponse.class));
     }
 
-    private void mockTunnelSetupSuccess(DataProfile dp, long sleepTime) {
+    private void mockTunnelSetupSuccess(DataProfile dp, long setupTime, Calendar calendar) {
         mSpyIwlanDataServiceProvider.setupDataCall(
                 AccessNetworkType.IWLAN, /* AccessNetworkType */
                 dp, /* dataProfile */
@@ -856,13 +1726,18 @@
                 mMockDataServiceCallback);
         doReturn(true)
                 .when(mMockEpdgTunnelManager)
-                .bringUpTunnel(any(TunnelSetupRequest.class), any(IwlanTunnelCallback.class));
+                .bringUpTunnel(
+                        any(TunnelSetupRequest.class),
+                        any(IwlanTunnelCallback.class),
+                        any(IwlanTunnelMetricsImpl.class));
+        mTestLooper.dispatchAll();
 
-        sleep(sleepTime);
+        advanceCalendarByTimeMs(setupTime, calendar);
 
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onOpened(TEST_APN_NAME, mMockTunnelLinkProperties);
+        mTestLooper.dispatchAll();
         verify(mMockDataServiceCallback, atLeastOnce())
                 .onSetupDataCallComplete(
                         eq(DataServiceCallback.RESULT_SUCCESS), any(DataCallResponse.class));
@@ -872,21 +1747,37 @@
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.IKE_INTERNAL_IO_EXCEPTION));
+        mTestLooper.dispatchAll();
     }
 
-    private void mockDeactivateTunnelDown(long sleepTime) {
+    private void mockDeactivateTunnel(long deactivationTime, Calendar calendar) {
         mSpyIwlanDataServiceProvider.deactivateDataCall(
                 TEST_APN_NAME.hashCode() /* cid: hashcode() of "ims" */,
                 DataService.REQUEST_REASON_NORMAL /* DataService.REQUEST_REASON_NORMAL */,
                 mMockDataServiceCallback);
-        verify(mMockEpdgTunnelManager, atLeastOnce()).closeTunnel(eq(TEST_APN_NAME), anyBoolean());
+        mTestLooper.dispatchAll();
+        verify(mMockEpdgTunnelManager, atLeastOnce())
+                .closeTunnel(
+                        eq(TEST_APN_NAME),
+                        anyBoolean(),
+                        any(IwlanTunnelCallback.class),
+                        any(IwlanTunnelMetricsImpl.class));
 
-        sleep(sleepTime);
+        advanceCalendarByTimeMs(deactivationTime, calendar);
 
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.NO_ERROR));
+        mTestLooper.dispatchAll();
         verify(mMockDataServiceCallback, atLeastOnce())
                 .onDeactivateDataCallComplete(eq(DataServiceCallback.RESULT_SUCCESS));
     }
+
+    private Object callUnsupportedAppUsageMethod(
+            Object target, String methodName, Class<?>[] parameterTypes, Object[] args)
+            throws Exception {
+        Method method = target.getClass().getDeclaredMethod(methodName, parameterTypes);
+        method.setAccessible(true);
+        return method.invoke(target, args);
+    }
 }
diff --git a/test/com/google/android/iwlan/IwlanEventListenerTest.java b/test/com/google/android/iwlan/IwlanEventListenerTest.java
index 789ab25..79d53c0 100644
--- a/test/com/google/android/iwlan/IwlanEventListenerTest.java
+++ b/test/com/google/android/iwlan/IwlanEventListenerTest.java
@@ -18,7 +18,6 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 
-import static org.junit.Assert.*;
 import static org.mockito.Mockito.*;
 
 import android.content.ContentResolver;
@@ -93,15 +92,14 @@
         when(mMockContext.getSystemService(eq(SubscriptionManager.class)))
                 .thenReturn(mMockSubscriptionManager);
 
-        when(mMockSubscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(
-                        eq(DEFAULT_SLOT_INDEX)))
+        when(mMockSubscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(anyInt()))
                 .thenReturn(mMockSubscriptionInfo);
 
         when(mMockContext.getContentResolver()).thenReturn(mMockContentResolver);
 
         when(mMockImsMmTelManager.isVoWiFiSettingEnabled()).thenReturn(true).thenReturn(false);
 
-        when(mMockImsManager.getImsMmTelManager(eq(2))).thenReturn(mMockImsMmTelManager);
+        when(mMockImsManager.getImsMmTelManager(anyInt())).thenReturn(mMockImsMmTelManager);
 
         when(mMockContext.getSystemService(eq(ImsManager.class))).thenReturn(mMockImsManager);
 
@@ -111,6 +109,7 @@
         when(mMockTelephonyManager.createForSubscriptionId(eq(0)))
                 .thenReturn(mMockTelephonyManager);
 
+        IwlanEventListener.resetAllInstances();
         mIwlanEventListener = IwlanEventListener.getInstance(mMockContext, DEFAULT_SLOT_INDEX);
     }
 
@@ -121,7 +120,10 @@
 
     @Test
     public void testWifiApChanged() throws Exception {
-        when(mMockHandler.obtainMessage(eq(IwlanEventListener.WIFI_AP_CHANGED_EVENT)))
+        when(mMockHandler.obtainMessage(
+                        eq(IwlanEventListener.WIFI_AP_CHANGED_EVENT),
+                        eq(DEFAULT_SLOT_INDEX),
+                        anyInt()))
                 .thenReturn(mMockMessage);
 
         events = new ArrayList<Integer>();
@@ -140,7 +142,10 @@
 
     @Test
     public void testCrossSimCallingSettingEnableChanged() throws Exception {
-        when(mMockHandler.obtainMessage(eq(IwlanEventListener.CROSS_SIM_CALLING_ENABLE_EVENT)))
+        when(mMockHandler.obtainMessage(
+                        eq(IwlanEventListener.CROSS_SIM_CALLING_ENABLE_EVENT),
+                        eq(DEFAULT_SLOT_INDEX),
+                        anyInt()))
                 .thenReturn(mMockMessage);
 
         events = new ArrayList<Integer>();
@@ -161,7 +166,10 @@
 
     @Test
     public void testCrossSimCallingSettingDisableChanged() throws Exception {
-        when(mMockHandler.obtainMessage(eq(IwlanEventListener.CROSS_SIM_CALLING_DISABLE_EVENT)))
+        when(mMockHandler.obtainMessage(
+                        eq(IwlanEventListener.CROSS_SIM_CALLING_DISABLE_EVENT),
+                        eq(DEFAULT_SLOT_INDEX),
+                        anyInt()))
                 .thenReturn(mMockMessage);
 
         events = new ArrayList<Integer>();
@@ -182,10 +190,15 @@
 
     @Test
     public void testOnReceivedCarrierConfigChangedIntent() throws Exception {
-        when(mMockHandler.obtainMessage(eq(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT)))
+        when(mMockHandler.obtainMessage(
+                        eq(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT),
+                        eq(DEFAULT_SLOT_INDEX),
+                        anyInt()))
                 .thenReturn(mMockMessage);
         when(mMockHandler.obtainMessage(
-                        eq(IwlanEventListener.CARRIER_CONFIG_UNKNOWN_CARRIER_EVENT)))
+                        eq(IwlanEventListener.CARRIER_CONFIG_UNKNOWN_CARRIER_EVENT),
+                        eq(DEFAULT_SLOT_INDEX),
+                        anyInt()))
                 .thenReturn(mMockMessage_2);
 
         events = new ArrayList<Integer>();
@@ -217,9 +230,15 @@
 
     @Test
     public void testWfcSettingChanged() throws Exception {
-        when(mMockHandler.obtainMessage(eq(IwlanEventListener.WIFI_CALLING_ENABLE_EVENT)))
+        when(mMockHandler.obtainMessage(
+                        eq(IwlanEventListener.WIFI_CALLING_ENABLE_EVENT),
+                        eq(DEFAULT_SLOT_INDEX),
+                        anyInt()))
                 .thenReturn(mMockMessage);
-        when(mMockHandler.obtainMessage(eq(IwlanEventListener.WIFI_CALLING_DISABLE_EVENT)))
+        when(mMockHandler.obtainMessage(
+                        eq(IwlanEventListener.WIFI_CALLING_DISABLE_EVENT),
+                        eq(DEFAULT_SLOT_INDEX),
+                        anyInt()))
                 .thenReturn(mMockMessage_2);
 
         events = new ArrayList<Integer>();
@@ -242,7 +261,11 @@
                 .when(IwlanHelper.getSubId(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
                 .thenReturn(0);
 
-        when(mMockHandler.obtainMessage(eq(IwlanEventListener.CELLINFO_CHANGED_EVENT), eq(arrayCi)))
+        when(mMockHandler.obtainMessage(
+                        eq(IwlanEventListener.CELLINFO_CHANGED_EVENT),
+                        eq(DEFAULT_SLOT_INDEX),
+                        anyInt(),
+                        eq(arrayCi)))
                 .thenReturn(mMockMessage);
 
         events = new ArrayList<Integer>();
@@ -257,4 +280,46 @@
 
         verify(mMockMessage, times(1)).sendToTarget();
     }
+
+    @Test
+    public void testCallStateChanged() throws Exception {
+        when(mMockHandler.obtainMessage(
+                        eq(IwlanEventListener.CALL_STATE_CHANGED_EVENT),
+                        eq(DEFAULT_SLOT_INDEX),
+                        eq(TelephonyManager.CALL_STATE_OFFHOOK)))
+                .thenReturn(mMockMessage);
+
+        events = new ArrayList<Integer>();
+        events.add(IwlanEventListener.CALL_STATE_CHANGED_EVENT);
+        mIwlanEventListener.addEventListener(events, mMockHandler);
+
+        mIwlanEventListener.registerTelephonyCallback();
+
+        TelephonyCallback.CallStateListener mTelephonyCallback =
+                mIwlanEventListener.getTelephonyCallback();
+        mTelephonyCallback.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK);
+
+        verify(mMockMessage, times(1)).sendToTarget();
+    }
+
+    @Test
+    public void testWfcChangeThrowIAE() throws Exception {
+        when(mMockHandler.obtainMessage(
+                        eq(IwlanEventListener.WIFI_CALLING_DISABLE_EVENT),
+                        eq(DEFAULT_SLOT_INDEX),
+                        anyInt()))
+                .thenReturn(mMockMessage);
+
+        events = new ArrayList<Integer>();
+        events.add(IwlanEventListener.WIFI_CALLING_DISABLE_EVENT);
+        mIwlanEventListener.addEventListener(events, mMockHandler);
+        mIwlanEventListener.setWfcEnabledUri(WFC_ENABLED_URI);
+
+        doThrow(new IllegalArgumentException("IllegalArgumentException at isVoWiFiSettingEnabled"))
+                .when(mMockImsMmTelManager)
+                .isVoWiFiSettingEnabled();
+
+        mIwlanEventListener.notifyCurrentSetting(WFC_ENABLED_URI);
+        verify(mMockMessage, times(1)).sendToTarget();
+    }
 }
diff --git a/test/com/google/android/iwlan/IwlanNetworkServiceTest.java b/test/com/google/android/iwlan/IwlanNetworkServiceTest.java
index 000190a..0633900 100644
--- a/test/com/google/android/iwlan/IwlanNetworkServiceTest.java
+++ b/test/com/google/android/iwlan/IwlanNetworkServiceTest.java
@@ -21,8 +21,13 @@
 import static org.junit.Assert.*;
 import static org.mockito.Mockito.*;
 
+import android.annotation.Nullable;
 import android.content.Context;
 import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.vcn.VcnTransportInfo;
 import android.telephony.AccessNetworkConstants;
 import android.telephony.INetworkService;
 import android.telephony.INetworkServiceCallback;
@@ -31,6 +36,8 @@
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsMmTelManager;
 
 import com.google.android.iwlan.IwlanNetworkService.IwlanNetworkServiceProvider;
 
@@ -40,18 +47,23 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
 
 import java.util.Arrays;
 
 public class IwlanNetworkServiceTest {
     private static final String TAG = IwlanNetworkServiceTest.class.getSimpleName();
     private static final int DEFAULT_SLOT_INDEX = 0;
+    private static final int DEFAULT_SUB_INDEX = 0;
 
     @Mock private Context mMockContext;
     @Mock private ConnectivityManager mMockConnectivityManager;
     @Mock private SubscriptionManager mMockSubscriptionManager;
     @Mock private SubscriptionInfo mMockSubscriptionInfo;
+    @Mock private ImsManager mMockImsManager;
+    @Mock private ImsMmTelManager mMockImsMmTelManager;
     @Mock private INetworkServiceCallback mCallback;
+    @Mock private Network mMockNetwork;
     MockitoSession mStaticMockSession;
 
     IwlanNetworkService mIwlanNetworkService;
@@ -64,8 +76,8 @@
 
         mStaticMockSession =
                 mockitoSession()
-                        .mockStatic(IwlanHelper.class)
                         .mockStatic(SubscriptionManager.class)
+                        .strictness(Strictness.LENIENT)
                         .startMocking();
 
         when(mMockContext.getSystemService(eq(ConnectivityManager.class)))
@@ -77,10 +89,17 @@
         when(mMockSubscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(
                         eq(DEFAULT_SLOT_INDEX)))
                 .thenReturn(mMockSubscriptionInfo);
+        when(mMockSubscriptionManager.getDefaultDataSubscriptionId()).thenReturn(DEFAULT_SUB_INDEX);
+        when(mMockSubscriptionManager.getSlotIndex(DEFAULT_SUB_INDEX))
+                .thenReturn(DEFAULT_SLOT_INDEX);
+        when(mMockSubscriptionManager.getSlotIndex(DEFAULT_SUB_INDEX + 1))
+                .thenReturn(DEFAULT_SLOT_INDEX + 1);
 
-        lenient()
-                .when(SubscriptionManager.from(eq(mMockContext)))
-                .thenReturn(mMockSubscriptionManager);
+        when(mMockSubscriptionInfo.getSubscriptionId()).thenReturn(DEFAULT_SUB_INDEX);
+
+        when(mMockContext.getSystemService(eq(ImsManager.class))).thenReturn(mMockImsManager);
+
+        when(mMockImsManager.getImsMmTelManager(anyInt())).thenReturn(mMockImsMmTelManager);
 
         mIwlanNetworkService = new IwlanNetworkService();
         mIwlanNetworkService.setAppContext(mMockContext);
@@ -97,22 +116,23 @@
         mStaticMockSession.finishMocking();
     }
 
-    @Test
-    public void testRequestNetworkRegistrationInfo() throws Exception {
-        int domain = NetworkRegistrationInfo.DOMAIN_PS;
-        boolean mIsSubActive = true;
-        long startTime;
-
+    @Nullable
+    IwlanNetworkServiceProvider initNSP() {
         // Wait for IwlanNetworkServiceProvider created and timeout is 1 second.
-        startTime = System.currentTimeMillis();
+        long startTime = System.currentTimeMillis();
+        IwlanNetworkServiceProvider nsp = null;
         while (System.currentTimeMillis() - startTime < 1000) {
-            mIwlanNetworkServiceProvider =
-                    mIwlanNetworkService.getNetworkServiceProvider(DEFAULT_SLOT_INDEX);
-            if (mIwlanNetworkServiceProvider != null) {
+            nsp = mIwlanNetworkService.getNetworkServiceProvider(DEFAULT_SLOT_INDEX);
+            if (nsp != null) {
                 break;
             }
         }
+        return nsp;
+    }
 
+    @Test
+    public void testRequestNetworkRegistrationInfo() throws Exception {
+        mIwlanNetworkServiceProvider = initNSP();
         assertTrue(mIwlanNetworkServiceProvider != null);
 
         // Set Wifi on and verify mCallback should receive onNetworkStateChanged.
@@ -126,223 +146,202 @@
         // Create expected NetworkRegistrationInfo
         NetworkRegistrationInfo.Builder expectedStateBuilder =
                 generateStateBuilder(
-                        domain, mIsSubActive, NetworkRegistrationInfo.REGISTRATION_STATE_HOME);
+                        NetworkRegistrationInfo.DOMAIN_PS,
+                        true /* isSubActive */,
+                        NetworkRegistrationInfo.REGISTRATION_STATE_HOME);
 
-        mBinder.requestNetworkRegistrationInfo(0, domain, mCallback);
+        mBinder.requestNetworkRegistrationInfo(0, NetworkRegistrationInfo.DOMAIN_PS, mCallback);
 
         verify(mCallback, timeout(1000).times(1))
                 .onRequestNetworkRegistrationInfoComplete(
                         eq(NetworkServiceCallback.RESULT_SUCCESS),
                         eq(expectedStateBuilder.build()));
+    }
 
-        IwlanNetworkService.setNetworkConnected(
-                false, IwlanNetworkService.Transport.UNSPECIFIED_NETWORK);
+    private NetworkCapabilities prepareCellularNetworkCapabilitiesForTest(
+            int subId, boolean isVcn) {
+        NetworkCapabilities.Builder builder =
+                new NetworkCapabilities.Builder()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+        if (isVcn) {
+            builder.setTransportInfo(new VcnTransportInfo(subId));
+        } else {
+            builder.setNetworkSpecifier(new TelephonyNetworkSpecifier(subId));
+        }
+        return builder.build();
+    }
+
+    private NetworkCapabilities prepareWifiNetworkCapabilitiesForTest() {
+        return new NetworkCapabilities.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .build();
     }
 
     @Test
-    public void testNetworkRegistrationInfoForCellularAndCstDisabled() throws Exception {
-        int domain = NetworkRegistrationInfo.DOMAIN_PS;
-        boolean mIsSubActive = true;
-        long startTime;
-
-        // Wait for IwlanNetworkServiceProvider created and timeout is 1 second.
-        startTime = System.currentTimeMillis();
-        while (System.currentTimeMillis() - startTime < 1000) {
-            mIwlanNetworkServiceProvider =
-                    mIwlanNetworkService.getNetworkServiceProvider(DEFAULT_SLOT_INDEX);
-            if (mIwlanNetworkServiceProvider != null) {
-                break;
-            }
-        }
-
-        lenient()
-                .when(
-                        IwlanHelper.isCrossSimCallingEnabled(
-                                eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
-                .thenReturn(false);
-        lenient()
-                .when(IwlanHelper.isDefaultDataSlot(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
-                .thenReturn(true);
-
+    public void testNetworkRegistrationInfoSearchingForCellularAndCstDisabled() throws Exception {
+        mIwlanNetworkServiceProvider = initNSP();
         assertTrue(mIwlanNetworkServiceProvider != null);
 
-        // Set Network on and verify mCallback should receive onNetworkStateChanged.
-        mIwlanNetworkService.setNetworkConnected(true, IwlanNetworkService.Transport.MOBILE);
-        verify(mCallback, timeout(1000).times(1)).onNetworkStateChanged();
+        when(mMockImsMmTelManager.isCrossSimCallingEnabled()).thenReturn(false);
 
-        // Set Sub active and verify mCallback should receive onNetworkStateChanged.
+        NetworkCapabilities nc =
+                prepareCellularNetworkCapabilitiesForTest(DEFAULT_SUB_INDEX, false /* is Vcn */);
+        mIwlanNetworkService.getNetworkMonitorCallback().onCapabilitiesChanged(mMockNetwork, nc);
         mIwlanNetworkServiceProvider.subscriptionChanged();
-        verify(mCallback, timeout(1000).times(2)).onNetworkStateChanged();
 
         // Create expected NetworkRegistrationInfo
         NetworkRegistrationInfo.Builder expectedStateBuilder =
                 generateStateBuilder(
-                        domain,
-                        mIsSubActive,
+                        NetworkRegistrationInfo.DOMAIN_PS,
+                        true /* isSubActive */,
                         NetworkRegistrationInfo.REGISTRATION_STATE_NOT_REGISTERED_SEARCHING);
 
-        mBinder.requestNetworkRegistrationInfo(0, domain, mCallback);
+        mBinder.requestNetworkRegistrationInfo(0, NetworkRegistrationInfo.DOMAIN_PS, mCallback);
 
         verify(mCallback, timeout(1000).times(1))
                 .onRequestNetworkRegistrationInfoComplete(
                         eq(NetworkServiceCallback.RESULT_SUCCESS),
                         eq(expectedStateBuilder.build()));
-
-        IwlanNetworkService.setNetworkConnected(
-                false, IwlanNetworkService.Transport.UNSPECIFIED_NETWORK);
     }
 
     @Test
-    public void testNetworkRegistrationInfoForCellularAndCstEnabled() throws Exception {
-        int domain = NetworkRegistrationInfo.DOMAIN_PS;
-        boolean mIsSubActive = true;
-        long startTime;
-
-        // Wait for IwlanNetworkServiceProvider created and timeout is 1 second.
-        startTime = System.currentTimeMillis();
-        while (System.currentTimeMillis() - startTime < 1000) {
-            mIwlanNetworkServiceProvider =
-                    mIwlanNetworkService.getNetworkServiceProvider(DEFAULT_SLOT_INDEX);
-            if (mIwlanNetworkServiceProvider != null) {
-                break;
-            }
-        }
-
-        lenient()
-                .when(
-                        IwlanHelper.isCrossSimCallingEnabled(
-                                eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
-                .thenReturn(true);
-        lenient()
-                .when(IwlanHelper.isDefaultDataSlot(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
-                .thenReturn(false);
-
+    public void testNetworkRegistrationInfoSearchingForCellularOnSameSubAndCstEnabled()
+            throws Exception {
+        mIwlanNetworkServiceProvider = initNSP();
         assertTrue(mIwlanNetworkServiceProvider != null);
 
-        // Set Network on and verify mCallback should receive onNetworkStateChanged.
-        mIwlanNetworkService.setNetworkConnected(true, IwlanNetworkService.Transport.MOBILE);
-        verify(mCallback, timeout(1000).times(1)).onNetworkStateChanged();
+        when(mMockImsMmTelManager.isCrossSimCallingEnabled()).thenReturn(true);
 
-        // Set Sub active and verify mCallback should receive onNetworkStateChanged.
+        NetworkCapabilities nc =
+                prepareCellularNetworkCapabilitiesForTest(DEFAULT_SUB_INDEX, false /* is Vcn */);
+        mIwlanNetworkService.getNetworkMonitorCallback().onCapabilitiesChanged(mMockNetwork, nc);
         mIwlanNetworkServiceProvider.subscriptionChanged();
-        verify(mCallback, timeout(1000).times(2)).onNetworkStateChanged();
 
         // Create expected NetworkRegistrationInfo
         NetworkRegistrationInfo.Builder expectedStateBuilder =
                 generateStateBuilder(
-                        domain, mIsSubActive, NetworkRegistrationInfo.REGISTRATION_STATE_HOME);
+                        NetworkRegistrationInfo.DOMAIN_PS,
+                        true /* mIsSubActive */,
+                        NetworkRegistrationInfo.REGISTRATION_STATE_NOT_REGISTERED_SEARCHING);
 
-        mBinder.requestNetworkRegistrationInfo(0, domain, mCallback);
+        mBinder.requestNetworkRegistrationInfo(0, NetworkRegistrationInfo.DOMAIN_PS, mCallback);
 
         verify(mCallback, timeout(1000).times(1))
                 .onRequestNetworkRegistrationInfoComplete(
                         eq(NetworkServiceCallback.RESULT_SUCCESS),
                         eq(expectedStateBuilder.build()));
-
-        IwlanNetworkService.setNetworkConnected(
-                false, IwlanNetworkService.Transport.UNSPECIFIED_NETWORK);
     }
 
     @Test
-    public void testNetworkRegistrationInfoForWiFiAndCstEnabled() throws Exception {
-        int domain = NetworkRegistrationInfo.DOMAIN_PS;
-        boolean mIsSubActive = true;
-        long startTime;
-
-        // Wait for IwlanNetworkServiceProvider created and timeout is 1 second.
-        startTime = System.currentTimeMillis();
-        while (System.currentTimeMillis() - startTime < 1000) {
-            mIwlanNetworkServiceProvider =
-                    mIwlanNetworkService.getNetworkServiceProvider(DEFAULT_SLOT_INDEX);
-            if (mIwlanNetworkServiceProvider != null) {
-                break;
-            }
-        }
-
-        lenient()
-                .when(
-                        IwlanHelper.isCrossSimCallingEnabled(
-                                eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
-                .thenReturn(true);
-        lenient()
-                .when(IwlanHelper.isDefaultDataSlot(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
-                .thenReturn(true);
-
+    public void testNetworkRegistrationInfoHomeForCellularOnDifferentSubAndCstEnabled()
+            throws Exception {
+        mIwlanNetworkServiceProvider = initNSP();
         assertTrue(mIwlanNetworkServiceProvider != null);
 
-        // Set Network on and verify mCallback should receive onNetworkStateChanged.
-        mIwlanNetworkService.setNetworkConnected(true, IwlanNetworkService.Transport.WIFI);
-        verify(mCallback, timeout(1000).times(1)).onNetworkStateChanged();
+        when(mMockImsMmTelManager.isCrossSimCallingEnabled()).thenReturn(true);
 
-        // Set Sub active and verify mCallback should receive onNetworkStateChanged.
+        // Cellular data is on the other sub
+        NetworkCapabilities nc =
+                prepareCellularNetworkCapabilitiesForTest(
+                        DEFAULT_SUB_INDEX + 1, false /* is Vcn */);
+        mIwlanNetworkService.getNetworkMonitorCallback().onCapabilitiesChanged(mMockNetwork, nc);
         mIwlanNetworkServiceProvider.subscriptionChanged();
-        verify(mCallback, timeout(1000).times(2)).onNetworkStateChanged();
 
         // Create expected NetworkRegistrationInfo
         NetworkRegistrationInfo.Builder expectedStateBuilder =
                 generateStateBuilder(
-                        domain, mIsSubActive, NetworkRegistrationInfo.REGISTRATION_STATE_HOME);
+                        NetworkRegistrationInfo.DOMAIN_PS,
+                        true /* isSubActive */,
+                        NetworkRegistrationInfo.REGISTRATION_STATE_HOME);
 
-        mBinder.requestNetworkRegistrationInfo(0, domain, mCallback);
+        mBinder.requestNetworkRegistrationInfo(0, NetworkRegistrationInfo.DOMAIN_PS, mCallback);
 
         verify(mCallback, timeout(1000).times(1))
                 .onRequestNetworkRegistrationInfoComplete(
                         eq(NetworkServiceCallback.RESULT_SUCCESS),
                         eq(expectedStateBuilder.build()));
-
-        IwlanNetworkService.setNetworkConnected(
-                false, IwlanNetworkService.Transport.UNSPECIFIED_NETWORK);
     }
 
     @Test
-    public void testNetworkRegistrationInfoForWiFiAndCstDisabled() throws Exception {
-        int domain = NetworkRegistrationInfo.DOMAIN_PS;
-        boolean mIsSubActive = true;
-        long startTime;
-
-        // Wait for IwlanNetworkServiceProvider created and timeout is 1 second.
-        startTime = System.currentTimeMillis();
-        while (System.currentTimeMillis() - startTime < 1000) {
-            mIwlanNetworkServiceProvider =
-                    mIwlanNetworkService.getNetworkServiceProvider(DEFAULT_SLOT_INDEX);
-            if (mIwlanNetworkServiceProvider != null) {
-                break;
-            }
-        }
-
-        lenient()
-                .when(
-                        IwlanHelper.isCrossSimCallingEnabled(
-                                eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
-                .thenReturn(false);
-        lenient()
-                .when(IwlanHelper.isDefaultDataSlot(eq(mMockContext), eq(DEFAULT_SLOT_INDEX)))
-                .thenReturn(true);
-
+    public void testNetworkRegistrationInfoHomeForCellularVcnOnDifferentSubAndCstEnabled()
+            throws Exception {
+        mIwlanNetworkServiceProvider = initNSP();
         assertTrue(mIwlanNetworkServiceProvider != null);
 
-        // Set Network on and verify mCallback should receive onNetworkStateChanged.
-        mIwlanNetworkService.setNetworkConnected(true, IwlanNetworkService.Transport.WIFI);
-        verify(mCallback, timeout(1000).times(1)).onNetworkStateChanged();
+        when(mMockImsMmTelManager.isCrossSimCallingEnabled()).thenReturn(true);
 
-        // Set Sub active and verify mCallback should receive onNetworkStateChanged.
+        // Cellular data as a VCN network is on the other sub
+        NetworkCapabilities nc =
+                prepareCellularNetworkCapabilitiesForTest(DEFAULT_SUB_INDEX + 1, true /* is Vcn */);
+        mIwlanNetworkService.getNetworkMonitorCallback().onCapabilitiesChanged(mMockNetwork, nc);
         mIwlanNetworkServiceProvider.subscriptionChanged();
-        verify(mCallback, timeout(1000).times(2)).onNetworkStateChanged();
 
         // Create expected NetworkRegistrationInfo
         NetworkRegistrationInfo.Builder expectedStateBuilder =
                 generateStateBuilder(
-                        domain, mIsSubActive, NetworkRegistrationInfo.REGISTRATION_STATE_HOME);
+                        NetworkRegistrationInfo.DOMAIN_PS,
+                        true /* isSubActive */,
+                        NetworkRegistrationInfo.REGISTRATION_STATE_HOME);
 
-        mBinder.requestNetworkRegistrationInfo(0, domain, mCallback);
+        mBinder.requestNetworkRegistrationInfo(0, NetworkRegistrationInfo.DOMAIN_PS, mCallback);
 
         verify(mCallback, timeout(1000).times(1))
                 .onRequestNetworkRegistrationInfoComplete(
                         eq(NetworkServiceCallback.RESULT_SUCCESS),
                         eq(expectedStateBuilder.build()));
+    }
 
-        IwlanNetworkService.setNetworkConnected(
-                false, IwlanNetworkService.Transport.UNSPECIFIED_NETWORK);
+    @Test
+    public void testNetworkRegistrationInfoHomeForWiFiAndCstEnabled() throws Exception {
+        mIwlanNetworkServiceProvider = initNSP();
+        assertTrue(mIwlanNetworkServiceProvider != null);
+
+        when(mMockImsMmTelManager.isCrossSimCallingEnabled()).thenReturn(true);
+
+        NetworkCapabilities nc = prepareWifiNetworkCapabilitiesForTest();
+        mIwlanNetworkService.getNetworkMonitorCallback().onCapabilitiesChanged(mMockNetwork, nc);
+        mIwlanNetworkServiceProvider.subscriptionChanged();
+
+        // Create expected NetworkRegistrationInfo
+        NetworkRegistrationInfo.Builder expectedStateBuilder =
+                generateStateBuilder(
+                        NetworkRegistrationInfo.DOMAIN_PS,
+                        true /* isSubActive */,
+                        NetworkRegistrationInfo.REGISTRATION_STATE_HOME);
+
+        mBinder.requestNetworkRegistrationInfo(0, NetworkRegistrationInfo.DOMAIN_PS, mCallback);
+
+        verify(mCallback, timeout(1000).times(1))
+                .onRequestNetworkRegistrationInfoComplete(
+                        eq(NetworkServiceCallback.RESULT_SUCCESS),
+                        eq(expectedStateBuilder.build()));
+    }
+
+    @Test
+    public void testNetworkRegistrationInfoHomeForWiFiAndCstDisabled() throws Exception {
+        mIwlanNetworkServiceProvider = initNSP();
+        assertTrue(mIwlanNetworkServiceProvider != null);
+
+        when(mMockImsMmTelManager.isCrossSimCallingEnabled()).thenReturn(false);
+
+        NetworkCapabilities nc = prepareWifiNetworkCapabilitiesForTest();
+        mIwlanNetworkService.getNetworkMonitorCallback().onCapabilitiesChanged(mMockNetwork, nc);
+
+        mIwlanNetworkServiceProvider.subscriptionChanged();
+
+        // Create expected NetworkRegistrationInfo
+        NetworkRegistrationInfo.Builder expectedStateBuilder =
+                generateStateBuilder(
+                        NetworkRegistrationInfo.DOMAIN_PS,
+                        true /* isSubActive */,
+                        NetworkRegistrationInfo.REGISTRATION_STATE_HOME);
+
+        mBinder.requestNetworkRegistrationInfo(0, NetworkRegistrationInfo.DOMAIN_PS, mCallback);
+
+        verify(mCallback, timeout(1000).times(1))
+                .onRequestNetworkRegistrationInfoComplete(
+                        eq(NetworkServiceCallback.RESULT_SUCCESS),
+                        eq(expectedStateBuilder.build()));
     }
 
     private NetworkRegistrationInfo.Builder generateStateBuilder(
diff --git a/test/com/google/android/iwlan/TunnelMetricsInterfaceTest.java b/test/com/google/android/iwlan/TunnelMetricsInterfaceTest.java
new file mode 100644
index 0000000..509037f
--- /dev/null
+++ b/test/com/google/android/iwlan/TunnelMetricsInterfaceTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.google.android.iwlan;
+
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+
+import com.google.android.iwlan.TunnelMetricsInterface.*;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(JUnit4.class)
+public class TunnelMetricsInterfaceTest {
+    private static final String TEST_EPDG_ADDRESS = "127.0.0.1";
+    private static final String TEST_APN_NAME = "www.xyz.com";
+
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Test
+    public void testTunnelMetricsBuilder() {
+        TunnelMetricsData metricsData =
+                new TunnelMetricsData.Builder()
+                        .setApnName(TEST_APN_NAME)
+                        .setEpdgServerAddress(InetAddresses.parseNumericAddress(TEST_EPDG_ADDRESS))
+                        .build();
+        assertEquals(TEST_APN_NAME, metricsData.getApnName());
+        assertEquals(TEST_EPDG_ADDRESS, metricsData.getEpdgServerAddress());
+    }
+
+    @Test
+    public void testOnOpenedMetricsBuilder() {
+        OnOpenedMetrics metricsData =
+                new OnOpenedMetrics.Builder()
+                        .setApnName(TEST_APN_NAME)
+                        .setEpdgServerAddress(InetAddresses.parseNumericAddress(TEST_EPDG_ADDRESS))
+                        .build();
+        assertEquals(TEST_APN_NAME, metricsData.getApnName());
+        assertEquals(TEST_EPDG_ADDRESS, metricsData.getEpdgServerAddress());
+    }
+
+    @Test
+    public void testOnClosedMetricsBuilder() {
+        OnClosedMetrics metricsData =
+                new OnClosedMetrics.Builder()
+                        .setApnName(TEST_APN_NAME)
+                        .setEpdgServerAddress(InetAddresses.parseNumericAddress(TEST_EPDG_ADDRESS))
+                        .build();
+        assertEquals(TEST_APN_NAME, metricsData.getApnName());
+        assertEquals(TEST_EPDG_ADDRESS, metricsData.getEpdgServerAddress());
+    }
+}
diff --git a/test/com/google/android/iwlan/epdg/EpdgSelectorTest.java b/test/com/google/android/iwlan/epdg/EpdgSelectorTest.java
index 9a4af93..3f23dab 100644
--- a/test/com/google/android/iwlan/epdg/EpdgSelectorTest.java
+++ b/test/com/google/android/iwlan/epdg/EpdgSelectorTest.java
@@ -44,11 +44,13 @@
 import android.telephony.CellInfoLte;
 import android.telephony.CellInfoNr;
 import android.telephony.CellInfoWcdma;
+import android.telephony.DataFailCause;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.util.Log;
 
+import com.google.android.iwlan.ErrorPolicyManager;
 import com.google.android.iwlan.IwlanError;
 
 import org.junit.After;
@@ -75,6 +77,11 @@
     private static final String TEST_IP_ADDRESS = "127.0.0.1";
     private static final String TEST_IP_ADDRESS_1 = "127.0.0.2";
     private static final String TEST_IP_ADDRESS_2 = "127.0.0.3";
+    private static final String TEST_IP_ADDRESS_3 = "127.0.0.4";
+    private static final String TEST_IP_ADDRESS_4 = "127.0.0.5";
+    private static final String TEST_IP_ADDRESS_5 = "127.0.0.6";
+    private static final String TEST_IP_ADDRESS_6 = "127.0.0.7";
+    private static final String TEST_IP_ADDRESS_7 = "127.0.0.8";
     private static final String TEST_IPV6_ADDRESS = "0000:0000:0000:0000:0000:0000:0000:0001";
 
     private static int testPcoIdIPv6 = 0xFF01;
@@ -86,6 +93,7 @@
 
     @Mock private Context mMockContext;
     @Mock private Network mMockNetwork;
+    @Mock private ErrorPolicyManager mMockErrorPolicyManager;
     @Mock private SubscriptionManager mMockSubscriptionManager;
     @Mock private SubscriptionInfo mMockSubscriptionInfo;
     @Mock private CarrierConfigManager mMockCarrierConfigManager;
@@ -108,9 +116,15 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mStaticMockSession = mockitoSession().mockStatic(DnsResolver.class).startMocking();
+        mStaticMockSession =
+                mockitoSession()
+                        .mockStatic(DnsResolver.class)
+                        .mockStatic(ErrorPolicyManager.class)
+                        .startMocking();
 
-        mEpdgSelector = new EpdgSelector(mMockContext, DEFAULT_SLOT_INDEX);
+        when(ErrorPolicyManager.getInstance(mMockContext, DEFAULT_SLOT_INDEX))
+                .thenReturn(mMockErrorPolicyManager);
+        mEpdgSelector = spy(new EpdgSelector(mMockContext, DEFAULT_SLOT_INDEX));
 
         when(mMockContext.getSystemService(eq(SubscriptionManager.class)))
                 .thenReturn(mMockSubscriptionManager);
@@ -122,6 +136,8 @@
 
         when(mMockSubscriptionInfo.getMncString()).thenReturn("120");
 
+        when(mMockTelephonyManager.getNetworkOperator()).thenReturn("311120");
+
         when(mMockContext.getSystemService(eq(TelephonyManager.class)))
                 .thenReturn(mMockTelephonyManager);
 
@@ -140,12 +156,13 @@
 
         // Mock carrier configs with test bundle
         mTestBundle = new PersistableBundle();
+        mTestBundle.putInt(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT,
+                CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV4_PREFERRED);
         when(mMockContext.getSystemService(eq(CarrierConfigManager.class)))
                 .thenReturn(mMockCarrierConfigManager);
         when(mMockCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(mTestBundle);
 
-        lenient().when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
-
         mFakeDns = new FakeDns();
         mFakeDns.startMocking();
     }
@@ -158,6 +175,10 @@
 
     @Test
     public void testStaticMethodPass() throws Exception {
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
+
         // Set DnsResolver query mock
         final String testStaticAddress = "epdg.epc.mnc088.mcc888.pub.3gppnetwork.org";
         mFakeDns.setAnswer(testStaticAddress, new String[] {TEST_IP_ADDRESS}, TYPE_A);
@@ -174,12 +195,32 @@
 
         InetAddress expectedAddress = InetAddress.getByName(TEST_IP_ADDRESS);
 
-        assertEquals(testInetAddresses.size(), 1);
-        assertEquals(testInetAddresses.get(0), expectedAddress);
+        assertEquals(1, testInetAddresses.size());
+        assertEquals(expectedAddress, testInetAddresses.get(0));
+    }
+
+    @Test
+    public void testStaticMethodDirectIpAddress_noDnsResolution() throws Exception {
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY,
+                new int[] {CarrierConfigManager.Iwlan.EPDG_ADDRESS_STATIC});
+        // Carrier config directly contains the ePDG IP address.
+        mTestBundle.putString(
+                CarrierConfigManager.Iwlan.KEY_EPDG_STATIC_ADDRESS_STRING, TEST_IP_ADDRESS);
+
+        ArrayList<InetAddress> testInetAddresses =
+                getValidatedServerListWithDefaultParams(false /*isEmergency*/);
+
+        assertEquals(1, testInetAddresses.size());
+        assertEquals(InetAddresses.parseNumericAddress(TEST_IP_ADDRESS), testInetAddresses.get(0));
     }
 
     @Test
     public void testRoamStaticMethodPass() throws Exception {
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
+
         // Set DnsResolver query mock
         final String testRoamStaticAddress = "epdg.epc.mnc088.mcc888.pub.3gppnetwork.org";
         mFakeDns.setAnswer(testRoamStaticAddress, new String[] {TEST_IP_ADDRESS}, TYPE_A);
@@ -197,8 +238,8 @@
 
         InetAddress expectedAddress = InetAddress.getByName(TEST_IP_ADDRESS);
 
-        assertEquals(testInetAddresses.size(), 1);
-        assertEquals(testInetAddresses.get(0), expectedAddress);
+        assertEquals(1, testInetAddresses.size());
+        assertEquals(expectedAddress, testInetAddresses.get(0));
     }
 
     @Test
@@ -213,58 +254,275 @@
 
     @Test
     public void testPlmnResolutionMethodWithNoPlmnInCarrierConfig() throws Exception {
-        // setUp() fills default values for mcc-mnc
-        String expectedFqdn1 = "epdg.epc.mnc120.mcc311.pub.3gppnetwork.org";
-        String expectedFqdn2 = "epdg.epc.mnc120.mcc300.pub.3gppnetwork.org";
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
 
-        mFakeDns.setAnswer(expectedFqdn1, new String[] {TEST_IP_ADDRESS_1}, TYPE_A);
-        mFakeDns.setAnswer(expectedFqdn2, new String[] {TEST_IP_ADDRESS_2}, TYPE_A);
+        // setUp() fills default values for mcc-mnc
+        String expectedFqdnFromImsi = "epdg.epc.mnc120.mcc311.pub.3gppnetwork.org";
+        String expectedFqdnFromEhplmn = "epdg.epc.mnc120.mcc300.pub.3gppnetwork.org";
+
+        mFakeDns.setAnswer(expectedFqdnFromImsi, new String[] {TEST_IP_ADDRESS_1}, TYPE_A);
+        mFakeDns.setAnswer(expectedFqdnFromEhplmn, new String[] {TEST_IP_ADDRESS_2}, TYPE_A);
 
         ArrayList<InetAddress> testInetAddresses =
                 getValidatedServerListWithDefaultParams(false /*isEmergency*/);
 
-        assertEquals(testInetAddresses.size(), 2);
+        assertEquals(2, testInetAddresses.size());
         assertTrue(testInetAddresses.contains(InetAddress.getByName(TEST_IP_ADDRESS_1)));
         assertTrue(testInetAddresses.contains(InetAddress.getByName(TEST_IP_ADDRESS_2)));
     }
 
     private void testPlmnResolutionMethod(boolean isEmergency) throws Exception {
-        String expectedFqdn1 =
-                (isEmergency)
-                        ? "sos.epdg.epc.mnc480.mcc310.pub.3gppnetwork.org"
-                        : "epdg.epc.mnc480.mcc310.pub.3gppnetwork.org";
-        String expectedFqdn2 =
-                (isEmergency)
-                        ? "sos.epdg.epc.mnc120.mcc300.pub.3gppnetwork.org"
-                        : "epdg.epc.mnc120.mcc300.pub.3gppnetwork.org";
-        String expectedFqdn3 =
-                (isEmergency)
-                        ? "sos.epdg.epc.mnc120.mcc311.pub.3gppnetwork.org"
-                        : "epdg.epc.mnc120.mcc311.pub.3gppnetwork.org";
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
+
+        String expectedFqdnFromImsi = "epdg.epc.mnc120.mcc311.pub.3gppnetwork.org";
+        String expectedFqdnFromRplmn = "epdg.epc.mnc121.mcc311.pub.3gppnetwork.org";
+        String expectedFqdnFromEhplmn = "epdg.epc.mnc120.mcc300.pub.3gppnetwork.org";
+        String excludedFqdnFromConfig = "epdg.epc.mnc480.mcc310.pub.3gppnetwork.org";
+
+        when(mMockTelephonyManager.getNetworkOperator()).thenReturn("311121");
 
         mTestBundle.putIntArray(
                 CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY,
                 new int[] {CarrierConfigManager.Iwlan.EPDG_ADDRESS_PLMN});
         mTestBundle.putStringArray(
                 CarrierConfigManager.Iwlan.KEY_MCC_MNCS_STRING_ARRAY,
-                new String[] {"310-480", "300-120", "311-120"});
+                new String[] {"310-480", "300-120", "311-120", "311-121"});
 
-        mFakeDns.setAnswer(expectedFqdn1, new String[] {TEST_IP_ADDRESS_1}, TYPE_A);
-        mFakeDns.setAnswer(expectedFqdn2, new String[] {TEST_IP_ADDRESS_2}, TYPE_A);
-        mFakeDns.setAnswer(expectedFqdn3, new String[] {TEST_IP_ADDRESS}, TYPE_A);
+        mFakeDns.setAnswer(expectedFqdnFromImsi, new String[] {TEST_IP_ADDRESS}, TYPE_A);
+        mFakeDns.setAnswer(expectedFqdnFromEhplmn, new String[] {TEST_IP_ADDRESS_1}, TYPE_A);
+        mFakeDns.setAnswer(excludedFqdnFromConfig, new String[] {TEST_IP_ADDRESS_2}, TYPE_A);
+        mFakeDns.setAnswer("sos." + expectedFqdnFromImsi, new String[] {TEST_IP_ADDRESS_3}, TYPE_A);
+        mFakeDns.setAnswer(
+                "sos." + expectedFqdnFromEhplmn, new String[] {TEST_IP_ADDRESS_4}, TYPE_A);
+        mFakeDns.setAnswer(
+                "sos." + excludedFqdnFromConfig, new String[] {TEST_IP_ADDRESS_5}, TYPE_A);
+        mFakeDns.setAnswer(expectedFqdnFromRplmn, new String[] {TEST_IP_ADDRESS_6}, TYPE_A);
+        mFakeDns.setAnswer(
+                "sos." + expectedFqdnFromRplmn, new String[] {TEST_IP_ADDRESS_7}, TYPE_A);
 
         ArrayList<InetAddress> testInetAddresses =
                 getValidatedServerListWithDefaultParams(isEmergency);
 
-        assertEquals(testInetAddresses.size(), 3);
-        assertEquals(testInetAddresses.get(0), InetAddress.getByName(TEST_IP_ADDRESS));
-        assertEquals(testInetAddresses.get(1), InetAddress.getByName(TEST_IP_ADDRESS_2));
-        assertEquals(testInetAddresses.get(2), InetAddress.getByName(TEST_IP_ADDRESS_1));
+        if (isEmergency) {
+            assertEquals(6, testInetAddresses.size());
+            assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_7), testInetAddresses.get(0));
+            assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_6), testInetAddresses.get(1));
+            assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_3), testInetAddresses.get(2));
+            assertEquals(InetAddress.getByName(TEST_IP_ADDRESS), testInetAddresses.get(3));
+            assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_4), testInetAddresses.get(4));
+            assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_1), testInetAddresses.get(5));
+        } else {
+            assertEquals(3, testInetAddresses.size());
+            assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_6), testInetAddresses.get(0));
+            assertEquals(InetAddress.getByName(TEST_IP_ADDRESS), testInetAddresses.get(1));
+            assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_1), testInetAddresses.get(2));
+        }
+    }
+
+    @Test
+    public void testPlmnResolutionMethodWithDuplicatedImsiAndEhplmn() throws Exception {
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
+
+        String fqdnFromEhplmn1 = "epdg.epc.mnc120.mcc300.pub.3gppnetwork.org";
+        String fqdnFromEhplmn2AndImsi = "epdg.epc.mnc120.mcc311.pub.3gppnetwork.org";
+        String fqdnFromEhplmn3 = "epdg.epc.mnc122.mcc300.pub.3gppnetwork.org";
+        String fqdnFromEhplmn4 = "epdg.epc.mnc123.mcc300.pub.3gppnetwork.org";
+
+        when(mMockTelephonyManager.getNetworkOperator()).thenReturn("300121");
+        ehplmnList.add("300122");
+        ehplmnList.add("300123");
+
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY,
+                new int[] {CarrierConfigManager.Iwlan.EPDG_ADDRESS_PLMN});
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_PLMN_PRIORITY_INT_ARRAY,
+                new int[] {
+                    CarrierConfigManager.Iwlan.EPDG_PLMN_HPLMN,
+                    CarrierConfigManager.Iwlan.EPDG_PLMN_EHPLMN_ALL,
+                });
+
+        mFakeDns.setAnswer(fqdnFromEhplmn1, new String[] {TEST_IP_ADDRESS}, TYPE_A);
+        mFakeDns.setAnswer(fqdnFromEhplmn2AndImsi, new String[] {TEST_IP_ADDRESS_1}, TYPE_A);
+        mFakeDns.setAnswer(fqdnFromEhplmn3, new String[] {TEST_IP_ADDRESS_2}, TYPE_A);
+        mFakeDns.setAnswer(fqdnFromEhplmn4, new String[] {TEST_IP_ADDRESS_3}, TYPE_A);
+
+        ArrayList<InetAddress> testInetAddresses = getValidatedServerListWithDefaultParams(false);
+
+        assertEquals(4, testInetAddresses.size());
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_1), testInetAddresses.get(0));
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS), testInetAddresses.get(1));
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_2), testInetAddresses.get(2));
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_3), testInetAddresses.get(3));
+    }
+
+    @Test
+    public void testPlmnResolutionMethodWithInvalidLengthPlmns() throws Exception {
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
+
+        when(mMockSubscriptionInfo.getMccString()).thenReturn("31");
+        when(mMockSubscriptionInfo.getMncString()).thenReturn("12");
+
+        when(mMockTelephonyManager.getNetworkOperator()).thenReturn("300");
+        ehplmnList.add("3001");
+        ehplmnList.add("3");
+
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY,
+                new int[] {CarrierConfigManager.Iwlan.EPDG_ADDRESS_PLMN});
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_PLMN_PRIORITY_INT_ARRAY,
+                new int[] {
+                    CarrierConfigManager.Iwlan.EPDG_PLMN_RPLMN,
+                    CarrierConfigManager.Iwlan.EPDG_PLMN_HPLMN,
+                    CarrierConfigManager.Iwlan.EPDG_PLMN_EHPLMN_ALL,
+                });
+
+        ArrayList<InetAddress> testInetAddresses = getValidatedServerListWithDefaultParams(false);
+
+        assertEquals(0, testInetAddresses.size());
+    }
+
+    @Test
+    public void testPlmnResolutionMethodWithInvalidCharacterPlmns() throws Exception {
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
+
+        when(mMockSubscriptionInfo.getMccString()).thenReturn("a b");
+        when(mMockSubscriptionInfo.getMncString()).thenReturn("!@#");
+
+        when(mMockTelephonyManager.getNetworkOperator()).thenReturn("a cde#");
+        ehplmnList.add("abcdef");
+        ehplmnList.add("1 23456");
+        ehplmnList.add("1 2345");
+
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY,
+                new int[] {CarrierConfigManager.Iwlan.EPDG_ADDRESS_PLMN});
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_PLMN_PRIORITY_INT_ARRAY,
+                new int[] {
+                    CarrierConfigManager.Iwlan.EPDG_PLMN_RPLMN,
+                    CarrierConfigManager.Iwlan.EPDG_PLMN_HPLMN,
+                    CarrierConfigManager.Iwlan.EPDG_PLMN_EHPLMN_ALL,
+                });
+
+        ArrayList<InetAddress> testInetAddresses = getValidatedServerListWithDefaultParams(false);
+
+        assertEquals(0, testInetAddresses.size());
+    }
+
+    @Test
+    public void testPlmnResolutionMethodWithEmptyPlmns() throws Exception {
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
+
+        when(mMockSubscriptionInfo.getMccString()).thenReturn(null);
+        when(mMockSubscriptionInfo.getMncString()).thenReturn(null);
+
+        when(mMockTelephonyManager.getNetworkOperator()).thenReturn("");
+        ehplmnList.add("");
+
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY,
+                new int[] {CarrierConfigManager.Iwlan.EPDG_ADDRESS_PLMN});
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_PLMN_PRIORITY_INT_ARRAY,
+                new int[] {
+                    CarrierConfigManager.Iwlan.EPDG_PLMN_RPLMN,
+                    CarrierConfigManager.Iwlan.EPDG_PLMN_HPLMN,
+                    CarrierConfigManager.Iwlan.EPDG_PLMN_EHPLMN_ALL,
+                });
+
+        ArrayList<InetAddress> testInetAddresses = getValidatedServerListWithDefaultParams(false);
+
+        assertEquals(0, testInetAddresses.size());
+    }
+
+    @Test
+    public void testPlmnResolutionMethodWithFirstEhplmn() throws Exception {
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
+
+        String fqdnFromEhplmn1 = "epdg.epc.mnc120.mcc300.pub.3gppnetwork.org";
+        String fqdnFromEhplmn2 = "epdg.epc.mnc121.mcc300.pub.3gppnetwork.org";
+        String fqdnFromEhplmn3 = "epdg.epc.mnc122.mcc300.pub.3gppnetwork.org";
+        String fqdnFromEhplmn4 = "epdg.epc.mnc123.mcc300.pub.3gppnetwork.org";
+
+        ehplmnList.add("300121");
+        ehplmnList.add("300122");
+        ehplmnList.add("300123");
+
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY,
+                new int[] {CarrierConfigManager.Iwlan.EPDG_ADDRESS_PLMN});
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_PLMN_PRIORITY_INT_ARRAY,
+                new int[] {CarrierConfigManager.Iwlan.EPDG_PLMN_EHPLMN_FIRST});
+
+        mFakeDns.setAnswer(fqdnFromEhplmn1, new String[] {TEST_IP_ADDRESS}, TYPE_A);
+        mFakeDns.setAnswer(fqdnFromEhplmn2, new String[] {TEST_IP_ADDRESS_1}, TYPE_A);
+        mFakeDns.setAnswer(fqdnFromEhplmn3, new String[] {TEST_IP_ADDRESS_2}, TYPE_A);
+        mFakeDns.setAnswer(fqdnFromEhplmn4, new String[] {TEST_IP_ADDRESS_3}, TYPE_A);
+
+        ArrayList<InetAddress> testInetAddresses = getValidatedServerListWithDefaultParams(false);
+
+        assertEquals(1, testInetAddresses.size());
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS), testInetAddresses.get(0));
+    }
+
+    @Test
+    public void testPlmnResolutionMethodWithRplmn() throws Exception {
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
+
+        String fqdnFromRplmn = "epdg.epc.mnc122.mcc300.pub.3gppnetwork.org";
+        String fqdnFromEhplmn1 = "epdg.epc.mnc120.mcc300.pub.3gppnetwork.org";
+        String fqdnFromEhplmn2 = "epdg.epc.mnc121.mcc300.pub.3gppnetwork.org";
+
+        when(mMockTelephonyManager.getNetworkOperator()).thenReturn("300122");
+        ehplmnList.add("300121");
+
+        mTestBundle.putStringArray(
+                CarrierConfigManager.Iwlan.KEY_MCC_MNCS_STRING_ARRAY,
+                new String[] {"310-480", "300-122", "300-121"});
+
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY,
+                new int[] {CarrierConfigManager.Iwlan.EPDG_ADDRESS_PLMN});
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_PLMN_PRIORITY_INT_ARRAY,
+                new int[] {CarrierConfigManager.Iwlan.EPDG_PLMN_RPLMN});
+
+        mFakeDns.setAnswer(fqdnFromRplmn, new String[] {TEST_IP_ADDRESS}, TYPE_A);
+        mFakeDns.setAnswer(fqdnFromEhplmn1, new String[] {TEST_IP_ADDRESS_1}, TYPE_A);
+        mFakeDns.setAnswer(fqdnFromEhplmn2, new String[] {TEST_IP_ADDRESS_2}, TYPE_A);
+
+        ArrayList<InetAddress> testInetAddresses = getValidatedServerListWithDefaultParams(false);
+
+        assertEquals(1, testInetAddresses.size());
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS), testInetAddresses.get(0));
     }
 
     @Test
     public void testCarrierConfigStaticAddressList() throws Exception {
-        // Set Network.getAllByName mock
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
+
+        // Set DnsResolver query mock
         final String addr1 = "epdg.epc.mnc480.mcc310.pub.3gppnetwork.org";
         final String addr2 = "epdg.epc.mnc120.mcc300.pub.3gppnetwork.org";
         final String addr3 = "epdg.epc.mnc120.mcc311.pub.3gppnetwork.org";
@@ -284,27 +542,37 @@
         ArrayList<InetAddress> testInetAddresses =
                 getValidatedServerListWithDefaultParams(false /*isEmergency*/);
 
-        assertEquals(testInetAddresses.size(), 3);
-        assertEquals(testInetAddresses.get(0), InetAddress.getByName(TEST_IP_ADDRESS_1));
-        assertEquals(testInetAddresses.get(1), InetAddress.getByName(TEST_IP_ADDRESS_2));
-        assertEquals(testInetAddresses.get(2), InetAddress.getByName(TEST_IP_ADDRESS));
+        assertEquals(3, testInetAddresses.size());
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_1), testInetAddresses.get(0));
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_2), testInetAddresses.get(1));
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS), testInetAddresses.get(2));
     }
 
     private ArrayList<InetAddress> getValidatedServerListWithDefaultParams(boolean isEmergency)
             throws Exception {
+        return getValidatedServerListWithIpPreference(
+                EpdgSelector.PROTO_FILTER_IPV4V6, EpdgSelector.IPV4_PREFERRED, isEmergency);
+    }
+
+    private ArrayList<InetAddress> getValidatedServerListWithIpPreference(
+            @EpdgSelector.ProtoFilter int filter,
+            @EpdgSelector.EpdgAddressOrder int order,
+            boolean isEmergency)
+            throws Exception {
         ArrayList<InetAddress> testInetAddresses = new ArrayList<InetAddress>();
         final CountDownLatch latch = new CountDownLatch(1);
         IwlanError ret =
                 mEpdgSelector.getValidatedServerList(
                         1234,
-                        EpdgSelector.PROTO_FILTER_IPV4V6,
-                        false /*isRoaming*/,
+                        filter,
+                        order,
+                        false /* isRoaming */,
                         isEmergency,
                         mMockNetwork,
                         new EpdgSelector.EpdgSelectorCallback() {
                             @Override
                             public void onServerListChanged(
-                                    int transactionId, ArrayList<InetAddress> validIPList) {
+                                    int transactionId, List<InetAddress> validIPList) {
                                 assertEquals(transactionId, 1234);
 
                                 for (InetAddress mInetAddress : validIPList) {
@@ -357,7 +625,7 @@
         ArrayList<InetAddress> testInetAddresses =
                 getValidatedServerListWithDefaultParams(false /* isEmergency */);
 
-        assertEquals(testInetAddresses.size(), 2);
+        assertEquals(2, testInetAddresses.size());
         assertTrue(testInetAddresses.contains(InetAddress.getByName(TEST_IP_ADDRESS)));
         assertTrue(testInetAddresses.contains(InetAddress.getByName(TEST_IPV6_ADDRESS)));
     }
@@ -378,6 +646,8 @@
     }
 
     private void testCellularResolutionMethod(boolean isEmergency) throws Exception {
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+
         int testMcc = 311;
         int testMnc = 120;
         String testMccString = "311";
@@ -429,10 +699,10 @@
         ArrayList<InetAddress> testInetAddresses =
                 getValidatedServerListWithDefaultParams(isEmergency);
 
-        assertEquals(testInetAddresses.size(), 3);
-        assertEquals(testInetAddresses.get(0), InetAddress.getByName(TEST_IP_ADDRESS));
-        assertEquals(testInetAddresses.get(1), InetAddress.getByName(TEST_IP_ADDRESS_1));
-        assertEquals(testInetAddresses.get(2), InetAddress.getByName(TEST_IP_ADDRESS_2));
+        assertEquals(3, testInetAddresses.size());
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS), testInetAddresses.get(0));
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_1), testInetAddresses.get(1));
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_2), testInetAddresses.get(2));
     }
 
     private void setAnswerForCellularMethod(boolean isEmergency, int mcc, int mnc)
@@ -471,6 +741,197 @@
         mFakeDns.setAnswer(expectedFqdn3, new String[] {TEST_IP_ADDRESS_2}, TYPE_A);
     }
 
+    @Test
+    public void testGetValidatedServerListIpv4Preferred() throws Exception {
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
+
+        final String addr1 = "epdg.epc.mnc120.mcc300.pub.3gppnetwork.org";
+        final String addr2 = "epdg.epc.mnc120.mcc311.pub.3gppnetwork.org";
+        final String testStaticAddress = addr1 + "," + addr2;
+
+        mFakeDns.setAnswer(addr1, new String[] {TEST_IP_ADDRESS_1}, TYPE_A);
+        mFakeDns.setAnswer(addr2, new String[] {TEST_IPV6_ADDRESS}, TYPE_AAAA);
+
+        // Set carrier config mock
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY,
+                new int[] {CarrierConfigManager.Iwlan.EPDG_ADDRESS_STATIC});
+        mTestBundle.putString(
+                CarrierConfigManager.Iwlan.KEY_EPDG_STATIC_ADDRESS_STRING, testStaticAddress);
+
+        ArrayList<InetAddress> testInetAddresses =
+                getValidatedServerListWithIpPreference(
+                        EpdgSelector.PROTO_FILTER_IPV4V6,
+                        EpdgSelector.IPV4_PREFERRED,
+                        false /*isEmergency*/);
+
+        assertEquals(2, testInetAddresses.size());
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_1), testInetAddresses.get(0));
+        assertEquals(InetAddress.getByName(TEST_IPV6_ADDRESS), testInetAddresses.get(1));
+    }
+
+    @Test
+    public void testGetValidatedServerListIpv6Preferred() throws Exception {
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
+
+        final String addr1 = "epdg.epc.mnc120.mcc300.pub.3gppnetwork.org";
+        final String addr2 = "epdg.epc.mnc120.mcc311.pub.3gppnetwork.org";
+        final String testStaticAddress = addr1 + "," + addr2;
+
+        mFakeDns.setAnswer(addr1, new String[] {TEST_IP_ADDRESS_1}, TYPE_A);
+        mFakeDns.setAnswer(addr2, new String[] {TEST_IPV6_ADDRESS}, TYPE_AAAA);
+
+        // Set carrier config mock
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY,
+                new int[] {CarrierConfigManager.Iwlan.EPDG_ADDRESS_STATIC});
+        mTestBundle.putString(
+                CarrierConfigManager.Iwlan.KEY_EPDG_STATIC_ADDRESS_STRING, testStaticAddress);
+
+        ArrayList<InetAddress> testInetAddresses =
+                getValidatedServerListWithIpPreference(
+                        EpdgSelector.PROTO_FILTER_IPV4V6,
+                        EpdgSelector.IPV6_PREFERRED,
+                        false /*isEmergency*/);
+
+        assertEquals(2, testInetAddresses.size());
+        assertEquals(InetAddress.getByName(TEST_IPV6_ADDRESS), testInetAddresses.get(0));
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_1), testInetAddresses.get(1));
+    }
+
+    @Test
+    public void testGetValidatedServerListIpv4Only() throws Exception {
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
+
+        final String addr1 = "epdg.epc.mnc120.mcc300.pub.3gppnetwork.org";
+        final String addr2 = "epdg.epc.mnc120.mcc311.pub.3gppnetwork.org";
+        final String testStaticAddress = addr1 + "," + addr2;
+
+        mFakeDns.setAnswer(addr1, new String[] {TEST_IP_ADDRESS_1}, TYPE_A);
+        mFakeDns.setAnswer(addr2, new String[] {TEST_IPV6_ADDRESS}, TYPE_AAAA);
+
+        // Set carrier config mock
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY,
+                new int[] {CarrierConfigManager.Iwlan.EPDG_ADDRESS_STATIC});
+        mTestBundle.putString(
+                CarrierConfigManager.Iwlan.KEY_EPDG_STATIC_ADDRESS_STRING, testStaticAddress);
+
+        ArrayList<InetAddress> testInetAddresses =
+                getValidatedServerListWithIpPreference(
+                        EpdgSelector.PROTO_FILTER_IPV4,
+                        EpdgSelector.SYSTEM_PREFERRED,
+                        false /*isEmergency*/);
+
+        assertEquals(1, testInetAddresses.size());
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_1), testInetAddresses.get(0));
+    }
+
+    @Test
+    public void testGetValidatedServerListIpv4OnlyCongestion() throws Exception {
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
+
+        when(mMockErrorPolicyManager.getMostRecentDataFailCause())
+                .thenReturn(DataFailCause.IWLAN_CONGESTION);
+        when(mMockErrorPolicyManager.getCurrentFqdnIndex(anyInt())).thenReturn(0);
+
+        String expectedFqdnFromHplmn = "epdg.epc.mnc120.mcc311.pub.3gppnetwork.org";
+        String expectedFqdnFromEHplmn = "epdg.epc.mnc120.mcc300.pub.3gppnetwork.org";
+        String expectedFqdnFromConfig = "epdg.epc.mnc480.mcc310.pub.3gppnetwork.org";
+
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY,
+                new int[] {CarrierConfigManager.Iwlan.EPDG_ADDRESS_PLMN});
+        mTestBundle.putStringArray(
+                CarrierConfigManager.Iwlan.KEY_MCC_MNCS_STRING_ARRAY,
+                new String[] {"310-480", "300-120", "311-120"});
+
+        mFakeDns.setAnswer(expectedFqdnFromHplmn, new String[] {TEST_IPV6_ADDRESS}, TYPE_AAAA);
+        mFakeDns.setAnswer(expectedFqdnFromEHplmn, new String[] {TEST_IP_ADDRESS_1}, TYPE_A);
+        mFakeDns.setAnswer(expectedFqdnFromConfig, new String[] {TEST_IP_ADDRESS_2}, TYPE_A);
+
+        ArrayList<InetAddress> testInetAddresses =
+                getValidatedServerListWithIpPreference(
+                        EpdgSelector.PROTO_FILTER_IPV4,
+                        EpdgSelector.SYSTEM_PREFERRED,
+                        false /*isEmergency*/);
+
+        assertEquals(1, testInetAddresses.size());
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_1), testInetAddresses.get(0));
+    }
+
+    @Test
+    public void testGetValidatedServerListIpv6Only() throws Exception {
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
+
+        final String addr1 = "epdg.epc.mnc120.mcc300.pub.3gppnetwork.org";
+        final String addr2 = "epdg.epc.mnc120.mcc311.pub.3gppnetwork.org";
+        final String testStaticAddress = addr1 + "," + addr2;
+
+        mFakeDns.setAnswer(addr1, new String[] {TEST_IP_ADDRESS_1}, TYPE_A);
+        mFakeDns.setAnswer(addr2, new String[] {TEST_IPV6_ADDRESS}, TYPE_AAAA);
+
+        // Set carrier config mock
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY,
+                new int[] {CarrierConfigManager.Iwlan.EPDG_ADDRESS_STATIC});
+        mTestBundle.putString(
+                CarrierConfigManager.Iwlan.KEY_EPDG_STATIC_ADDRESS_STRING, testStaticAddress);
+
+        ArrayList<InetAddress> testInetAddresses =
+                getValidatedServerListWithIpPreference(
+                        EpdgSelector.PROTO_FILTER_IPV6,
+                        EpdgSelector.SYSTEM_PREFERRED,
+                        false /*isEmergency*/);
+
+        assertEquals(1, testInetAddresses.size());
+        assertEquals(InetAddress.getByName(TEST_IPV6_ADDRESS), testInetAddresses.get(0));
+    }
+
+    @Test
+    public void testGetValidatedServerListSystemPreferred() throws Exception {
+        when(DnsResolver.getInstance()).thenReturn(mMockDnsResolver);
+        doReturn(true).when(mEpdgSelector).hasIpv4Address(mMockNetwork);
+        doReturn(true).when(mEpdgSelector).hasIpv6Address(mMockNetwork);
+
+        final String addr1 = "epdg.epc.mnc120.mcc300.pub.3gppnetwork.org";
+        final String addr2 = "epdg.epc.mnc120.mcc311.pub.3gppnetwork.org";
+        final String addr3 = "epdg.epc.mnc120.mcc312.pub.3gppnetwork.org";
+        final String testStaticAddress = addr1 + "," + addr2 + "," + addr3;
+
+        mFakeDns.setAnswer(addr1, new String[] {TEST_IP_ADDRESS_1}, TYPE_A);
+        mFakeDns.setAnswer(addr2, new String[] {TEST_IPV6_ADDRESS}, TYPE_AAAA);
+        mFakeDns.setAnswer(addr3, new String[] {TEST_IP_ADDRESS_2}, TYPE_A);
+
+        // Set carrier config mock
+        mTestBundle.putIntArray(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_PRIORITY_INT_ARRAY,
+                new int[] {CarrierConfigManager.Iwlan.EPDG_ADDRESS_STATIC});
+        mTestBundle.putString(
+                CarrierConfigManager.Iwlan.KEY_EPDG_STATIC_ADDRESS_STRING, testStaticAddress);
+
+        ArrayList<InetAddress> testInetAddresses =
+                getValidatedServerListWithIpPreference(
+                        EpdgSelector.PROTO_FILTER_IPV4V6,
+                        EpdgSelector.SYSTEM_PREFERRED,
+                        false /*isEmergency*/);
+
+        assertEquals(3, testInetAddresses.size());
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_1), testInetAddresses.get(0));
+        assertEquals(InetAddress.getByName(TEST_IPV6_ADDRESS), testInetAddresses.get(1));
+        assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_2), testInetAddresses.get(2));
+    }
+
     /**
      * Fakes DNS responses.
      *
@@ -492,24 +953,24 @@
             // Full match or partial match that target host contains the entry hostname to support
             // random private dns probe hostname.
             private boolean matches(String hostname, int type) {
-                return hostname.endsWith(mHostname) && type == mType;
+                return hostname.equals(mHostname) && type == mType;
             }
         }
 
-        private final ArrayList<DnsEntry> mAnswers = new ArrayList<DnsEntry>();
+        private final List<DnsEntry> mAnswers = new ArrayList<>();
 
         /** Clears all DNS entries. */
         private synchronized void clearAll() {
             mAnswers.clear();
         }
 
-        /** Returns the answer for a given name and type on the given mock network. */
-        private synchronized List<InetAddress> getAnswer(Object mock, String hostname, int type) {
+        /** Returns the answer for a given name and type. */
+        private synchronized List<InetAddress> getAnswer(String hostname, int type) {
             return mAnswers.stream()
                     .filter(e -> e.matches(hostname, type))
                     .map(answer -> answer.mAddresses)
                     .findFirst()
-                    .orElse(null);
+                    .orElse(List.of());
         }
 
         /** Sets the answer for a given name and type. */
@@ -530,10 +991,20 @@
         }
 
         // Regardless of the type, depends on what the responses contained in the network.
-        private List<InetAddress> queryAllTypes(Object mock, String hostname) {
+        private List<InetAddress> queryIpv4(String hostname) {
+            return getAnswer(hostname, TYPE_A);
+        }
+
+        // Regardless of the type, depends on what the responses contained in the network.
+        private List<InetAddress> queryIpv6(String hostname) {
+            return getAnswer(hostname, TYPE_AAAA);
+        }
+
+        // Regardless of the type, depends on what the responses contained in the network.
+        private List<InetAddress> queryAllTypes(String hostname) {
             List<InetAddress> answer = new ArrayList<>();
-            addAllIfNotNull(answer, getAnswer(mock, hostname, TYPE_A));
-            addAllIfNotNull(answer, getAnswer(mock, hostname, TYPE_AAAA));
+            answer.addAll(queryIpv4(hostname));
+            answer.addAll(queryIpv6(hostname));
             return answer;
         }
 
@@ -545,32 +1016,55 @@
 
         /** Starts mocking DNS queries. */
         private void startMocking() throws UnknownHostException {
+            // 5-arg DnsResolver.query()
             doAnswer(
                             invocation -> {
                                 return mockQuery(
                                         invocation,
                                         1 /* posHostname */,
+                                        -1 /* posType */,
                                         3 /* posExecutor */,
-                                        5 /* posCallback */,
-                                        -1 /* posType */);
+                                        5 /* posCallback */);
                             })
                     .when(mMockDnsResolver)
-                    .query(any(), any(), anyInt(), any(), any(), any());
+                    .query(any(), anyString(), anyInt(), any(), any(), any());
+
+            // 6-arg DnsResolver.query() with explicit query type (IPv4 or v6).
+            doAnswer(
+                            invocation -> {
+                                return mockQuery(
+                                        invocation,
+                                        1 /* posHostname */,
+                                        2 /* posType */,
+                                        4 /* posExecutor */,
+                                        6 /* posCallback */);
+                            })
+                    .when(mMockDnsResolver)
+                    .query(any(), anyString(), anyInt(), anyInt(), any(), any(), any());
         }
 
         // Mocking queries on DnsResolver#query.
         private Answer mockQuery(
                 InvocationOnMock invocation,
                 int posHostname,
+                int posType,
                 int posExecutor,
-                int posCallback,
-                int posType) {
-            String hostname = (String) invocation.getArgument(posHostname);
-            Executor executor = (Executor) invocation.getArgument(posExecutor);
+                int posCallback) {
+            String hostname = invocation.getArgument(posHostname);
+            Executor executor = invocation.getArgument(posExecutor);
             DnsResolver.Callback<List<InetAddress>> callback = invocation.getArgument(posCallback);
             List<InetAddress> answer;
 
-            answer = queryAllTypes(invocation.getMock(), hostname);
+            switch (posType) {
+                case TYPE_A:
+                    answer = queryIpv4(hostname);
+                    break;
+                case TYPE_AAAA:
+                    answer = queryIpv6(hostname);
+                    break;
+                default:
+                    answer = queryAllTypes(hostname);
+            }
 
             if (answer != null && answer.size() > 0) {
                 new Handler(Looper.getMainLooper())
diff --git a/test/com/google/android/iwlan/epdg/EpdgTunnelManagerTest.java b/test/com/google/android/iwlan/epdg/EpdgTunnelManagerTest.java
index f2cba73..76ceec8 100644
--- a/test/com/google/android/iwlan/epdg/EpdgTunnelManagerTest.java
+++ b/test/com/google/android/iwlan/epdg/EpdgTunnelManagerTest.java
@@ -19,11 +19,23 @@
 import static org.junit.Assert.assertArrayEquals;
 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.Mockito.*;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
 import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.eq;
+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.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -47,6 +59,7 @@
 import android.net.ipsec.ike.SaProposal;
 import android.net.ipsec.ike.TunnelModeChildSessionParams;
 import android.net.ipsec.ike.exceptions.IkeException;
+import android.net.ipsec.ike.exceptions.IkeIOException;
 import android.net.ipsec.ike.exceptions.IkeInternalException;
 import android.net.ipsec.ike.exceptions.IkeProtocolException;
 import android.net.ipsec.ike.ike3gpp.Ike3gppBackoffTimer;
@@ -61,8 +74,12 @@
 import android.telephony.data.ApnSetting;
 
 import com.google.android.iwlan.IwlanError;
+import com.google.android.iwlan.IwlanTunnelMetricsImpl;
+import com.google.android.iwlan.TunnelMetricsInterface.OnClosedMetrics;
+import com.google.android.iwlan.TunnelMetricsInterface.OnOpenedMetrics;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -78,7 +95,6 @@
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.Executor;
 
@@ -86,41 +102,30 @@
 public class EpdgTunnelManagerTest {
     public static final int DEFAULT_SLOT_INDEX = 0;
     public static final int DEFAULT_SUBID = 0;
+    public static final int DEFAULT_TOKEN = 0;
 
     private static final String EPDG_ADDRESS = "127.0.0.1";
+    private static final String EPDG_ADDRESS_IPV6 = "2600:387:f:707::1";
     private static final String TEST_APN_NAME = "www.xyz.com";
 
-    private static final ArrayList<InetAddress> EXPECTED_LOCAL_ADDRESSES =
-            new ArrayList<>(
-                    Arrays.asList(
-                            new InetAddress[] {InetAddresses.parseNumericAddress("201.1.100.10")}));
-    private static final ArrayList<InetAddress> EXPECTED_EPDG_ADDRESSES =
-            new ArrayList<>(
-                    Arrays.asList(
-                            new InetAddress[] {InetAddresses.parseNumericAddress(EPDG_ADDRESS)}));
-    private static final ArrayList<LinkAddress> EXPECTED_INTERNAL_ADDRESSES =
-            new ArrayList<>(
-                    Arrays.asList(
-                            new LinkAddress[] {
-                                new LinkAddress(
-                                        InetAddresses.parseNumericAddress("198.50.100.10"), 24)
-                            }));
-    private static final ArrayList<InetAddress> EXPECTED_PCSCF_ADDRESSES =
-            new ArrayList<>(
-                    Arrays.asList(
-                            new InetAddress[] {
-                                InetAddresses.parseNumericAddress("198.51.100.10")
-                            }));
-    private static final ArrayList<InetAddress> EXPECTED_DNS_ADDRESSES =
-            new ArrayList<>(
-                    Arrays.asList(
-                            new InetAddress[] {
-                                InetAddresses.parseNumericAddress("198.50.100.10")
-                            }));
+    private static final List<InetAddress> EXPECTED_LOCAL_ADDRESSES =
+            List.of(InetAddresses.parseNumericAddress("201.1.100.10"));
+    private static final List<InetAddress> EXPECTED_IPV6_LOCAL_ADDRESSES =
+            List.of(InetAddresses.parseNumericAddress("2001:db8::1:2"));
+    private static final List<InetAddress> EXPECTED_EPDG_ADDRESSES =
+            List.of(InetAddresses.parseNumericAddress(EPDG_ADDRESS));
+    private static final List<InetAddress> EXPECTED_EPDG_ADDRESSES_IPV6 =
+            List.of(InetAddresses.parseNumericAddress(EPDG_ADDRESS_IPV6));
+    private static final List<LinkAddress> EXPECTED_INTERNAL_ADDRESSES =
+            List.of(new LinkAddress(InetAddresses.parseNumericAddress("198.50.100.10"), 24));
+    private static final List<InetAddress> EXPECTED_PCSCF_ADDRESSES =
+            List.of(InetAddresses.parseNumericAddress("198.51.100.10"));
+    private static final List<InetAddress> EXPECTED_DNS_ADDRESSES =
+            List.of(InetAddresses.parseNumericAddress("198.50.100.10"));
 
     private EpdgTunnelManager mEpdgTunnelManager;
 
-    private class IwlanTunnelCallback implements EpdgTunnelManager.TunnelCallback {
+    private static class IwlanTunnelCallback implements EpdgTunnelManager.TunnelCallback {
         public void onOpened(String apnName, TunnelLinkProperties linkProperties) {}
 
         public void onClosed(String apnName, IwlanError error) {}
@@ -130,10 +135,11 @@
     private TestLooper mTestLooper = new TestLooper();
 
     @Mock private Context mMockContext;
+    @Mock private Network mMockDefaultNetwork;
     @Mock private IwlanTunnelCallback mMockIwlanTunnelCallback;
+    @Mock private IwlanTunnelMetricsImpl mMockIwlanTunnelMetrics;
     @Mock private IkeSession mMockIkeSession;
     @Mock private EpdgSelector mMockEpdgSelector;
-    @Mock private Network mMockNetwork;
     @Mock CarrierConfigManager mMockCarrierConfigManager;
     @Mock ConnectivityManager mMockConnectivityManager;
     @Mock SubscriptionManager mMockSubscriptionManager;
@@ -142,6 +148,7 @@
     @Mock IpSecManager mMockIpSecManager;
     @Mock EpdgTunnelManager.IkeSessionCreator mMockIkeSessionCreator;
     @Mock IkeException mMockIkeException;
+    @Mock IkeIOException mMockIkeIoException;
     @Mock IkeSessionConfiguration mMockIkeSessionConfiguration;
     @Mock ChildSessionConfiguration mMockChildSessionConfiguration;
     @Mock IpSecManager.IpSecTunnelInterface mMockIpSecTunnelInterface;
@@ -150,10 +157,20 @@
     @Mock IpSecTransform mMockedIpSecTransformOut;
     @Mock LinkProperties mMockLinkProperties;
 
-    ArgumentCaptor<ChildSessionCallback> mChildSessionCallbackCaptor;
+    static class IkeSessionArgumentCaptors {
+        ArgumentCaptor<IkeSessionParams> mIkeSessionParamsCaptor =
+                ArgumentCaptor.forClass(IkeSessionParams.class);
+        ArgumentCaptor<ChildSessionParams> mChildSessionParamsCaptor =
+                ArgumentCaptor.forClass(ChildSessionParams.class);
+        ArgumentCaptor<IkeSessionCallback> mIkeSessionCallbackCaptor =
+                ArgumentCaptor.forClass(IkeSessionCallback.class);
+        ArgumentCaptor<ChildSessionCallback> mChildSessionCallbackCaptor =
+                ArgumentCaptor.forClass(ChildSessionCallback.class);
+    }
 
     @Before
     public void setUp() throws Exception {
+        EpdgTunnelManager.resetAllInstances();
         mEpdgTunnelManager = spy(EpdgTunnelManager.getInstance(mMockContext, DEFAULT_SLOT_INDEX));
 
         when(mMockContext.getSystemService(eq(IpSecManager.class))).thenReturn(mMockIpSecManager);
@@ -161,16 +178,16 @@
         doReturn(mTestLooper.getLooper()).when(mEpdgTunnelManager).getLooper();
         setVariable(mEpdgTunnelManager, "mContext", mMockContext);
         mEpdgTunnelManager.initHandler();
-        mEpdgTunnelManager.resetTunnelManagerState();
-        when(mEpdgTunnelManager.getEpdgSelector()).thenReturn(mMockEpdgSelector);
+        doReturn(mMockEpdgSelector).when(mEpdgTunnelManager).getEpdgSelector();
         when(mEpdgTunnelManager.getIkeSessionCreator()).thenReturn(mMockIkeSessionCreator);
 
         when(mMockEpdgSelector.getValidatedServerList(
                         anyInt(),
                         anyInt(),
+                        anyInt(),
                         anyBoolean(),
                         anyBoolean(),
-                        eq(mMockNetwork),
+                        any(Network.class),
                         any(EpdgSelector.EpdgSelectorCallback.class)))
                 .thenReturn(new IwlanError(IwlanError.NO_ERROR));
 
@@ -185,18 +202,20 @@
         when(mMockChildSessionConfiguration.getInternalAddresses())
                 .thenReturn(EXPECTED_INTERNAL_ADDRESSES);
 
-        when(mMockIpSecManager.createIpSecTunnelInterface(any(), any(), any()))
+        when(mMockIpSecManager.createIpSecTunnelInterface(
+                        any(InetAddress.class), any(InetAddress.class), any(Network.class)))
                 .thenReturn(mMockIpSecTunnelInterface);
+        when(mMockIpSecTunnelInterface.getInterfaceName()).thenReturn("ipsec10");
 
-        when(mMockIpSecTunnelInterface.getInterfaceName()).thenReturn("wlan0");
-
-        when(mMockIkeSessionConnectionInfo.getNetwork()).thenReturn(mMockNetwork);
-
-        mChildSessionCallbackCaptor = ArgumentCaptor.forClass(ChildSessionCallback.class);
+        when(mMockIkeSessionConnectionInfo.getNetwork()).thenReturn(mMockDefaultNetwork);
 
         doReturn(EXPECTED_LOCAL_ADDRESSES)
                 .when(mEpdgTunnelManager)
                 .getAddressForNetwork(any(), any());
+
+        when(mMockLinkProperties.isReachable(any())).thenReturn(true);
+        mEpdgTunnelManager.updateNetwork(mMockDefaultNetwork, mMockLinkProperties);
+        mTestLooper.dispatchAll();
     }
 
     @Test
@@ -204,7 +223,8 @@
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_PPP),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertFalse(ret);
     }
 
@@ -213,13 +233,15 @@
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IPV6, 16),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertFalse(ret);
 
         ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IPV6, -1),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertFalse(ret);
     }
 
@@ -242,13 +264,19 @@
         doReturn(true).when(mEpdgTunnelManager).canBringUpTunnel(eq(testApnName2));
         doReturn(true).when(mEpdgTunnelManager).canBringUpTunnel(eq(testApnName3));
 
-        boolean ret = mEpdgTunnelManager.bringUpTunnel(TSR_v4, mMockIwlanTunnelCallback);
+        boolean ret =
+                mEpdgTunnelManager.bringUpTunnel(
+                        TSR_v4, mMockIwlanTunnelCallback, mMockIwlanTunnelMetrics);
         assertTrue(ret);
 
-        ret = mEpdgTunnelManager.bringUpTunnel(TSR_v6, mMockIwlanTunnelCallback);
+        ret =
+                mEpdgTunnelManager.bringUpTunnel(
+                        TSR_v6, mMockIwlanTunnelCallback, mMockIwlanTunnelMetrics);
         assertTrue(ret);
 
-        ret = mEpdgTunnelManager.bringUpTunnel(TSR_v4v6, mMockIwlanTunnelCallback);
+        ret =
+                mEpdgTunnelManager.bringUpTunnel(
+                        TSR_v4v6, mMockIwlanTunnelCallback, mMockIwlanTunnelMetrics);
         assertTrue(ret);
     }
 
@@ -259,7 +287,9 @@
 
         when(mEpdgTunnelManager.getTunnelSetupRequestApnName(TSR)).thenReturn(null);
 
-        boolean ret = mEpdgTunnelManager.bringUpTunnel(TSR, mMockIwlanTunnelCallback);
+        boolean ret =
+                mEpdgTunnelManager.bringUpTunnel(
+                        TSR, mMockIwlanTunnelCallback, mMockIwlanTunnelMetrics);
         assertFalse(ret);
         verify(mEpdgTunnelManager).getTunnelSetupRequestApnName(TSR);
     }
@@ -270,7 +300,9 @@
 
         when(mEpdgTunnelManager.isTunnelConfigContainExistApn(TEST_APN_NAME)).thenReturn(true);
 
-        boolean ret = mEpdgTunnelManager.bringUpTunnel(TSR, mMockIwlanTunnelCallback);
+        boolean ret =
+                mEpdgTunnelManager.bringUpTunnel(
+                        TSR, mMockIwlanTunnelCallback, mMockIwlanTunnelMetrics);
         assertFalse(ret);
         verify(mEpdgTunnelManager).isTunnelConfigContainExistApn(TEST_APN_NAME);
     }
@@ -282,10 +314,17 @@
         TunnelSetupRequest TSR = getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP);
 
         mEpdgTunnelManager.putApnNameToTunnelConfig(
-                testApnName2, mMockIkeSession, mMockIwlanTunnelCallback, null, 0);
+                testApnName2,
+                mMockIkeSession,
+                mMockIwlanTunnelCallback,
+                mMockIwlanTunnelMetrics,
+                null,
+                0);
         doReturn(true).when(mEpdgTunnelManager).canBringUpTunnel(eq(TEST_APN_NAME));
 
-        boolean ret = mEpdgTunnelManager.bringUpTunnel(TSR, mMockIwlanTunnelCallback);
+        boolean ret =
+                mEpdgTunnelManager.bringUpTunnel(
+                        TSR, mMockIwlanTunnelCallback, mMockIwlanTunnelMetrics);
         assertTrue(ret);
     }
 
@@ -298,21 +337,25 @@
         PersistableBundle bundle = new PersistableBundle();
         setupMockForGetConfig(bundle);
 
-        boolean ret = mEpdgTunnelManager.bringUpTunnel(TSR, mMockIwlanTunnelCallback);
+        boolean ret =
+                mEpdgTunnelManager.bringUpTunnel(
+                        TSR, mMockIwlanTunnelCallback, mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
 
         verify(mMockEpdgSelector)
                 .getValidatedServerList(
                         anyInt(),
-                        eq(EpdgSelector.PROTO_FILTER_IPV4),
+                        eq(EpdgSelector.PROTO_FILTER_IPV4V6),
+                        anyInt(),
                         eq(false),
                         eq(false),
-                        eq(mMockNetwork),
+                        eq(mMockDefaultNetwork),
                         any());
     }
 
-    private void setupTunnelBringup() throws Exception {
+    private void setupTunnelBringup(
+            String apnName, List<InetAddress> epdgAddresses, int transactionId) throws Exception {
         setupMockForGetConfig(null);
         doReturn(null)
                 .when(mMockIkeSessionCreator)
@@ -324,21 +367,27 @@
                         any(IkeSessionCallback.class),
                         any(ChildSessionCallback.class));
 
-        doReturn(true).when(mEpdgTunnelManager).canBringUpTunnel(eq(TEST_APN_NAME));
+        doReturn(true).when(mEpdgTunnelManager).canBringUpTunnel(eq(apnName));
 
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
-                        getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
-                        mMockIwlanTunnelCallback);
+                        getBasicTunnelSetupRequest(apnName, ApnSetting.PROTOCOL_IP),
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
 
         mEpdgTunnelManager.sendSelectionRequestComplete(
-                EXPECTED_EPDG_ADDRESSES, new IwlanError(IwlanError.NO_ERROR), 1);
+                epdgAddresses, new IwlanError(IwlanError.NO_ERROR), transactionId);
         mTestLooper.dispatchAll();
     }
 
+    private void setupTunnelBringup() throws Exception {
+        setupTunnelBringup(TEST_APN_NAME, EXPECTED_EPDG_ADDRESSES, 1 /* transactionId */);
+    }
+
     @Test
+    @Ignore("b/239753287- Telus carrier errors out on parsing DEVICE_IDENTITY response")
     public void testBringUpTunnelSetsDeviceIdentityImeiSv() throws Exception {
         when(mMockContext.getSystemService(eq(TelephonyManager.class)))
                 .thenReturn(mMockTelephonyManager);
@@ -370,6 +419,7 @@
     }
 
     @Test
+    @Ignore("b/239753287- Telus carrier errors out on parsing DEVICE_IDENTITY response")
     public void testBringUpTunnelSetsDeviceIdentityImei() throws Exception {
         when(mMockContext.getSystemService(eq(TelephonyManager.class)))
                 .thenReturn(mMockTelephonyManager);
@@ -399,6 +449,7 @@
     }
 
     @Test
+    @Ignore("b/239753287- Telus carrier errors out on parsing DEVICE_IDENTITY response")
     public void testBringUpTunnelNoDeviceIdentityWhenImeiUnavailable() throws Exception {
         when(mMockContext.getSystemService(eq(TelephonyManager.class)))
                 .thenReturn(mMockTelephonyManager);
@@ -426,29 +477,7 @@
 
     @Test
     public void testBringUpTunnelWithMobilityOptions() throws Exception {
-        doReturn(null)
-                .when(mMockIkeSessionCreator)
-                .createIkeSession(
-                        eq(mMockContext),
-                        any(IkeSessionParams.class),
-                        any(ChildSessionParams.class),
-                        any(Executor.class),
-                        any(IkeSessionCallback.class),
-                        any(ChildSessionCallback.class));
-
-        doReturn(true).when(mEpdgTunnelManager).canBringUpTunnel(eq(TEST_APN_NAME));
-
-        boolean ret =
-                mEpdgTunnelManager.bringUpTunnel(
-                        getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
-                        mMockIwlanTunnelCallback);
-        assertTrue(ret);
-        mTestLooper.dispatchAll();
-
-        mEpdgTunnelManager.sendSelectionRequestComplete(
-                EXPECTED_EPDG_ADDRESSES, new IwlanError(IwlanError.NO_ERROR), 1);
-        mTestLooper.dispatchAll();
-
+        setupTunnelBringup();
         ArgumentCaptor<IkeSessionParams> ikeSessionParamsCaptor =
                 ArgumentCaptor.forClass(IkeSessionParams.class);
         verify(mMockIkeSessionCreator, atLeastOnce())
@@ -465,13 +494,70 @@
     }
 
     @Test
+    public void testBringUpTunnelIpv6_verifyMobikeDisabled() throws Exception {
+        setupTunnelBringup(TEST_APN_NAME, EXPECTED_EPDG_ADDRESSES_IPV6, 1);
+        ArgumentCaptor<IkeSessionParams> ikeSessionParamsCaptor =
+                ArgumentCaptor.forClass(IkeSessionParams.class);
+        verify(mMockIkeSessionCreator, atLeastOnce())
+                .createIkeSession(
+                        eq(mMockContext),
+                        ikeSessionParamsCaptor.capture(),
+                        any(ChildSessionParams.class),
+                        any(Executor.class),
+                        any(IkeSessionCallback.class),
+                        any(ChildSessionCallback.class));
+        IkeSessionParams ikeSessionParams = ikeSessionParamsCaptor.getValue();
+        assertTrue(ikeSessionParams.hasIkeOption(IkeSessionParams.IKE_OPTION_REKEY_MOBILITY));
+        assertFalse(ikeSessionParams.hasIkeOption(IkeSessionParams.IKE_OPTION_MOBIKE));
+    }
+
+    @Test
+    public void testInitialContactForFirstTunnelOnly() throws Exception {
+        final String firstApnName = "ims";
+        final String secondApnName = "mms";
+
+        IkeSessionArgumentCaptors firstTunnelArgumentCaptors =
+                verifyBringUpTunnelWithDnsQuery(firstApnName);
+        ChildSessionCallback firstCallback =
+                firstTunnelArgumentCaptors.mChildSessionCallbackCaptor.getValue();
+
+        IkeSessionArgumentCaptors secondTunnelArgumentCaptors =
+                verifyBringUpTunnel(secondApnName, true /* needPendingBringUpReq */);
+        verifyTunnelOnOpened(firstApnName, firstCallback);
+
+        ChildSessionCallback secondCallback =
+                secondTunnelArgumentCaptors.mChildSessionCallbackCaptor.getValue();
+        verifyTunnelOnOpened(secondApnName, secondCallback);
+
+        IkeSessionParams firstTunnelParams =
+                firstTunnelArgumentCaptors.mIkeSessionParamsCaptor.getValue();
+        IkeSessionParams secondTunnelParams =
+                secondTunnelArgumentCaptors.mIkeSessionParamsCaptor.getValue();
+        assertTrue(firstTunnelParams.hasIkeOption(IkeSessionParams.IKE_OPTION_INITIAL_CONTACT));
+        assertFalse(secondTunnelParams.hasIkeOption(IkeSessionParams.IKE_OPTION_INITIAL_CONTACT));
+    }
+
+    @Test
     public void testCloseTunnelWithNoTunnelForApn() throws Exception {
         String testApnName = "www.xyz.com";
+        doReturn(0L)
+                .when(mEpdgTunnelManager)
+                .reportIwlanError(eq(testApnName), eq(new IwlanError(IwlanError.TUNNEL_NOT_FOUND)));
 
-        boolean ret = mEpdgTunnelManager.closeTunnel(testApnName, false /*forceClose*/);
-        assertTrue(ret);
+        mEpdgTunnelManager.closeTunnel(
+                testApnName,
+                false /*forceClose*/,
+                mMockIwlanTunnelCallback,
+                mMockIwlanTunnelMetrics);
         mTestLooper.dispatchAll();
+
         verify(mEpdgTunnelManager).closePendingRequestsForApn(eq(testApnName));
+        verify(mMockIwlanTunnelCallback)
+                .onClosed(eq(testApnName), eq(new IwlanError(IwlanError.TUNNEL_NOT_FOUND)));
+        ArgumentCaptor<OnClosedMetrics> metricsCaptor =
+                ArgumentCaptor.forClass(OnClosedMetrics.class);
+        verify(mMockIwlanTunnelMetrics, times(1)).onClosed(metricsCaptor.capture());
+        assertEquals(testApnName, metricsCaptor.getValue().getApnName());
     }
 
     @Test
@@ -479,10 +565,18 @@
         String testApnName = "www.xyz.com";
 
         mEpdgTunnelManager.putApnNameToTunnelConfig(
-                testApnName, mMockIkeSession, mMockIwlanTunnelCallback, null, 0);
+                testApnName,
+                mMockIkeSession,
+                mMockIwlanTunnelCallback,
+                mMockIwlanTunnelMetrics,
+                null,
+                0);
 
-        boolean ret = mEpdgTunnelManager.closeTunnel(testApnName, true /*forceClose*/);
-        assertTrue(ret);
+        mEpdgTunnelManager.closeTunnel(
+                testApnName,
+                true /*forceClose*/,
+                mMockIwlanTunnelCallback,
+                mMockIwlanTunnelMetrics);
         mTestLooper.dispatchAll();
 
         verify(mMockIkeSession).kill();
@@ -494,10 +588,18 @@
         String testApnName = "www.xyz.com";
 
         mEpdgTunnelManager.putApnNameToTunnelConfig(
-                testApnName, mMockIkeSession, mMockIwlanTunnelCallback, null, 0);
+                testApnName,
+                mMockIkeSession,
+                mMockIwlanTunnelCallback,
+                mMockIwlanTunnelMetrics,
+                null,
+                0);
 
-        boolean ret = mEpdgTunnelManager.closeTunnel(testApnName, false /*forceClose*/);
-        assertTrue(ret);
+        mEpdgTunnelManager.closeTunnel(
+                testApnName,
+                false /*forceClose*/,
+                mMockIwlanTunnelCallback,
+                mMockIwlanTunnelMetrics);
         mTestLooper.dispatchAll();
 
         verify(mMockIkeSession).close();
@@ -526,15 +628,6 @@
 
         setupMockForGetConfig(bundle);
 
-        when(mMockEpdgSelector.getValidatedServerList(
-                        anyInt(),
-                        anyInt(),
-                        eq(false),
-                        eq(false),
-                        eq(mMockNetwork),
-                        any(EpdgSelector.EpdgSelectorCallback.class)))
-                .thenReturn(new IwlanError(IwlanError.NO_ERROR));
-
         doReturn(null)
                 .when(mMockIkeSessionCreator)
                 .createIkeSession(
@@ -549,7 +642,8 @@
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
 
@@ -592,15 +686,6 @@
 
         setupMockForGetConfig(bundle);
 
-        when(mMockEpdgSelector.getValidatedServerList(
-                        anyInt(),
-                        anyInt(),
-                        eq(false),
-                        eq(false),
-                        eq(mMockNetwork),
-                        any(EpdgSelector.EpdgSelectorCallback.class)))
-                .thenReturn(new IwlanError(IwlanError.NO_ERROR));
-
         doReturn(null)
                 .when(mMockIkeSessionCreator)
                 .createIkeSession(
@@ -615,7 +700,8 @@
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
 
@@ -649,14 +735,6 @@
         bundle.putInt(CarrierConfigManager.Iwlan.KEY_DPD_TIMER_SEC_INT, testDpdDelay);
 
         setupMockForGetConfig(bundle);
-        when(mMockEpdgSelector.getValidatedServerList(
-                        anyInt(),
-                        anyInt(),
-                        eq(false),
-                        eq(false),
-                        eq(mMockNetwork),
-                        any(EpdgSelector.EpdgSelectorCallback.class)))
-                .thenReturn(new IwlanError(IwlanError.NO_ERROR));
 
         doReturn(null)
                 .when(mMockIkeSessionCreator)
@@ -672,7 +750,8 @@
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
 
@@ -707,14 +786,6 @@
 
         doReturn(0L).when(mEpdgTunnelManager).reportIwlanError(eq(testApnName), eq(error));
         setupMockForGetConfig(null);
-        when(mMockEpdgSelector.getValidatedServerList(
-                        anyInt(),
-                        anyInt(),
-                        eq(false),
-                        eq(false),
-                        eq(mMockNetwork),
-                        any(EpdgSelector.EpdgSelectorCallback.class)))
-                .thenReturn(new IwlanError(IwlanError.NO_ERROR));
 
         doReturn(null)
                 .doReturn(null)
@@ -731,7 +802,8 @@
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
 
@@ -743,7 +815,7 @@
 
         EpdgTunnelManager.TmIkeSessionCallback ikeSessionCallback =
                 verifyCreateIkeSession(ipList2.get(0));
-        ikeSessionCallback.onClosedExceptionally(
+        ikeSessionCallback.onClosedWithException(
                 new IkeInternalException(new IOException("Retransmitting failure")));
         mTestLooper.dispatchAll();
 
@@ -764,14 +836,6 @@
 
         doReturn(0L).when(mEpdgTunnelManager).reportIwlanError(eq(testApnName), eq(error));
         setupMockForGetConfig(null);
-        when(mMockEpdgSelector.getValidatedServerList(
-                        anyInt(),
-                        anyInt(),
-                        eq(false),
-                        eq(false),
-                        eq(mMockNetwork),
-                        any(EpdgSelector.EpdgSelectorCallback.class)))
-                .thenReturn(new IwlanError(IwlanError.NO_ERROR));
 
         doReturn(null)
                 .doReturn(null)
@@ -788,7 +852,8 @@
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
 
@@ -801,7 +866,7 @@
 
         EpdgTunnelManager.TmIkeSessionCallback ikeSessionCallback =
                 verifyCreateIkeSession(ipList2.get(1));
-        ikeSessionCallback.onClosedExceptionally(
+        ikeSessionCallback.onClosedWithException(
                 new IkeInternalException(new IOException("Retransmitting failure")));
         mTestLooper.dispatchAll();
 
@@ -834,7 +899,7 @@
         LinkAddress l1 = new LinkAddress(a1, 64);
         InetAddress src = InetAddress.getByName("2600:381:4872:5d1e:0:10:3582:a501");
         EpdgTunnelManager.TunnelConfig tf =
-                mEpdgTunnelManager.new TunnelConfig(null, null, src, 64);
+                mEpdgTunnelManager.new TunnelConfig(null, null, null, src, 64);
         assertTrue(tf.isPrefixSameAsSrcIP(l1));
 
         // different prefix length
@@ -888,14 +953,7 @@
         }
 
         setupMockForGetConfig(null);
-        when(mMockEpdgSelector.getValidatedServerList(
-                        anyInt(),
-                        anyInt(),
-                        eq(false),
-                        eq(false),
-                        eq(mMockNetwork),
-                        any(EpdgSelector.EpdgSelectorCallback.class)))
-                .thenReturn(new IwlanError(IwlanError.NO_ERROR));
+
         doReturn(null)
                 .doReturn(null)
                 .when(mMockIkeSessionCreator)
@@ -911,7 +969,8 @@
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
 
@@ -941,7 +1000,7 @@
         ike3gppCallback.onIke3gppDataReceived(ike3gppInfoList);
         EpdgTunnelManager.TmIkeSessionCallback ikeSessionCallback =
                 ikeSessionCallbackCaptor.getValue();
-        ikeSessionCallback.onClosedExceptionally(new IkeInternalException(new Exception()));
+        ikeSessionCallback.onClosedWithException(new IkeInternalException(new Exception()));
         mTestLooper.dispatchAll();
 
         // if expected backoff time is negative - verify that backoff time is not reported.
@@ -964,7 +1023,6 @@
         TunnelSetupRequest ret =
                 TunnelSetupRequest.builder()
                         .setApnName(apnName)
-                        .setNetwork(mMockNetwork)
                         .setIsRoaming(false /*isRoaming*/)
                         .setIsEmergency(false /*IsEmergency*/)
                         .setRequestPcscf(false /*requestPcscf*/)
@@ -977,7 +1035,6 @@
     private TunnelSetupRequest getHandoverTunnelSetupRequest(String apnName, int apnIpProtocol) {
         TunnelSetupRequest.Builder bld = TunnelSetupRequest.builder();
         bld.setApnName(apnName)
-                .setNetwork(mMockNetwork)
                 .setIsRoaming(false /*isRoaming*/)
                 .setIsEmergency(false /*IsEmergency*/)
                 .setRequestPcscf(false /*requestPcscf*/)
@@ -1014,6 +1071,12 @@
                     SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_384_192,
                     SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_512_256,
                 });
+        if (!bundle.containsKey(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT)) {
+            bundle.putInt(
+                    CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT,
+                    CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV4_PREFERRED);
+        }
         when(mMockContext.getSystemService(eq(CarrierConfigManager.class)))
                 .thenReturn(mMockCarrierConfigManager);
         when(mMockContext.getSystemService(eq(ConnectivityManager.class)))
@@ -1036,28 +1099,36 @@
     }
 
     @Test
-    public void testHandleOnClosedWithEpdgAddressSelected_True() throws Exception {
+    public void testHandleOnClosedWithEpdgConnected_True() throws Exception {
         String testApnName = "www.xyz.com";
-        IwlanError error = new IwlanError(IwlanError.NETWORK_FAILURE);
+        IwlanError error =
+                new IwlanError(IwlanError.IKE_SESSION_CLOSED_BEFORE_CHILD_SESSION_OPENED);
 
         doReturn(0L).when(mEpdgTunnelManager).reportIwlanError(eq(testApnName), eq(error));
         mEpdgTunnelManager.putApnNameToTunnelConfig(
-                testApnName, mMockIkeSession, mMockIwlanTunnelCallback, null, 0);
+                testApnName,
+                mMockIkeSession,
+                mMockIwlanTunnelCallback,
+                mMockIwlanTunnelMetrics,
+                null,
+                0);
+        int token = mEpdgTunnelManager.incrementAndGetCurrentTokenForApn(testApnName);
 
-        mEpdgTunnelManager.setIsEpdgAddressSelected(true);
+        mEpdgTunnelManager.onConnectedToEpdg(true);
+        mEpdgTunnelManager.setEpdgAddress(InetAddresses.parseNumericAddress(EPDG_ADDRESS));
 
-        mEpdgTunnelManager.getTmIkeSessionCallback(testApnName).onClosed();
+        mEpdgTunnelManager.getTmIkeSessionCallback(testApnName, token).onClosed();
         mTestLooper.dispatchAll();
 
         verify(mMockIwlanTunnelCallback, times(1)).onClosed(eq(testApnName), eq(error));
-        verify(mEpdgTunnelManager, times(2)).resetTunnelManagerState();
         verify(mEpdgTunnelManager, times(1)).reportIwlanError(eq(testApnName), eq(error));
     }
 
     @Test
-    public void testHandleOnClosedWithEpdgAddressSelected_False() throws Exception {
+    public void testHandleOnClosedWithEpdgConnected_False() throws Exception {
         String testApnName = "www.xyz.com";
-        IwlanError error = new IwlanError(IwlanError.NETWORK_FAILURE);
+        IwlanError error =
+                new IwlanError(IwlanError.IKE_SESSION_CLOSED_BEFORE_CHILD_SESSION_OPENED);
 
         doReturn(0L).when(mEpdgTunnelManager).reportIwlanError(eq(testApnName), eq(error));
 
@@ -1068,7 +1139,8 @@
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
 
@@ -1076,27 +1148,35 @@
                 EXPECTED_EPDG_ADDRESSES, new IwlanError(IwlanError.NO_ERROR), 1);
         mTestLooper.dispatchAll();
 
-        mEpdgTunnelManager.setIsEpdgAddressSelected(false);
+        mEpdgTunnelManager.onConnectedToEpdg(false);
 
-        mEpdgTunnelManager.getTmIkeSessionCallback(testApnName).onClosed();
+        mEpdgTunnelManager.getTmIkeSessionCallback(testApnName, DEFAULT_TOKEN).onClosed();
         mTestLooper.dispatchAll();
 
         verify(mMockIwlanTunnelCallback, times(1)).onClosed(eq(testApnName), eq(error));
-        verify(mEpdgTunnelManager, times(2)).resetTunnelManagerState();
         verify(mEpdgTunnelManager, times(1)).reportIwlanError(eq(testApnName), eq(error));
     }
 
     private void setOneTunnelOpened(String apnName) throws Exception {
         mEpdgTunnelManager.putApnNameToTunnelConfig(
-                apnName, mMockIkeSession, mMockIwlanTunnelCallback, null, 0);
-        setVariable(mEpdgTunnelManager, "mLocalAddresses", EXPECTED_LOCAL_ADDRESSES);
+                apnName,
+                mMockIkeSession,
+                mMockIwlanTunnelCallback,
+                mMockIwlanTunnelMetrics,
+                null,
+                0);
         mEpdgTunnelManager.validateAndSetEpdgAddress(EXPECTED_EPDG_ADDRESSES);
-        mEpdgTunnelManager.setIsEpdgAddressSelected(true);
+        mEpdgTunnelManager.onConnectedToEpdg(true);
     }
 
-    private ChildSessionCallback verifyBringUpTunnelWithDnsQuery(
+    private IkeSessionArgumentCaptors verifyBringUpTunnelWithDnsQuery(String apnName) {
+        return verifyBringUpTunnelWithDnsQuery(apnName, null);
+    }
+
+    private IkeSessionArgumentCaptors verifyBringUpTunnelWithDnsQuery(
             String apnName, IkeSession ikeSession) {
         reset(mMockIwlanTunnelCallback);
+        IkeSessionArgumentCaptors ikeSessionArgumentCaptors = new IkeSessionArgumentCaptors();
 
         verifyBringUpTunnel(apnName, true /* needPendingBringUpReq */);
 
@@ -1104,11 +1184,11 @@
                 .when(mMockIkeSessionCreator)
                 .createIkeSession(
                         eq(mMockContext),
-                        any(IkeSessionParams.class),
-                        any(ChildSessionParams.class),
+                        ikeSessionArgumentCaptors.mIkeSessionParamsCaptor.capture(),
+                        ikeSessionArgumentCaptors.mChildSessionParamsCaptor.capture(),
                         any(Executor.class),
-                        any(IkeSessionCallback.class),
-                        mChildSessionCallbackCaptor.capture());
+                        ikeSessionArgumentCaptors.mIkeSessionCallbackCaptor.capture(),
+                        ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.capture());
 
         mEpdgTunnelManager.sendSelectionRequestComplete(
                 EXPECTED_EPDG_ADDRESSES, new IwlanError(IwlanError.NO_ERROR), 1);
@@ -1117,56 +1197,62 @@
         verify(mMockIkeSessionCreator, times(1))
                 .createIkeSession(
                         eq(mMockContext),
-                        any(IkeSessionParams.class),
-                        any(ChildSessionParams.class),
+                        ikeSessionArgumentCaptors.mIkeSessionParamsCaptor.capture(),
+                        ikeSessionArgumentCaptors.mChildSessionParamsCaptor.capture(),
                         any(Executor.class),
-                        any(IkeSessionCallback.class),
-                        mChildSessionCallbackCaptor.capture());
+                        ikeSessionArgumentCaptors.mIkeSessionCallbackCaptor.capture(),
+                        ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.capture());
 
-        return mChildSessionCallbackCaptor.getValue();
+        return ikeSessionArgumentCaptors;
     }
 
-    private ChildSessionCallback verifyBringUpTunnel(
+    private IkeSessionArgumentCaptors verifyBringUpTunnel(
             String apnName, boolean needPendingBringUpReq) {
         reset(mMockIkeSessionCreator);
+        IkeSessionArgumentCaptors ikeSessionArgumentCaptors = new IkeSessionArgumentCaptors();
 
         doReturn(null)
                 .when(mMockIkeSessionCreator)
                 .createIkeSession(
                         eq(mMockContext),
-                        any(IkeSessionParams.class),
-                        any(ChildSessionParams.class),
+                        ikeSessionArgumentCaptors.mIkeSessionParamsCaptor.capture(),
+                        ikeSessionArgumentCaptors.mChildSessionParamsCaptor.capture(),
                         any(Executor.class),
-                        any(IkeSessionCallback.class),
-                        mChildSessionCallbackCaptor.capture());
+                        ikeSessionArgumentCaptors.mIkeSessionCallbackCaptor.capture(),
+                        ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.capture());
 
         doReturn(true).when(mEpdgTunnelManager).canBringUpTunnel(eq(apnName));
 
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(apnName, ApnSetting.PROTOCOL_IP),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
 
         verify(mMockIkeSessionCreator, times(needPendingBringUpReq ? 0 : 1))
                 .createIkeSession(
                         eq(mMockContext),
-                        any(IkeSessionParams.class),
-                        any(ChildSessionParams.class),
+                        ikeSessionArgumentCaptors.mIkeSessionParamsCaptor.capture(),
+                        ikeSessionArgumentCaptors.mChildSessionParamsCaptor.capture(),
                         any(Executor.class),
-                        any(IkeSessionCallback.class),
-                        mChildSessionCallbackCaptor.capture());
+                        ikeSessionArgumentCaptors.mIkeSessionCallbackCaptor.capture(),
+                        ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.capture());
 
-        return needPendingBringUpReq ? null : mChildSessionCallbackCaptor.getValue();
+        return ikeSessionArgumentCaptors;
     }
 
-    private void verifyTunnelOnOpened(String apnName) {
-        verifyTunnelOnOpened(apnName, mChildSessionCallbackCaptor.getValue());
-    }
+    private void verifyTunnelOnOpened(String apnName, ChildSessionCallback childSessionCallback)
+            throws Exception {
+        clearInvocations(mMockIpSecManager);
+        doReturn(0L)
+                .when(mEpdgTunnelManager)
+                .reportIwlanError(eq(apnName), eq(new IwlanError(IwlanError.NO_ERROR)));
 
-    private void verifyTunnelOnOpened(String apnName, ChildSessionCallback childSessionCallback) {
-        mEpdgTunnelManager.getTmIkeSessionCallback(apnName).onOpened(mMockIkeSessionConfiguration);
+        mEpdgTunnelManager
+                .getTmIkeSessionCallback(apnName, mEpdgTunnelManager.getCurrentTokenForApn(apnName))
+                .onOpened(mMockIkeSessionConfiguration);
         mTestLooper.dispatchAll();
         childSessionCallback.onIpSecTransformCreated(
                 mMockedIpSecTransformIn, IpSecManager.DIRECTION_IN);
@@ -1174,21 +1260,35 @@
         childSessionCallback.onIpSecTransformCreated(
                 mMockedIpSecTransformOut, IpSecManager.DIRECTION_OUT);
         mTestLooper.dispatchAll();
+        verify(mMockIpSecManager, times(1))
+                .createIpSecTunnelInterface(
+                        any(InetAddress.class), any(InetAddress.class), eq(mMockDefaultNetwork));
+
         childSessionCallback.onOpened(mMockChildSessionConfiguration);
         mTestLooper.dispatchAll();
-
+        verify(mEpdgTunnelManager, times(1))
+                .reportIwlanError(eq(apnName), eq(new IwlanError(IwlanError.NO_ERROR)));
         verify(mMockIwlanTunnelCallback, times(1)).onOpened(eq(apnName), any());
     }
 
     @Test
-    public void testHandleOnOpenedWithEpdgAddressSelected_True() throws Exception {
+    public void testHandleOnOpenedWithEpdgConnected_True() throws Exception {
         final String openedApnName = "ims";
         final String toBeOpenedApnName = "mms";
 
         setOneTunnelOpened(openedApnName);
 
-        verifyBringUpTunnel(toBeOpenedApnName, false /* needPendingBringUpReq */);
-        verifyTunnelOnOpened(toBeOpenedApnName);
+        // FIXME: Since the network from bringUpTunnel() will only be stored for the first request,
+        // and we are skipping the first tunnel setup procedure in this test case, it is necessary
+        // to set the network instance directly.
+        mEpdgTunnelManager.updateNetwork(mMockDefaultNetwork, mMockLinkProperties);
+        mTestLooper.dispatchAll();
+
+        IkeSessionArgumentCaptors ikeSessionArgumentCaptors =
+                verifyBringUpTunnel(toBeOpenedApnName, false /* needPendingBringUpReq */);
+        ChildSessionCallback childSessionCallback =
+                ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.getValue();
+        verifyTunnelOnOpened(toBeOpenedApnName, childSessionCallback);
     }
 
     @Test
@@ -1196,36 +1296,50 @@
         final String firstApnName = "ims";
         final String secondApnName = "mms";
 
-        ChildSessionCallback firstCallback = verifyBringUpTunnelWithDnsQuery(firstApnName, null);
-        verifyBringUpTunnel(secondApnName, true /* needPendingBringUpReq */);
+        IkeSessionArgumentCaptors firstTunnelArgumentCaptors =
+                verifyBringUpTunnelWithDnsQuery(firstApnName);
+        ChildSessionCallback firstCallback =
+                firstTunnelArgumentCaptors.mChildSessionCallbackCaptor.getValue();
+
+        IkeSessionArgumentCaptors secondTunnelArgumentCaptors =
+                verifyBringUpTunnel(secondApnName, true /* needPendingBringUpReq */);
         verifyTunnelOnOpened(firstApnName, firstCallback);
-        verifyTunnelOnOpened(secondApnName, mChildSessionCallbackCaptor.getValue());
+
+        ChildSessionCallback secondCallback =
+                secondTunnelArgumentCaptors.mChildSessionCallbackCaptor.getValue();
+        verifyTunnelOnOpened(secondApnName, secondCallback);
     }
 
     @Test
-    public void testHandleOnClosedExceptionallyWithEpdgAddressSelected_True() throws Exception {
+    public void testHandleOnClosedExceptionallyWithEpdgConnected_True() throws Exception {
         String testApnName = "www.xyz.com";
         IwlanError error = new IwlanError(mMockIkeException);
 
         doReturn(0L).when(mEpdgTunnelManager).reportIwlanError(eq(testApnName), eq(error));
 
         mEpdgTunnelManager.putApnNameToTunnelConfig(
-                testApnName, mMockIkeSession, mMockIwlanTunnelCallback, null, 0);
+                testApnName,
+                mMockIkeSession,
+                mMockIwlanTunnelCallback,
+                mMockIwlanTunnelMetrics,
+                null,
+                0);
+        int token = mEpdgTunnelManager.incrementAndGetCurrentTokenForApn(testApnName);
 
-        mEpdgTunnelManager.setIsEpdgAddressSelected(true);
+        mEpdgTunnelManager.onConnectedToEpdg(true);
+        mEpdgTunnelManager.setEpdgAddress(InetAddresses.parseNumericAddress(EPDG_ADDRESS));
 
         mEpdgTunnelManager
-                .getTmIkeSessionCallback(testApnName)
-                .onClosedExceptionally(mMockIkeException);
+                .getTmIkeSessionCallback(testApnName, token)
+                .onClosedWithException(mMockIkeException);
         mTestLooper.dispatchAll();
 
         verify(mMockIwlanTunnelCallback, times(1)).onClosed(eq(testApnName), eq(error));
-        verify(mEpdgTunnelManager, times(2)).resetTunnelManagerState();
         verify(mEpdgTunnelManager, times(1)).reportIwlanError(eq(testApnName), eq(error));
     }
 
     @Test
-    public void testHandleOnClosedExceptionallyWithEpdgAddressSelected_False() throws Exception {
+    public void testHandleOnClosedExceptionallyWithEpdgConnected_False() throws Exception {
         String testApnName = "www.xyz.com";
         IwlanError error = new IwlanError(mMockIkeException);
 
@@ -1238,7 +1352,8 @@
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
 
@@ -1246,15 +1361,14 @@
                 EXPECTED_EPDG_ADDRESSES, new IwlanError(IwlanError.NO_ERROR), 1);
         mTestLooper.dispatchAll();
 
-        mEpdgTunnelManager.setIsEpdgAddressSelected(false);
+        mEpdgTunnelManager.onConnectedToEpdg(false);
 
         mEpdgTunnelManager
-                .getTmIkeSessionCallback(testApnName)
-                .onClosedExceptionally(mMockIkeException);
+                .getTmIkeSessionCallback(testApnName, DEFAULT_TOKEN)
+                .onClosedWithException(mMockIkeException);
         mTestLooper.dispatchAll();
 
         verify(mMockIwlanTunnelCallback, times(1)).onClosed(eq(testApnName), any(IwlanError.class));
-        verify(mEpdgTunnelManager, times(2)).resetTunnelManagerState();
         verify(mEpdgTunnelManager, times(1)).reportIwlanError(eq(testApnName), eq(error));
     }
 
@@ -1265,12 +1379,18 @@
 
         doReturn(0L).when(mEpdgTunnelManager).reportIwlanError(eq(testApnName), eq(error));
         mEpdgTunnelManager.putApnNameToTunnelConfig(
-                testApnName, mMockIkeSession, mMockIwlanTunnelCallback, null, 0);
+                testApnName,
+                mMockIkeSession,
+                mMockIwlanTunnelCallback,
+                mMockIwlanTunnelMetrics,
+                null,
+                0);
+        int token = mEpdgTunnelManager.incrementAndGetCurrentTokenForApn(testApnName);
 
         when(mMockIkeSessionConfiguration.getPcscfServers()).thenReturn(EXPECTED_EPDG_ADDRESSES);
 
         mEpdgTunnelManager
-                .getTmIkeSessionCallback(testApnName)
+                .getTmIkeSessionCallback(testApnName, token)
                 .onOpened(mMockIkeSessionConfiguration);
         mTestLooper.dispatchAll();
 
@@ -1286,8 +1406,10 @@
         doThrow(new IllegalArgumentException())
                 .when(mMockIpSecManager)
                 .applyTunnelModeTransform(eq(mMockIpSecTunnelInterface), anyInt(), any());
-        ChildSessionCallback childSessionCallback =
+        IkeSessionArgumentCaptors ikeSessionArgumentCaptors =
                 verifyBringUpTunnelWithDnsQuery(testApnName, mMockIkeSession);
+        ChildSessionCallback childSessionCallback =
+                ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.getValue();
         childSessionCallback.onIpSecTransformCreated(
                 mMockedIpSecTransformIn, IpSecManager.DIRECTION_IN);
         mTestLooper.dispatchAll();
@@ -1300,17 +1422,19 @@
         String testApnName = "ims";
         when(mMockConnectivityManager.getLinkProperties(any())).thenReturn(mMockLinkProperties);
 
-        ChildSessionCallback childSessionCallback =
+        IkeSessionArgumentCaptors ikeSessionArgumentCaptors =
                 verifyBringUpTunnelWithDnsQuery(testApnName, mMockIkeSession);
+        ChildSessionCallback childSessionCallback =
+                ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.getValue();
         childSessionCallback.onIpSecTransformCreated(
                 mMockedIpSecTransformIn, IpSecManager.DIRECTION_IN);
 
         mEpdgTunnelManager
-                .getTmIkeSessionCallback(testApnName)
+                .getTmIkeSessionCallback(testApnName, DEFAULT_TOKEN)
                 .onIkeSessionConnectionInfoChanged(mMockIkeSessionConnectionInfo);
         mTestLooper.dispatchAll();
 
-        verify(mMockIpSecTunnelInterface, times(1)).setUnderlyingNetwork(mMockNetwork);
+        verify(mMockIpSecTunnelInterface, times(1)).setUnderlyingNetwork(mMockDefaultNetwork);
     }
 
     @Test
@@ -1319,13 +1443,15 @@
         String testApnName = "ims";
         when(mMockConnectivityManager.getLinkProperties(any())).thenReturn(null);
 
-        ChildSessionCallback childSessionCallback =
+        IkeSessionArgumentCaptors ikeSessionArgumentCaptors =
                 verifyBringUpTunnelWithDnsQuery(testApnName, mMockIkeSession);
+        ChildSessionCallback childSessionCallback =
+                ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.getValue();
         childSessionCallback.onIpSecTransformCreated(
                 mMockedIpSecTransformIn, IpSecManager.DIRECTION_IN);
 
         mEpdgTunnelManager
-                .getTmIkeSessionCallback(testApnName)
+                .getTmIkeSessionCallback(testApnName, DEFAULT_TOKEN)
                 .onIkeSessionConnectionInfoChanged(mMockIkeSessionConnectionInfo);
         mTestLooper.dispatchAll();
 
@@ -1368,15 +1494,6 @@
         PersistableBundle bundle = new PersistableBundle();
         setupMockForGetConfig(bundle);
 
-        when(mMockEpdgSelector.getValidatedServerList(
-                        anyInt(),
-                        anyInt(),
-                        eq(false),
-                        eq(false),
-                        eq(mMockNetwork),
-                        any(EpdgSelector.EpdgSelectorCallback.class)))
-                .thenReturn(new IwlanError(IwlanError.NO_ERROR));
-
         doReturn(null)
                 .when(mMockIkeSessionCreator)
                 .createIkeSession(
@@ -1394,12 +1511,14 @@
             ret =
                     mEpdgTunnelManager.bringUpTunnel(
                             getHandoverTunnelSetupRequest(TEST_APN_NAME, apnProtocol),
-                            mMockIwlanTunnelCallback);
+                            mMockIwlanTunnelCallback,
+                            mMockIwlanTunnelMetrics);
         } else {
             ret =
                     mEpdgTunnelManager.bringUpTunnel(
                             getBasicTunnelSetupRequest(TEST_APN_NAME, apnProtocol),
-                            mMockIwlanTunnelCallback);
+                            mMockIwlanTunnelCallback,
+                            mMockIwlanTunnelMetrics);
         }
 
         assertTrue(ret);
@@ -1499,17 +1618,23 @@
         doReturn(0L).when(mEpdgTunnelManager).reportIwlanError(eq(testApnName), eq(error));
 
         mEpdgTunnelManager.putApnNameToTunnelConfig(
-                testApnName, mMockIkeSession, mMockIwlanTunnelCallback, null, 0);
+                testApnName,
+                mMockIkeSession,
+                mMockIwlanTunnelCallback,
+                mMockIwlanTunnelMetrics,
+                null,
+                0);
+        int token = mEpdgTunnelManager.incrementAndGetCurrentTokenForApn(testApnName);
 
-        mEpdgTunnelManager.setIsEpdgAddressSelected(true);
+        mEpdgTunnelManager.onConnectedToEpdg(true);
+        mEpdgTunnelManager.setEpdgAddress(InetAddresses.parseNumericAddress(EPDG_ADDRESS));
 
         mEpdgTunnelManager
-                .getTmIkeSessionCallback(testApnName)
-                .onClosedExceptionally(mockException);
+                .getTmIkeSessionCallback(testApnName, token)
+                .onClosedWithException(mockException);
         mTestLooper.dispatchAll();
 
         verify(mMockIwlanTunnelCallback, times(1)).onClosed(eq(testApnName), eq(error));
-        verify(mEpdgTunnelManager, times(2)).resetTunnelManagerState();
         verify(mEpdgTunnelManager, times(1)).reportIwlanError(eq(testApnName), eq(error));
     }
 
@@ -1527,19 +1652,43 @@
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
 
         mEpdgTunnelManager.sendSelectionRequestComplete(null, error, 1);
         mTestLooper.dispatchAll();
 
-        mEpdgTunnelManager.setIsEpdgAddressSelected(false);
+        mEpdgTunnelManager.onConnectedToEpdg(false);
 
         verify(mEpdgTunnelManager, times(1)).reportIwlanError(eq(testApnName), eq(error));
     }
 
     @Test
+    public void testNeverReportIwlanErrorWhenCloseAnOpenedTunnel() throws Exception {
+        IkeInternalException ikeException =
+                new IkeInternalException(new IOException("Retransmitting failure"));
+
+        IkeSessionArgumentCaptors ikeSessionArgumentCaptors =
+                verifyBringUpTunnelWithDnsQuery(TEST_APN_NAME);
+
+        ChildSessionCallback childSessionCallback =
+                ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.getValue();
+        verifyTunnelOnOpened(TEST_APN_NAME, childSessionCallback);
+
+        reset(mEpdgTunnelManager); // reset number of times of reportIwlanError()
+
+        mEpdgTunnelManager
+                .getTmIkeSessionCallback(TEST_APN_NAME, 0)
+                .onClosedWithException(ikeException);
+        mTestLooper.dispatchAll();
+        verify(mEpdgTunnelManager, never()).reportIwlanError(eq(TEST_APN_NAME), any());
+        verify(mMockIwlanTunnelCallback, times(1))
+                .onClosed(eq(TEST_APN_NAME), eq(new IwlanError(ikeException)));
+    }
+
+    @Test
     public void testCanBringUpTunnel() throws Exception {
         String testApnName = "www.xyz.com";
         IwlanError error = new IwlanError(mMockIkeException);
@@ -1553,7 +1702,8 @@
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
         verify(mMockIwlanTunnelCallback, times(1)).onClosed(eq(testApnName), eq(error));
@@ -1567,15 +1717,6 @@
         PersistableBundle bundle = new PersistableBundle();
         setupMockForGetConfig(bundle);
 
-        when(mMockEpdgSelector.getValidatedServerList(
-                        anyInt(),
-                        anyInt(),
-                        eq(false),
-                        eq(false),
-                        eq(mMockNetwork),
-                        any(EpdgSelector.EpdgSelectorCallback.class)))
-                .thenReturn(new IwlanError(IwlanError.NO_ERROR));
-
         doReturn(null)
                 .when(mMockIkeSessionCreator)
                 .createIkeSession(
@@ -1593,7 +1734,8 @@
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(
                                 TEST_APN_NAME, ApnSetting.PROTOCOL_IPV6, PDU_SESSION_ID),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
 
         assertTrue(ret);
         mTestLooper.dispatchAll();
@@ -1637,14 +1779,6 @@
         bundle.putInt(CarrierConfigManager.Iwlan.KEY_NATT_KEEP_ALIVE_TIMER_SEC_INT, nattTimer);
 
         setupMockForGetConfig(bundle);
-        when(mMockEpdgSelector.getValidatedServerList(
-                        anyInt(),
-                        anyInt(),
-                        eq(false),
-                        eq(false),
-                        eq(mMockNetwork),
-                        any(EpdgSelector.EpdgSelectorCallback.class)))
-                .thenReturn(new IwlanError(IwlanError.NO_ERROR));
 
         doReturn(null)
                 .when(mMockIkeSessionCreator)
@@ -1660,7 +1794,8 @@
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
 
@@ -1698,7 +1833,6 @@
         TunnelSetupRequest tsr =
                 TunnelSetupRequest.builder()
                         .setApnName(testApnName)
-                        .setNetwork(mMockNetwork)
                         .setApnIpProtocol(ApnSetting.PROTOCOL_IPV4V6)
                         .setSrcIpv6Address(testAddressV6)
                         .setSrcIpv6AddressPrefixLength(ipv6AddressLen)
@@ -1710,14 +1844,6 @@
                         .build();
 
         setupMockForGetConfig(null);
-        when(mMockEpdgSelector.getValidatedServerList(
-                        anyInt(),
-                        anyInt(),
-                        eq(isRoaming),
-                        eq(isEmergency),
-                        eq(mMockNetwork),
-                        any(EpdgSelector.EpdgSelectorCallback.class)))
-                .thenReturn(new IwlanError(IwlanError.NO_ERROR));
 
         doReturn(null)
                 .when(mMockIkeSessionCreator)
@@ -1730,7 +1856,9 @@
                         any(ChildSessionCallback.class));
         doReturn(true).when(mEpdgTunnelManager).canBringUpTunnel(eq(testApnName));
 
-        boolean ret = mEpdgTunnelManager.bringUpTunnel(tsr, mMockIwlanTunnelCallback);
+        boolean ret =
+                mEpdgTunnelManager.bringUpTunnel(
+                        tsr, mMockIwlanTunnelCallback, mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
 
@@ -1739,9 +1867,10 @@
                 .getValidatedServerList(
                         anyInt(),
                         anyInt(), // only Ipv6 address is added
+                        anyInt(),
                         eq(isRoaming),
                         eq(isEmergency),
-                        eq(mMockNetwork),
+                        eq(mMockDefaultNetwork),
                         any(EpdgSelector.EpdgSelectorCallback.class));
 
         mEpdgTunnelManager.sendSelectionRequestComplete(
@@ -1771,7 +1900,7 @@
         assertEquals(ikeId.fqdn, testApnName);
 
         // verify Network
-        assertEquals(ikeSessionParams.getNetwork(), mMockNetwork);
+        assertEquals(ikeSessionParams.getNetwork(), mMockDefaultNetwork);
 
         // verify requestPcscf (true) with Apn protocol IPV6
         // it should add the pcscf config requests of type ConfigRequestIpv6PcscfServer and
@@ -1835,7 +1964,8 @@
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
 
@@ -1859,12 +1989,14 @@
 
         when(mMockSubscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(DEFAULT_SLOT_INDEX))
                 .thenReturn(mMockSubscriptionInfo)
+                .thenReturn(mMockSubscriptionInfo)
                 .thenReturn(null);
 
         boolean ret =
                 mEpdgTunnelManager.bringUpTunnel(
                         getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
-                        mMockIwlanTunnelCallback);
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
         assertTrue(ret);
         mTestLooper.dispatchAll();
 
@@ -1874,4 +2006,474 @@
 
         verify(mMockIwlanTunnelCallback, times(1)).onClosed(eq(testApnName), eq(error));
     }
+
+    @Test
+    public void testCloseTunnelWithEpdgSelectionIncomplete() {
+        // Bring up tunnel
+        doReturn(true).when(mEpdgTunnelManager).canBringUpTunnel(eq(TEST_APN_NAME));
+
+        boolean ret =
+                mEpdgTunnelManager.bringUpTunnel(
+                        getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
+                        mMockIwlanTunnelCallback,
+                        mMockIwlanTunnelMetrics);
+        assertTrue(ret);
+
+        // close tunnel when ePDG selection is incomplete
+        mEpdgTunnelManager.closeTunnel(
+                TEST_APN_NAME,
+                false /*forceClose*/,
+                mMockIwlanTunnelCallback,
+                mMockIwlanTunnelMetrics);
+        mTestLooper.dispatchAll();
+
+        verify(mMockIwlanTunnelCallback, times(1))
+                .onClosed(eq(TEST_APN_NAME), eq(new IwlanError(IwlanError.NO_ERROR)));
+        ArgumentCaptor<OnClosedMetrics> metricsCaptor =
+                ArgumentCaptor.forClass(OnClosedMetrics.class);
+        verify(mMockIwlanTunnelMetrics, times(1)).onClosed(metricsCaptor.capture());
+        assertEquals(TEST_APN_NAME, metricsCaptor.getValue().getApnName());
+        assertNull(metricsCaptor.getValue().getEpdgServerAddress());
+    }
+
+    @Test
+    public void testIgnoreSignalFromObsoleteCallback() throws Exception {
+        int transactionId = 0;
+
+        // testApnName with token 0
+        setupTunnelBringup(TEST_APN_NAME, EXPECTED_EPDG_ADDRESSES, ++transactionId);
+        mEpdgTunnelManager.onConnectedToEpdg(true);
+
+        IwlanError error = new IwlanError(mMockIkeException);
+        doReturn(0L).when(mEpdgTunnelManager).reportIwlanError(eq(TEST_APN_NAME), eq(error));
+
+        mEpdgTunnelManager
+                .getTmIkeSessionCallback(TEST_APN_NAME, 0 /* token */)
+                .onClosedWithException(mMockIkeException);
+        mTestLooper.dispatchAll();
+        verify(mMockIwlanTunnelCallback, times(1)).onClosed(eq(TEST_APN_NAME), eq(error));
+        assertNull(mEpdgTunnelManager.getTunnelConfigForApn(TEST_APN_NAME));
+
+        // testApnName1 with token 1
+        setupTunnelBringup(TEST_APN_NAME, EXPECTED_EPDG_ADDRESSES, ++transactionId);
+        mEpdgTunnelManager.onConnectedToEpdg(true);
+
+        // signal from obsolete callback (token 0), ignore it
+        reset(mMockIwlanTunnelCallback);
+        mEpdgTunnelManager
+                .getTmIkeSessionCallback(TEST_APN_NAME, 0 /* token */)
+                .onClosedWithException(mMockIkeException);
+        mTestLooper.dispatchAll();
+        verify(mMockIwlanTunnelCallback, never()).onClosed(eq(TEST_APN_NAME), eq(error));
+        assertNotNull(mEpdgTunnelManager.getTunnelConfigForApn(TEST_APN_NAME));
+
+        // signals from active callback
+        mEpdgTunnelManager
+                .getTmIkeSessionCallback(TEST_APN_NAME, 1 /* token */)
+                .onClosedWithException(mMockIkeException);
+        mTestLooper.dispatchAll();
+        verify(mMockIwlanTunnelCallback, times(1)).onClosed(eq(TEST_APN_NAME), eq(error));
+        assertNull(mEpdgTunnelManager.getTunnelConfigForApn(TEST_APN_NAME));
+    }
+
+    @Test
+    public void testBringUpTunnelIpv4Preferred() throws Exception {
+        TunnelSetupRequest TSR = getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP);
+        doReturn(true).when(mEpdgTunnelManager).canBringUpTunnel(eq(TEST_APN_NAME));
+
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putInt(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT,
+                CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV4_PREFERRED);
+        setupMockForGetConfig(bundle);
+
+        boolean ret =
+                mEpdgTunnelManager.bringUpTunnel(
+                        TSR, mMockIwlanTunnelCallback, mMockIwlanTunnelMetrics);
+        assertTrue(ret);
+        mTestLooper.dispatchAll();
+
+        verify(mMockEpdgSelector)
+                .getValidatedServerList(
+                        anyInt(),
+                        eq(EpdgSelector.PROTO_FILTER_IPV4V6),
+                        eq(EpdgSelector.IPV4_PREFERRED),
+                        eq(false),
+                        eq(false),
+                        eq(mMockDefaultNetwork),
+                        any());
+    }
+
+    @Test
+    public void testBringUpTunnelIpv6Preferred() throws Exception {
+        TunnelSetupRequest TSR = getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP);
+        doReturn(true).when(mEpdgTunnelManager).canBringUpTunnel(eq(TEST_APN_NAME));
+
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putInt(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT,
+                CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV6_PREFERRED);
+        setupMockForGetConfig(bundle);
+
+        boolean ret =
+                mEpdgTunnelManager.bringUpTunnel(
+                        TSR, mMockIwlanTunnelCallback, mMockIwlanTunnelMetrics);
+        assertTrue(ret);
+        mTestLooper.dispatchAll();
+
+        verify(mMockEpdgSelector)
+                .getValidatedServerList(
+                        anyInt(),
+                        eq(EpdgSelector.PROTO_FILTER_IPV4V6),
+                        eq(EpdgSelector.IPV6_PREFERRED),
+                        eq(false),
+                        eq(false),
+                        eq(mMockDefaultNetwork),
+                        any());
+    }
+
+    @Test
+    public void testBringUpTunnelIpv4Only() throws Exception {
+        TunnelSetupRequest TSR = getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP);
+        doReturn(true).when(mEpdgTunnelManager).canBringUpTunnel(eq(TEST_APN_NAME));
+
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putInt(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT,
+                CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV4_ONLY);
+        setupMockForGetConfig(bundle);
+
+        boolean ret =
+                mEpdgTunnelManager.bringUpTunnel(
+                        TSR, mMockIwlanTunnelCallback, mMockIwlanTunnelMetrics);
+        assertTrue(ret);
+        mTestLooper.dispatchAll();
+
+        verify(mMockEpdgSelector)
+                .getValidatedServerList(
+                        anyInt(),
+                        eq(EpdgSelector.PROTO_FILTER_IPV4),
+                        eq(EpdgSelector.SYSTEM_PREFERRED),
+                        eq(false),
+                        eq(false),
+                        eq(mMockDefaultNetwork),
+                        any());
+    }
+
+    @Test
+    public void testBringUpTunnelIpv6Only() throws Exception {
+        TunnelSetupRequest TSR =
+                getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IPV6);
+        doReturn(true).when(mEpdgTunnelManager).canBringUpTunnel(eq(TEST_APN_NAME));
+        doReturn(EXPECTED_IPV6_LOCAL_ADDRESSES)
+                .when(mEpdgTunnelManager)
+                .getAddressForNetwork(any(), any());
+
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putInt(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT,
+                CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV6_ONLY);
+        setupMockForGetConfig(bundle);
+
+        boolean ret =
+                mEpdgTunnelManager.bringUpTunnel(
+                        TSR, mMockIwlanTunnelCallback, mMockIwlanTunnelMetrics);
+        assertTrue(ret);
+        mTestLooper.dispatchAll();
+
+        verify(mMockEpdgSelector)
+                .getValidatedServerList(
+                        anyInt(),
+                        eq(EpdgSelector.PROTO_FILTER_IPV6),
+                        eq(EpdgSelector.SYSTEM_PREFERRED),
+                        eq(false),
+                        eq(false),
+                        eq(mMockDefaultNetwork),
+                        any());
+    }
+
+    @Test
+    public void testBringUpTunnelIpv6OnlyOnIpv4Wifi() throws Exception {
+        TunnelSetupRequest TSR =
+                getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IPV6);
+        IwlanError error = new IwlanError(IwlanError.EPDG_ADDRESS_ONLY_IPV6_ALLOWED);
+        doReturn(true).when(mEpdgTunnelManager).canBringUpTunnel(eq(TEST_APN_NAME));
+        doReturn(0L).when(mEpdgTunnelManager).reportIwlanError(eq(TEST_APN_NAME), eq(error));
+
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putInt(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT,
+                CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV6_ONLY);
+        setupMockForGetConfig(bundle);
+
+        boolean ret =
+                mEpdgTunnelManager.bringUpTunnel(
+                        TSR, mMockIwlanTunnelCallback, mMockIwlanTunnelMetrics);
+        assertTrue(ret);
+        mTestLooper.dispatchAll();
+
+        verify(mMockEpdgSelector, never())
+                .getValidatedServerList(
+                        anyInt(),
+                        anyInt(),
+                        anyInt(),
+                        eq(false),
+                        eq(false),
+                        eq(mMockDefaultNetwork),
+                        any());
+        verify(mEpdgTunnelManager, times(1)).reportIwlanError(eq(TEST_APN_NAME), eq(error));
+        verify(mMockIwlanTunnelCallback, times(1)).onClosed(eq(TEST_APN_NAME), eq(error));
+    }
+
+    @Test
+    public void testBringUpTunnelSystemPreferred() throws Exception {
+        TunnelSetupRequest TSR = getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP);
+        doReturn(true).when(mEpdgTunnelManager).canBringUpTunnel(eq(TEST_APN_NAME));
+
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putInt(
+                CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT,
+                CarrierConfigManager.Iwlan.EPDG_ADDRESS_SYSTEM_PREFERRED);
+        setupMockForGetConfig(bundle);
+
+        boolean ret =
+                mEpdgTunnelManager.bringUpTunnel(
+                        TSR, mMockIwlanTunnelCallback, mMockIwlanTunnelMetrics);
+        assertTrue(ret);
+        mTestLooper.dispatchAll();
+
+        verify(mMockEpdgSelector)
+                .getValidatedServerList(
+                        anyInt(),
+                        eq(EpdgSelector.PROTO_FILTER_IPV4V6),
+                        eq(EpdgSelector.SYSTEM_PREFERRED),
+                        eq(false),
+                        eq(false),
+                        eq(mMockDefaultNetwork),
+                        any());
+    }
+
+    @Test
+    public void testOnOpenedTunnelMetricsData() throws Exception {
+        doReturn(true).when(mEpdgTunnelManager).canBringUpTunnel(eq(TEST_APN_NAME));
+        mEpdgTunnelManager.bringUpTunnel(
+                getBasicTunnelSetupRequest(TEST_APN_NAME, ApnSetting.PROTOCOL_IP),
+                mMockIwlanTunnelCallback,
+                mMockIwlanTunnelMetrics);
+        IkeSessionArgumentCaptors ikeSessionArgumentCaptors =
+                verifyBringUpTunnelWithDnsQuery(TEST_APN_NAME);
+        ChildSessionCallback childSessionCallback =
+                ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.getValue();
+        verifyTunnelOnOpened(TEST_APN_NAME, childSessionCallback);
+        mTestLooper.dispatchAll();
+
+        ArgumentCaptor<OnOpenedMetrics> metricsCaptor =
+                ArgumentCaptor.forClass(OnOpenedMetrics.class);
+        verify(mMockIwlanTunnelMetrics, times(1)).onOpened(metricsCaptor.capture());
+        assertEquals(TEST_APN_NAME, metricsCaptor.getValue().getApnName());
+    }
+
+    @Test
+    public void testCloseTunnelWithIkeInitTimeout() throws Exception {
+        String testApnName = "www.xyz.com";
+        IwlanError error = new IwlanError(IwlanError.IKE_INIT_TIMEOUT, mMockIkeIoException);
+        doReturn(0L).when(mEpdgTunnelManager).reportIwlanError(eq(testApnName), eq(error));
+
+        setupTunnelBringup();
+
+        ArgumentCaptor<EpdgTunnelManager.TmIkeSessionCallback> ikeSessionCallbackCaptor =
+                ArgumentCaptor.forClass(EpdgTunnelManager.TmIkeSessionCallback.class);
+        verify(mMockIkeSessionCreator, atLeastOnce())
+                .createIkeSession(
+                        eq(mMockContext),
+                        any(IkeSessionParams.class),
+                        any(ChildSessionParams.class),
+                        any(Executor.class),
+                        ikeSessionCallbackCaptor.capture(),
+                        any(ChildSessionCallback.class));
+        ikeSessionCallbackCaptor.getValue().onClosedWithException(mMockIkeIoException);
+        mTestLooper.dispatchAll();
+
+        verify(mEpdgTunnelManager, times(1)).reportIwlanError(eq(testApnName), eq(error));
+        verify(mMockIwlanTunnelCallback, atLeastOnce()).onClosed(eq(testApnName), eq(error));
+    }
+
+    @Test
+    public void testCloseTunnelWithIkeDpdTimeout() throws Exception {
+        IwlanError error = new IwlanError(IwlanError.IKE_DPD_TIMEOUT, mMockIkeIoException);
+
+        IkeSessionArgumentCaptors ikeSessionArgumentCaptors =
+                verifyBringUpTunnelWithDnsQuery(TEST_APN_NAME);
+        ChildSessionCallback childSessionCallback =
+                ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.getValue();
+        verifyTunnelOnOpened(TEST_APN_NAME, childSessionCallback);
+        mEpdgTunnelManager
+                .getTmIkeSessionCallback(
+                        TEST_APN_NAME, mEpdgTunnelManager.getCurrentTokenForApn(TEST_APN_NAME))
+                .onClosedWithException(mMockIkeIoException);
+        mTestLooper.dispatchAll();
+
+        verify(mEpdgTunnelManager, never()).reportIwlanError(eq(TEST_APN_NAME), eq(error));
+        verify(mMockIwlanTunnelCallback, times(1)).onClosed(eq(TEST_APN_NAME), eq(error));
+    }
+
+    @Test
+    public void testCloseTunnelWithIkeMobilityTimeout() throws Exception {
+        IwlanError error = new IwlanError(IwlanError.IKE_MOBILITY_TIMEOUT, mMockIkeIoException);
+
+        IkeSessionArgumentCaptors ikeSessionArgumentCaptors =
+                verifyBringUpTunnelWithDnsQuery(TEST_APN_NAME, mMockIkeSession);
+        ChildSessionCallback childSessionCallback =
+                ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.getValue();
+        verifyTunnelOnOpened(TEST_APN_NAME, childSessionCallback);
+
+        Network newNetwork = mock(Network.class);
+        mEpdgTunnelManager.updateNetwork(newNetwork, mMockLinkProperties);
+        mTestLooper.dispatchAll();
+
+        mEpdgTunnelManager
+                .getTmIkeSessionCallback(
+                        TEST_APN_NAME, mEpdgTunnelManager.getCurrentTokenForApn(TEST_APN_NAME))
+                .onClosedWithException(mMockIkeIoException);
+        mTestLooper.dispatchAll();
+
+        verify(mMockIkeSession, times(1)).setNetwork(eq(newNetwork));
+        verify(mEpdgTunnelManager, never()).reportIwlanError(eq(TEST_APN_NAME), eq(error));
+        verify(mMockIwlanTunnelCallback, times(1)).onClosed(eq(TEST_APN_NAME), eq(error));
+    }
+
+    private boolean testIsN1ModeSupported(int[] nrAvailability) {
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putIntArray(
+                CarrierConfigManager.KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY, nrAvailability);
+
+        setupMockForGetConfig(bundle);
+
+        return mEpdgTunnelManager.isN1ModeSupported();
+    }
+
+    @Test
+    public void testIsN1ModeSupportedTrue() throws Exception {
+        assertTrue(
+                testIsN1ModeSupported(
+                        new int[] {
+                            CarrierConfigManager.CARRIER_NR_AVAILABILITY_NSA,
+                            CarrierConfigManager.CARRIER_NR_AVAILABILITY_SA
+                        }));
+    }
+
+    @Test
+    public void testIsN1ModeSupportedFalse() throws Exception {
+        assertFalse(
+                testIsN1ModeSupported(
+                        new int[] {CarrierConfigManager.CARRIER_NR_AVAILABILITY_NSA}));
+    }
+
+    @Test
+    public void testUpdateNetworkToOpenedTunnel() throws Exception {
+        String apnName = "ims";
+
+        IkeSessionArgumentCaptors ikeSessionArgumentCaptors =
+                verifyBringUpTunnelWithDnsQuery(apnName, mMockIkeSession);
+        ChildSessionCallback childSessionCallback =
+                ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.getValue();
+        childSessionCallback.onIpSecTransformCreated(
+                mMockedIpSecTransformIn, IpSecManager.DIRECTION_IN);
+        mTestLooper.dispatchAll();
+
+        mEpdgTunnelManager.onConnectedToEpdg(true);
+        Network newNetwork = mock(Network.class);
+        mEpdgTunnelManager.updateNetwork(newNetwork, mMockLinkProperties);
+        mTestLooper.dispatchAll();
+        verify(mMockIkeSession, times(1)).setNetwork(eq(newNetwork));
+    }
+
+    @Test
+    public void testUpdateNetworkForIncomingSetupRequest() throws Exception {
+        String apnName = "ims";
+        Network newNetwork = mock(Network.class);
+
+        mEpdgTunnelManager.updateNetwork(newNetwork, mMockLinkProperties);
+        mTestLooper.dispatchAll();
+
+        IkeSessionArgumentCaptors ikeSessionArgumentCaptors =
+                verifyBringUpTunnelWithDnsQuery(apnName, mMockIkeSession);
+        ChildSessionCallback childSessionCallback =
+                ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.getValue();
+        childSessionCallback.onIpSecTransformCreated(
+                mMockedIpSecTransformIn, IpSecManager.DIRECTION_IN);
+        mTestLooper.dispatchAll();
+
+        verify(mMockEpdgSelector, times(1))
+                .getValidatedServerList(
+                        anyInt(), /* transactionId */
+                        anyInt(), /* filter */
+                        anyInt(), /* order */
+                        eq(false), /* isRoaming */
+                        eq(false), /* isEmergency */
+                        eq(newNetwork),
+                        any(EpdgSelector.EpdgSelectorCallback.class));
+        IkeSessionParams ikeSessionParams =
+                ikeSessionArgumentCaptors.mIkeSessionParamsCaptor.getValue();
+        assertEquals(ikeSessionParams.getNetwork(), newNetwork);
+    }
+
+    @Test
+    public void testUpdateNullNetworkToOpenedTunnel() throws Exception {
+        String apnName = "ims";
+
+        IkeSessionArgumentCaptors ikeSessionArgumentCaptors =
+                verifyBringUpTunnelWithDnsQuery(apnName, mMockIkeSession);
+        ChildSessionCallback childSessionCallback =
+                ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.getValue();
+        childSessionCallback.onIpSecTransformCreated(
+                mMockedIpSecTransformIn, IpSecManager.DIRECTION_IN);
+        mTestLooper.dispatchAll();
+
+        mEpdgTunnelManager.updateNetwork(null, null);
+        mTestLooper.dispatchAll();
+        verify(mMockIkeSession, never()).setNetwork(any());
+    }
+
+    @Test
+    public void testUpdateNullNetworkAndRejectIncomingSetupRequest() throws Exception {
+        String apnName = "ims";
+
+        doReturn(0L).when(mEpdgTunnelManager).reportIwlanError(eq(apnName), any(IwlanError.class));
+
+        mEpdgTunnelManager.updateNetwork(null, null);
+        mTestLooper.dispatchAll();
+
+        mEpdgTunnelManager.bringUpTunnel(
+                getBasicTunnelSetupRequest(apnName, ApnSetting.PROTOCOL_IP),
+                mMockIwlanTunnelCallback,
+                mMockIwlanTunnelMetrics);
+        mTestLooper.dispatchAll();
+        verify(mMockIwlanTunnelCallback, times(1)).onClosed(eq(apnName), any(IwlanError.class));
+    }
+
+    @Test
+    public void testUpdateUnreachableLinkProperties() throws Exception {
+        String apnName = "ims";
+
+        IkeSessionArgumentCaptors ikeSessionArgumentCaptors =
+                verifyBringUpTunnelWithDnsQuery(apnName, mMockIkeSession);
+        ChildSessionCallback childSessionCallback =
+                ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.getValue();
+        childSessionCallback.onIpSecTransformCreated(
+                mMockedIpSecTransformIn, IpSecManager.DIRECTION_IN);
+        mTestLooper.dispatchAll();
+
+        mEpdgTunnelManager.onConnectedToEpdg(true);
+        Network newNetwork = mock(Network.class);
+        LinkProperties mockUnreachableLinkProperties = mock(LinkProperties.class);
+        when(mockUnreachableLinkProperties.isReachable(any())).thenReturn(false);
+        mEpdgTunnelManager.updateNetwork(newNetwork, mockUnreachableLinkProperties);
+        mTestLooper.dispatchAll();
+        verify(mMockIkeSession, never()).setNetwork(eq(newNetwork));
+
+        mEpdgTunnelManager.updateNetwork(newNetwork, mMockLinkProperties);
+        mTestLooper.dispatchAll();
+        verify(mMockIkeSession, times(1)).setNetwork(eq(newNetwork));
+    }
 }