Merge a7cabd5d16264bb5c64c99240d7fc87cb6aa04d4 on remote branch

Change-Id: I56e5edfa2acd33cc61b000eb48d6362bc3f6a15e
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 6a7a2d4..5c3cfac 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -8,6 +8,7 @@
   <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.NETWORK_SETTINGS"/>
   <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" />
diff --git a/assets/defaultiwlanerrorconfig.json b/assets/defaultiwlanerrorconfig.json
index eae1ae2..7dd5818 100644
--- a/assets/defaultiwlanerrorconfig.json
+++ b/assets/defaultiwlanerrorconfig.json
@@ -75,7 +75,7 @@
       },
       {
         "ErrorType": "IKE_PROTOCOL_ERROR_TYPE",
-        "ErrorDetails": ["24"],
+        "ErrorDetails": ["24", "9002"],
         "RetryArray": ["10", "20", "40", "80", "160"],
         "UnthrottlingEvents": ["APM_ENABLE_EVENT", "WIFI_DISABLE_EVENT", "WIFI_CALLING_DISABLE_EVENT"]
       }
diff --git a/src/com/google/android/iwlan/ErrorPolicyManager.java b/src/com/google/android/iwlan/ErrorPolicyManager.java
index 59fad64..6ab3141 100644
--- a/src/com/google/android/iwlan/ErrorPolicyManager.java
+++ b/src/com/google/android/iwlan/ErrorPolicyManager.java
@@ -116,6 +116,9 @@
     private static final int IKE_PROTOCOL_ERROR_PLMN_NOT_ALLOWED = 11011;
     private static final int IKE_PROTOCOL_ERROR_UNAUTHENTICATED_EMERGENCY_NOT_SUPPORTED = 11055;
 
+    /** 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;
+
     @IntDef({
         IKE_PROTOCOL_ERROR_PDN_CONNECTION_REJECTION,
         IKE_PROTOCOL_ERROR_MAX_CONNECTION_REACHED,
@@ -132,7 +135,8 @@
         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
+        IKE_PROTOCOL_ERROR_UNAUTHENTICATED_EMERGENCY_NOT_SUPPORTED,
+        IKE_PROTOCOL_ERROR_CONGESTION
     })
     @interface IkeProtocolErrorType {};
 
@@ -153,6 +157,10 @@
     // String APN as key to identify the ErrorInfo associated with that APN
     private 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;
 
@@ -178,6 +186,11 @@
         return mInstances.computeIfAbsent(slotId, k -> new ErrorPolicyManager(context, slotId));
     }
 
+    @VisibleForTesting
+    public static void resetAllInstances() {
+        mInstances.clear();
+    }
+
     /**
      * Release or reset the instance.
      *
@@ -202,6 +215,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");
@@ -292,13 +306,19 @@
      * @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.IKE_INTERNAL_IO_EXCEPTION) {
             ret = DataFailCause.IWLAN_IKEV2_MSG_TIMEOUT;
@@ -362,6 +382,9 @@
                     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;
                         break;
@@ -371,6 +394,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 +415,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
@@ -532,6 +579,7 @@
                                 errorType,
                                 parseErrorDetails(errorType, errorDetailArray),
                                 parseRetryArray((JSONArray) errorTypeObject.get("RetryArray")),
+                                errorTypeObject.optInt("NumAttemptsPerFqdn", -1),
                                 parseUnthrottlingEvents(
                                         (JSONArray) errorTypeObject.get("UnthrottlingEvents")));
 
@@ -778,16 +826,19 @@
         @ErrorPolicyErrorType int mErrorType;
         List<String> mErrorDetails;
         List<Integer> mRetryArray;
+        int mNumAttemptsPerFqdn;
         List<Integer> mUnthrottlingEvents;
 
         ErrorPolicy(
                 @ErrorPolicyErrorType int errorType,
                 List<String> errorDetails,
                 List<Integer> retryArray,
+                int numAttemptsPerFqdn,
                 List<Integer> unthrottlingEvents) {
             mErrorType = errorType;
             mErrorDetails = errorDetails;
             mRetryArray = retryArray;
+            mNumAttemptsPerFqdn = numAttemptsPerFqdn;
             mUnthrottlingEvents = unthrottlingEvents;
         }
 
@@ -814,6 +865,16 @@
             return retryTime;
         }
 
+        int getCurrentFqdnIndex(int retryIndex, int numFqdns) {
+            int result = -1;
+            if (mNumAttemptsPerFqdn == -1 || mRetryArray.size() <= 0) {
+                return result;
+            }
+            // Cycles between 0 and (numFqdns - 1), based on the current attempt count and size of
+            // mRetryArray.
+            return (retryIndex + 1) / mNumAttemptsPerFqdn % numFqdns;
+        }
+
         @ErrorPolicyErrorType
         int getErrorType() {
             return mErrorType;
@@ -901,6 +962,9 @@
     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;
@@ -958,6 +1022,11 @@
             return time;
         }
 
+        int getCurrentFqdnIndex(int numFqdns) {
+            ErrorPolicy errorPolicy = getErrorPolicy();
+            return errorPolicy.getCurrentFqdnIndex(mCurrentRetryIndex, numFqdns);
+        }
+
         boolean isBackOffTimeValid() {
             return mIsBackOffTimeValid;
         }
@@ -996,6 +1065,16 @@
         }
     }
 
+    static class ApnWithIwlanError {
+        @NonNull final String mApn;
+        @NonNull final IwlanError mIwlanError;
+
+        ApnWithIwlanError(String apn, IwlanError iwlanError) {
+            mApn = apn;
+            mIwlanError = iwlanError;
+        }
+    }
+
     private boolean isValidCarrierConfigChangedEvent(int currentCarrierId) {
         String errorPolicyConfig =
                 (String) IwlanHelper.getConfig(KEY_ERROR_POLICY_CONFIG_STRING, mContext, mSlotId);
diff --git a/src/com/google/android/iwlan/IwlanDataService.java b/src/com/google/android/iwlan/IwlanDataService.java
index a254535..77207a2 100644
--- a/src/com/google/android/iwlan/IwlanDataService.java
+++ b/src/com/google/android/iwlan/IwlanDataService.java
@@ -31,7 +31,6 @@
 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;
@@ -76,12 +75,22 @@
     private static final String TAG = IwlanDataService.class.getSimpleName();
     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>();
+    @VisibleForTesting Handler mIwlanDataServiceHandler;
+    private HandlerThread mIwlanDataServiceHandlerThread;
+    private static final Map<Integer, IwlanDataServiceProvider> sIwlanDataServiceProviders =
+            new ConcurrentHashMap<>();
+
+    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;
 
     @VisibleForTesting
     enum Transport {
@@ -103,6 +112,7 @@
 
     // 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. */
@@ -138,7 +148,7 @@
         public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) {
             Log.d(TAG, "onLinkPropertiesChanged: " + linkProperties);
             if (isLinkProtocolTypeChanged(linkProperties)) {
-                for (IwlanDataServiceProvider dp : sIwlanDataServiceProviderList) {
+                for (IwlanDataServiceProvider dp : sIwlanDataServiceProviders.values()) {
                     dp.dnsPrefetchCheck();
                 }
             }
@@ -180,8 +190,6 @@
         private final String SUB_TAG;
         private final IwlanDataService mIwlanDataService;
         private final IwlanTunnelCallback mIwlanTunnelCallback;
-        private HandlerThread mHandlerThread;
-        @VisibleForTesting Handler mHandler;
         private boolean mWfcEnabled = false;
         private boolean mCarrierConfigReady = false;
         private EpdgSelector mEpdgSelector;
@@ -189,10 +197,7 @@
         private CellInfo mCellInfo = null;
 
         // 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")
+        // Access should be serialized inside IwlanDataServiceHandler
         private Map<String, TunnelState> mTunnelStateForApn = new ConcurrentHashMap<>();
 
         // Holds the state of a tunnel (for an APN)
@@ -204,6 +209,7 @@
             // 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_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;
@@ -230,7 +236,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 substract tunnelling overhead
+                }
             }
 
             public void setProtocolType(int protocolType) {
@@ -264,7 +274,9 @@
                 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) {
@@ -345,154 +357,22 @@
                 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));
-                }
+                mIwlanDataServiceHandler.sendMessage(
+                        mIwlanDataServiceHandler.obtainMessage(
+                                EVENT_TUNNEL_OPENED,
+                                new TunnelOpenedData(
+                                        apnName, linkProperties, IwlanDataServiceProvider.this)));
             }
 
             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);
+                mIwlanDataServiceHandler.sendMessage(
+                        mIwlanDataServiceHandler.obtainMessage(
+                                EVENT_TUNNEL_CLOSED,
+                                new TunnelClosedData(
+                                        apnName, error, IwlanDataServiceProvider.this)));
             }
         }
 
@@ -637,12 +517,6 @@
             }
         }
 
-        Looper getLooper() {
-            mHandlerThread = new HandlerThread("DSPHandlerThread");
-            mHandlerThread.start();
-            return mHandlerThread.getLooper();
-        }
-
         /**
          * Constructor
          *
@@ -661,18 +535,14 @@
             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.CELLINFO_CHANGED_EVENT);
-            IwlanEventListener.getInstance(mContext, slotIndex).addEventListener(events, mHandler);
-        }
-
-        void initHandler() {
-            mHandler = new DSPHandler(getLooper());
+            IwlanEventListener.getInstance(mContext, slotIndex)
+                    .addEventListener(events, mIwlanDataServiceHandler);
         }
 
         @VisibleForTesting
@@ -693,15 +563,22 @@
                     .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();
+            List<LinkAddress> linkAddrList = tunnelLinkProperties.internalAddresses();
             if (linkAddrList.stream().anyMatch(t -> t.isIpv4())) {
                 try {
                     gatewayList.add(Inet4Address.getByName("0.0.0.0"));
@@ -717,18 +594,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())
@@ -844,128 +719,24 @@
                             + ", 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)) {
-
-                deliverCallback(
-                        CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
-                        DataServiceCallback.RESULT_ERROR_INVALID_ARG,
-                        callback,
-                        null);
-                return;
-            }
-
-            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 */,
+            SetupDataCallData setupDataCallData =
+                    new SetupDataCallData(
+                            accessNetworkType,
+                            dataProfile,
+                            isRoaming,
+                            allowRoaming,
+                            reason,
+                            linkProperties,
+                            pduSessionId,
+                            sliceInfo,
+                            trafficDescriptor,
+                            matchAllRuleAllowed,
                             callback,
-                            null);
-                    return;
-                }
+                            this);
 
-                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,
-                        (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;
-                }
-            }
+            mIwlanDataServiceHandler.sendMessage(
+                    mIwlanDataServiceHandler.obtainMessage(
+                            EVENT_SETUP_DATA_CALL, setupDataCallData));
         }
 
         /**
@@ -992,49 +763,26 @@
                             + "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);
-            }
+            mIwlanDataServiceHandler.sendMessage(
+                    mIwlanDataServiceHandler.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);
                 }
             }
         }
 
         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);
             }
         }
 
@@ -1045,11 +793,10 @@
          */
         @Override
         public void requestDataCallList(DataServiceCallback callback) {
-            deliverCallback(
-                    CALLBACK_TYPE_GET_DATACALL_LIST_COMPLETE,
-                    DataServiceCallback.RESULT_SUCCESS,
-                    callback,
-                    null);
+            mIwlanDataServiceHandler.sendMessage(
+                    mIwlanDataServiceHandler.obtainMessage(
+                            EVENT_DATA_CALL_LIST_REQUEST,
+                            new DataCallRequestData(callback, IwlanDataServiceProvider.this)));
         }
 
         @VisibleForTesting
@@ -1081,23 +828,20 @@
 
         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());
-                            }
+                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());
                         }
                     }
                 }
@@ -1125,23 +869,21 @@
                             IwlanHelper.isDefaultDataSlot(mContext, 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 == true
+                    && mCarrierConfigReady == true
+                    && mWfcEnabled == true
+                    && 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);
             }
         }
 
@@ -1160,8 +902,8 @@
         public void close() {
             // TODO: call epdgtunnelmanager.releaseInstance or equivalent
             mIwlanDataService.removeDataServiceProvider(this);
-            IwlanEventListener.getInstance(mContext, getSlotIndex()).removeEventListener(mHandler);
-            mHandlerThread.quit();
+            IwlanEventListener.getInstance(mContext, getSlotIndex())
+                    .removeEventListener(mIwlanDataServiceHandler);
         }
 
         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
@@ -1174,11 +916,9 @@
                             + isNetworkConnected(isDDS, 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);
@@ -1187,8 +927,528 @@
         }
     }
 
+    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;
+
+            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);
+                    iwlanDataServiceProvider.mTunnelStats.reportTunnelDown(apnName, tunnelState);
+                    iwlanDataServiceProvider.mTunnelStateForApn.remove(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 (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()
+                                == IwlanDataServiceProvider.TunnelState.TUNNEL_IN_BRINGUP) {
+                            respBuilder.setCause(
+                                    ErrorPolicyManager.getInstance(
+                                                    mContext,
+                                                    iwlanDataServiceProvider.getSlotIndex())
+                                            .getDataFailCause(apnName));
+                            respBuilder.setRetryDurationMillis(
+                                    ErrorPolicyManager.getInstance(
+                                                    mContext,
+                                                    iwlanDataServiceProvider.getSlotIndex())
+                                            .getCurrentRetryTimeMs(apnName));
+                        } else if (tunnelState.getState()
+                                == IwlanDataServiceProvider.TunnelState
+                                        .TUNNEL_IN_FORCE_CLEAN_WAS_IN_BRINGUP) {
+                            respBuilder.setCause(DataFailCause.IWLAN_NETWORK_FAILURE);
+                            respBuilder.setRetryDurationMillis(5000);
+                        }
+
+                        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());
+                    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.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 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;
+                    }
+
+                    boolean isDDS =
+                            IwlanHelper.isDefaultDataSlot(
+                                    mContext, iwlanDataServiceProvider.getSlotIndex());
+                    boolean isCSTEnabled =
+                            IwlanHelper.isCrossSimCallingEnabled(
+                                    mContext, iwlanDataServiceProvider.getSlotIndex());
+                    boolean networkConnected = isNetworkConnected(isDDS, isCSTEnabled);
+                    Log.d(
+                            TAG,
+                            "isDds: "
+                                    + isDDS
+                                    + ", isCstEnabled: "
+                                    + isCSTEnabled
+                                    + ", transport: "
+                                    + sDefaultDataTransport);
+
+                    if (networkConnected == false) {
+                        iwlanDataServiceProvider.deliverCallback(
+                                IwlanDataServiceProvider.CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
+                                5 /* DataServiceCallback.RESULT_ERROR_TEMPORARILY_UNAVAILABLE
+                                   */,
+                                callback,
+                                null);
+                        return;
+                    }
+
+                    tunnelState =
+                            iwlanDataServiceProvider.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()
+                                        == IwlanDataServiceProvider.TunnelState.TUNNEL_UP) {
+                            Log.w(
+                                    TAG,
+                                    "The tunnel for " + dataProfile.getApn() + " already exists.");
+                            iwlanDataServiceProvider.deliverCallback(
+                                    IwlanDataServiceProvider.CALLBACK_TYPE_SETUP_DATACALL_COMPLETE,
+                                    DataServiceCallback.RESULT_SUCCESS,
+                                    callback,
+                                    iwlanDataServiceProvider.apnTunnelStateToDataCallResponse(
+                                            dataProfile.getApn()));
+                            return;
+                        } else {
+                            Log.e(
+                                    TAG,
+                                    "Force close the existing PDN. pduSessionId = "
+                                            + tunnelState.getPduSessionId()
+                                            + " Tunnel State = "
+                                            + tunnelState.getState());
+                            iwlanDataServiceProvider
+                                    .getTunnelManager()
+                                    .closeTunnel(dataProfile.getApn(), true /* forceClose */);
+                            iwlanDataServiceProvider.deliverCallback(
+                                    IwlanDataServiceProvider.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);
+
+                    iwlanDataServiceProvider.setTunnelState(
+                            dataProfile,
+                            callback,
+                            IwlanDataServiceProvider.TunnelState.TUNNEL_IN_BRINGUP,
+                            null,
+                            (reason == DataService.REQUEST_REASON_HANDOVER),
+                            pduSessionId);
+
+                    boolean result =
+                            iwlanDataServiceProvider
+                                    .getTunnelManager()
+                                    .bringUpTunnel(
+                                            tunnelReqBuilder.build(),
+                                            iwlanDataServiceProvider.getIwlanTunnelCallback());
+                    Log.d(TAG, "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;
+                    int cid = deactivateDataCallData.mCid;
+                    reason = deactivateDataCallData.mReason;
+                    callback = deactivateDataCallData.mCallback;
+                    iwlanDataServiceProvider = deactivateDataCallData.mIwlanDataServiceProvider;
+
+                    for (String tunnelApnName :
+                            iwlanDataServiceProvider.mTunnelStateForApn.keySet()) {
+                        if (tunnelApnName.hashCode() == cid) {
+                            /*
+                            No need to check state since dataconnection in framework serializes
+                            setup and deactivate calls using callId/cid.
+                            */
+                            iwlanDataServiceProvider
+                                    .mTunnelStateForApn
+                                    .get(tunnelApnName)
+                                    .setState(
+                                            IwlanDataServiceProvider.TunnelState
+                                                    .TUNNEL_IN_BRINGDOWN);
+                            iwlanDataServiceProvider
+                                    .mTunnelStateForApn
+                                    .get(tunnelApnName)
+                                    .setDataServiceCallback(callback);
+                            boolean isConnected =
+                                    isNetworkConnected(
+                                            IwlanHelper.isDefaultDataSlot(
+                                                    mContext,
+                                                    iwlanDataServiceProvider.getSlotIndex()),
+                                            IwlanHelper.isCrossSimCallingEnabled(
+                                                    mContext,
+                                                    iwlanDataServiceProvider.getSlotIndex()));
+                            iwlanDataServiceProvider
+                                    .getTunnelManager()
+                                    .closeTunnel(tunnelApnName, !isConnected);
+                            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;
+
+                    IwlanDataServiceProvider dsp =
+                            sIwlanDataServiceProviders.remove(
+                                    iwlanDataServiceProvider.getSlotIndex());
+                    if (dsp == null) {
+                        Log.w(
+                                TAG,
+                                "No DataServiceProvider exists for slot "
+                                        + iwlanDataServiceProvider.getSlotIndex());
+                    }
+
+                    if (sIwlanDataServiceProviders.isEmpty()) {
+                        deinitNetworkCallback();
+                    }
+                    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;
+        }
+    }
+
     @VisibleForTesting
-    static synchronized boolean isNetworkConnected(boolean isDds, boolean isCstEnabled) {
+    static boolean isNetworkConnected(boolean isDds, boolean isCstEnabled) {
         if (!isDds && isCstEnabled) {
             // Only Non-DDS sub with CST enabled, can use any transport.
             return sNetworkConnected;
@@ -1200,9 +1460,6 @@
 
     @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) {
 
@@ -1210,59 +1467,57 @@
         boolean hasTransportChanged = false;
         boolean hasNetworkConnectedChanged = false;
 
-        synchronized (IwlanDataService.class) {
-            if (sNetworkConnected == networkConnected
-                    && network.equals(sNetwork)
-                    && sDefaultDataTransport == transport) {
-                // Nothing changed
-                return;
-            }
+        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;
-            }
+        // 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 (!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 (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;
-            }
+        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;
-            }
+        sNetworkConnected = networkConnected;
+        sDefaultDataTransport = transport;
+        sNetwork = network;
+        if (!networkConnected) {
+            // reset link protocol type
+            sLinkProtocolType = LinkProtocolType.UNKNOWN;
         }
 
         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 +1527,14 @@
             }
             // only prefetch dns and updateNetwork if Network has changed
             if (hasNetworkChanged) {
-                for (IwlanDataServiceProvider dp : sIwlanDataServiceProviderList) {
+                for (IwlanDataServiceProvider dp : sIwlanDataServiceProviders.values()) {
                     dp.dnsPrefetchCheck();
                     dp.updateNetwork(sNetwork);
                 }
                 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
@@ -1338,16 +1593,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() {
@@ -1359,43 +1605,53 @@
         // TODO: validity check on slot index
         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);
+        if (mIwlanDataServiceHandler == null) {
+            initHandler();
+        }
 
-            // register for default network callback
+        if (mNetworkMonitorCallback == null) {
+            // start monitoring network and register for default network callback
             ConnectivityManager connectivityManager =
                     mContext.getSystemService(ConnectivityManager.class);
             mNetworkMonitorCallback = new IwlanNetworkMonitorCallback();
-            connectivityManager.registerDefaultNetworkCallback(mNetworkMonitorCallback, handler);
+            connectivityManager.registerSystemDefaultNetworkCallback(
+                    mNetworkMonitorCallback, mIwlanDataServiceHandler);
             Log.d(TAG, "Registered with Connectivity Service");
         }
 
         IwlanDataServiceProvider dp = new IwlanDataServiceProvider(slotIndex, this);
-        addIwlanDataServiceProvider(dp);
+
+        mIwlanDataServiceHandler.sendMessage(
+                mIwlanDataServiceHandler.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;
-        }
+        mIwlanDataServiceHandler.sendMessage(
+                mIwlanDataServiceHandler.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);
+        connectivityManager.unregisterNetworkCallback(mNetworkMonitorCallback);
+        mNetworkMonitorCallback = null;
+        if (mIwlanDataServiceHandlerThread != null) {
+            mIwlanDataServiceHandlerThread.quit();
+            mIwlanDataServiceHandlerThread = null;
+        }
+        mIwlanDataServiceHandler = null;
     }
 
     @VisibleForTesting
@@ -1408,6 +1664,51 @@
         return mNetworkMonitorCallback;
     }
 
+    @VisibleForTesting
+    void initHandler() {
+        mIwlanDataServiceHandler = new IwlanDataServiceHandler(getLooper());
+    }
+
+    @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.CELLINFO_CHANGED_EVENT:
+                return "CELLINFO_CHANGED_EVENT";
+            default:
+                return "Unknown(" + event + ")";
+        }
+    }
+
     @Override
     public void onCreate() {
         setAppContext(getApplicationContext());
@@ -1429,11 +1730,8 @@
     @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();
-        }
+        mIwlanDataServiceHandler.sendMessage(
+                mIwlanDataServiceHandler.obtainMessage(EVENT_FORCE_CLOSE_TUNNEL));
         return super.onUnbind(intent);
     }
 
@@ -1446,7 +1744,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/IwlanEventListener.java b/src/com/google/android/iwlan/IwlanEventListener.java
index b4d38e2..5cb98e9 100644
--- a/src/com/google/android/iwlan/IwlanEventListener.java
+++ b/src/com/google/android/iwlan/IwlanEventListener.java
@@ -85,6 +85,16 @@
     /** On Cellinfo changed */
     public static final int CELLINFO_CHANGED_EVENT = 11;
 
+    /* Events used and handled by IwlanDataService internally */
+    public static final int DATA_SERVICE_INTERNAL_EVENT_BASE = 100;
+
+    public static final int DATA_SERVICE_INTERNAL_EVENT_END = 200;
+
+    /* Events used and handled by IwlanNetworkService internally */
+    public static final int NETWORK_SERVICE_INTERNAL_EVENT_BASE = 200;
+
+    public static final int NETWORK_SERVICE_INTERNAL_EVENT_END = 300;
+
     @IntDef({
         CARRIER_CONFIG_CHANGED_EVENT,
         WIFI_DISABLE_EVENT,
@@ -152,11 +162,16 @@
         }
     }
 
-    /** Returns IwlanEventListener instance */
+    /** c 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.
      *
@@ -485,7 +500,7 @@
         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();
             }
         }
     }
@@ -494,7 +509,7 @@
         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();
             }
         }
     }
diff --git a/src/com/google/android/iwlan/IwlanNetworkService.java b/src/com/google/android/iwlan/IwlanNetworkService.java
index 6adbaff..361eaa1 100644
--- a/src/com/google/android/iwlan/IwlanNetworkService.java
+++ b/src/com/google/android/iwlan/IwlanNetworkService.java
@@ -26,6 +26,7 @@
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
@@ -43,16 +44,24 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 public class IwlanNetworkService extends NetworkService {
     private static final String TAG = IwlanNetworkService.class.getSimpleName();
     private 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 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 {
@@ -63,6 +72,7 @@
 
     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
@@ -133,7 +143,7 @@
          */
         @Override
         public void onSubscriptionsChanged() {
-            for (IwlanNetworkServiceProvider np : sIwlanNetworkServiceProviderList) {
+            for (IwlanNetworkServiceProvider np : sIwlanNetworkServiceProviders.values()) {
                 np.subscriptionChanged();
             }
         }
@@ -144,45 +154,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 +166,19 @@
             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, mIwlanNetworkServiceHandler);
         }
 
         @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());
+            mIwlanNetworkServiceHandler.sendMessage(
+                    mIwlanNetworkServiceHandler.obtainMessage(
+                            EVENT_NETWORK_REGISTRATION_INFO_REQUEST,
+                            new NetworkRegistrationInfoRequestData(domain, callback, this)));
         }
 
         /**
@@ -252,8 +189,8 @@
         @Override
         public void close() {
             mIwlanNetworkService.removeNetworkServiceProvider(this);
-            IwlanEventListener.getInstance(mContext, getSlotIndex()).removeEventListener(mHandler);
-            mHandlerThread.quit();
+            IwlanEventListener.getInstance(mContext, getSlotIndex())
+                    .removeEventListener(mIwlanNetworkServiceHandler);
         }
 
         @VisibleForTesting
@@ -277,6 +214,126 @@
         }
     }
 
+    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;
+
+            switch (msg.what) {
+                case IwlanEventListener.CROSS_SIM_CALLING_ENABLE_EVENT:
+                    iwlanNetworkServiceProvider = getNetworkServiceProvider(msg.arg1);
+                    iwlanNetworkServiceProvider.notifyNetworkRegistrationInfoChanged();
+                    break;
+
+                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(
+                                    Arrays.asList(NetworkRegistrationInfo.SERVICE_TYPE_DATA))
+                            .setTransportType(AccessNetworkConstants.TRANSPORT_TYPE_WLAN)
+                            .setEmergencyOnly(!iwlanNetworkServiceProvider.mIsSubActive)
+                            .setDomain(NetworkRegistrationInfo.DOMAIN_PS);
+
+                    if (!IwlanNetworkService.isNetworkConnected(
+                            IwlanHelper.isDefaultDataSlot(
+                                    mContext, iwlanNetworkServiceProvider.getSlotIndex()),
+                            IwlanHelper.isCrossSimCallingEnabled(
+                                    mContext, iwlanNetworkServiceProvider.getSlotIndex()))) {
+                        nriBuilder
+                                .setRegistrationState(
+                                        NetworkRegistrationInfo
+                                                .REGISTRATION_STATE_NOT_REGISTERED_SEARCHING)
+                                .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_UNKNOWN);
+                        Log.d(TAG, "reg state REGISTRATION_STATE_NOT_REGISTERED_SEARCHING");
+                    } else {
+                        nriBuilder
+                                .setRegistrationState(
+                                        NetworkRegistrationInfo.REGISTRATION_STATE_HOME)
+                                .setAccessNetworkTechnology(TelephonyManager.NETWORK_TYPE_IWLAN);
+                        Log.d(TAG, "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;
+                    IwlanNetworkServiceProvider nsp =
+                            sIwlanNetworkServiceProviders.remove(
+                                    iwlanNetworkServiceProvider.getSlotIndex());
+                    if (nsp == null) {
+                        Log.w(
+                                TAG,
+                                "No NetworkServiceProvider exists for slot "
+                                        + iwlanNetworkServiceProvider.getSlotIndex());
+                        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,31 +349,14 @@
 
         // 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");
+        if (mIwlanNetworkServiceHandler == null) {
+            initHandler();
         }
 
         IwlanNetworkServiceProvider np = new IwlanNetworkServiceProvider(slotIndex, this);
-        sIwlanNetworkServiceProviderList.add(np);
+        mIwlanNetworkServiceHandler.sendMessage(
+                mIwlanNetworkServiceHandler.obtainMessage(
+                        EVENT_CREATE_NETWORK_SERVICE_PROVIDER, np));
         return np;
     }
 
@@ -340,28 +380,61 @@
         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);
+    }
+
+    public void removeNetworkServiceProvider(IwlanNetworkServiceProvider np) {
+        mIwlanNetworkServiceHandler.sendMessage(
+                mIwlanNetworkServiceHandler.obtainMessage(
+                        EVENT_REMOVE_NETWORK_SERVICE_PROVIDER, np));
+    }
+
+    void initCallback() {
+        // register for default network callback
+        ConnectivityManager connectivityManager =
+                mContext.getSystemService(ConnectivityManager.class);
+        mNetworkMonitorCallback = new IwlanNetworkMonitorCallback();
+        connectivityManager.registerSystemDefaultNetworkCallback(
+                mNetworkMonitorCallback, mIwlanNetworkServiceHandler);
+        Log.d(TAG, "Registered with Connectivity Service");
+
+        /* register with subscription manager */
+        SubscriptionManager subscriptionManager =
+                mContext.getSystemService(SubscriptionManager.class);
+        mSubsChangeListener = new IwlanOnSubscriptionsChangedListener();
+        subscriptionManager.addOnSubscriptionsChangedListener(
+                new HandlerExecutor(mIwlanNetworkServiceHandler), mSubsChangeListener);
+        Log.d(TAG, "Registered with Subscription Service");
+    }
+
+    void deinitCallback() {
+        // deinit network related stuff
+        ConnectivityManager connectivityManager =
+                mContext.getSystemService(ConnectivityManager.class);
+        connectivityManager.unregisterNetworkCallback(mNetworkMonitorCallback);
+        mNetworkMonitorCallback = null;
+
+        // deinit subscription manager related stuff
+        SubscriptionManager subscriptionManager =
+                mContext.getSystemService(SubscriptionManager.class);
+        subscriptionManager.removeOnSubscriptionsChangedListener(mSubsChangeListener);
+        mSubsChangeListener = null;
+        if (mIwlanNetworkServiceHandlerThread != null) {
+            mIwlanNetworkServiceHandlerThread.quit();
+            mIwlanNetworkServiceHandlerThread = null;
+        }
+        mIwlanNetworkServiceHandler = null;
     }
 
     Context getContext() {
@@ -375,12 +448,36 @@
 
     @VisibleForTesting
     IwlanNetworkServiceProvider getNetworkServiceProvider(int slotIndex) {
-        for (IwlanNetworkServiceProvider np : sIwlanNetworkServiceProviderList) {
-            if (np.getSlotIndex() == slotIndex) {
-                return np;
-            }
+        return sIwlanNetworkServiceProviders.get(slotIndex);
+    }
+
+    @VisibleForTesting
+    void initHandler() {
+        mIwlanNetworkServiceHandler = new IwlanNetworkServiceHandler(getLooper());
+    }
+
+    @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 + ")";
         }
-        return null;
     }
 
     @Override
diff --git a/src/com/google/android/iwlan/epdg/EpdgSelector.java b/src/com/google/android/iwlan/epdg/EpdgSelector.java
index 54b082f..d4f168a 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;
@@ -54,6 +57,8 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.TimeUnit;
 
 public class EpdgSelector {
     private static final String TAG = "EpdgSelector";
@@ -65,6 +70,9 @@
     private int mV6PcoId = -1;
     private byte[] mV4PcoData = null;
     private byte[] mV6PcoData = null;
+    @NonNull private ErrorPolicyManager mErrorPolicyManager;
+
+    private static final long DNS_RESOLVER_TIMEOUT_DURATION_SEC = 5L;
 
     final Comparator<InetAddress> inetAddressComparator =
             new Comparator<InetAddress>() {
@@ -98,6 +106,8 @@
     EpdgSelector(Context context, int slotId) {
         mContext = context;
         mSlotId = slotId;
+
+        mErrorPolicyManager = ErrorPolicyManager.getInstance(mContext, mSlotId);
     }
 
     public static EpdgSelector getSelectorInstance(Context context, int slotId) {
@@ -143,57 +153,81 @@
         mV6PcoData = null;
     }
 
-    private void getIP(
-            String domainName, int filter, ArrayList<InetAddress> validIpList, Network network) {
-        InetAddress[] ipList;
+    private Map.Entry<String, List<InetAddress>> getIP(
+            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);
-                            }
-                            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);
-                }
+        if (InetAddresses.isNumericAddress(domainName)) {
+            try {
+                Log.d(TAG, domainName + " is a numeric ip address");
+                ipList.add(InetAddress.getByName(domainName));
+            } catch (UnknownHostException e) {
+                Log.e(TAG, "Exception when resolving domainName : " + domainName + ".", e);
             }
-        } catch (Exception e) {
-            Log.e(TAG, "Exception when resolving domainName : " + domainName + ".", e);
+        } 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);
+                            }
+
+                            @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);
+                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) {
+                if (Thread.currentThread().interrupted()) {
+                    Thread.currentThread().interrupt();
+                }
+                Log.e(TAG, "InterruptedException: ", e);
+            } catch (TimeoutException e) {
+                Log.e(TAG, "TimeoutException: ", e);
+            }
         }
+
+        List<InetAddress> filteredIpList = new ArrayList<>();
+        // Filter the IP list by input ProtoFilter
+        for (InetAddress ipAddress : ipList) {
+            switch (filter) {
+                case PROTO_FILTER_IPV4:
+                    if (ipAddress instanceof Inet4Address) {
+                        filteredIpList.add(ipAddress);
+                    }
+                    break;
+                case PROTO_FILTER_IPV6:
+                    if (!IwlanHelper.isIpv4EmbeddedIpv6Address(ipAddress)) {
+                        filteredIpList.add(ipAddress);
+                    }
+                    break;
+                case PROTO_FILTER_IPV4V6:
+                    filteredIpList.add(ipAddress);
+                    break;
+                default:
+                    Log.d(TAG, "Invalid ProtoFilter : " + filter);
+            }
+        }
+        validIpList.addAll(filteredIpList);
+
+        return Map.entry(domainName, filteredIpList);
     }
 
     private String[] getPlmnList() {
@@ -270,7 +304,7 @@
         return combinedList.toArray(new String[combinedList.size()]);
     }
 
-    private ArrayList<InetAddress> removeDuplicateIp(ArrayList<InetAddress> validIpList) {
+    private ArrayList<InetAddress> removeDuplicateIp(List<InetAddress> validIpList) {
         ArrayList<InetAddress> resultIpList = new ArrayList<InetAddress>();
 
         for (Iterator<InetAddress> iterator = validIpList.iterator(); iterator.hasNext(); ) {
@@ -304,7 +338,7 @@
     }
 
     private void resolutionMethodStatic(
-            int filter, ArrayList<InetAddress> validIpList, boolean isRoaming, Network network) {
+            int filter, List<InetAddress> validIpList, boolean isRoaming, Network network) {
         String[] domainNames = null;
 
         Log.d(TAG, "STATIC Method");
@@ -359,10 +393,11 @@
         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();
+        Map<String, List<InetAddress>> domainNameToIpAddr = new LinkedHashMap<>();
 
         Log.d(TAG, "PLMN Method");
 
@@ -377,22 +412,35 @@
              * 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");
+                getIP(domainName.toString(), filter, validIpList, network);
+                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);
+            Map.Entry<String, List<InetAddress>> entry =
+                    getIP(domainName.toString(), filter, validIpList, network);
+            domainNameToIpAddr.put(entry.getKey(), entry.getValue());
             domainName.setLength(0);
         }
+        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();
 
@@ -514,7 +562,7 @@
 
     private void lacDomainNameResolution(
             int filter,
-            ArrayList<InetAddress> validIpList,
+            List<InetAddress> validIpList,
             String lacString,
             boolean isEmergency,
             Network network) {
@@ -548,7 +596,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 +634,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 +695,7 @@
 
     private void processNaptrResponse(
             int filter,
-            ArrayList<InetAddress> validIpList,
+            List<InetAddress> validIpList,
             boolean isEmergency,
             Network network,
             boolean isRegisteredWith3GPP,
@@ -699,7 +747,7 @@
     }
 
     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);
@@ -777,7 +825,8 @@
         NaptrDnsResolver.query(network, domainName.toString(), r -> r.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(
@@ -797,6 +846,8 @@
                 Thread.currentThread().interrupt();
             }
             Log.e(TAG, "InterruptedException: ", e);
+        } catch (TimeoutException e) {
+            Log.e(TAG, "TimeoutException: ", e);
         }
     }
 
@@ -807,13 +858,11 @@
             boolean isEmergency,
             @NonNull Network network,
             EpdgSelectorCallback selectorCallback) {
-        ArrayList<InetAddress> validIpList = new ArrayList<InetAddress>();
-        StringBuilder domainName = new StringBuilder();
 
         Runnable doValidation =
                 () -> {
+                    List<InetAddress> validIpList = new ArrayList<>();
                     Log.d(TAG, "Processing request with transactionId: " + transactionId);
-                    String[] plmnList;
 
                     int[] addrResolutionMethods =
                             IwlanHelper.getConfig(
@@ -834,6 +883,7 @@
                         resolutionMethodVisitedCountry(filter, validIpList, isEmergency, network);
                     }
 
+                    Map<String, List<InetAddress>> plmnDomainNamesToIpAddress = null;
                     for (int addrResolutionMethod : addrResolutionMethods) {
                         switch (addrResolutionMethod) {
                             case CarrierConfigManager.Iwlan.EPDG_ADDRESS_STATIC:
@@ -841,7 +891,9 @@
                                 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,6 +914,23 @@
                     }
 
                     if (selectorCallback != null) {
+                        if (mErrorPolicyManager.getMostRecentDataFailCause()
+                                == DataFailCause.IWLAN_CONGESTION) {
+                            int numFqdns = plmnDomainNamesToIpAddress.size();
+                            int index = mErrorPolicyManager.getCurrentFqdnIndex(numFqdns);
+                            if (index >= 0 && index < numFqdns) {
+                                Object[] keys = plmnDomainNamesToIpAddress.keySet().toArray();
+                                validIpList = plmnDomainNamesToIpAddress.get(keys[index]);
+                            } else {
+                                Log.w(
+                                        TAG,
+                                        "CONGESTION error handling- invalid index: "
+                                                + index
+                                                + " number of PLMN FQDNs: "
+                                                + numFqdns);
+                            }
+                        }
+
                         if (!validIpList.isEmpty()) {
                             Collections.sort(validIpList, inetAddressComparator);
                             selectorCallback.onServerListChanged(
diff --git a/src/com/google/android/iwlan/epdg/EpdgTunnelManager.java b/src/com/google/android/iwlan/epdg/EpdgTunnelManager.java
index ea78893..5c87b7a 100644
--- a/src/com/google/android/iwlan/epdg/EpdgTunnelManager.java
+++ b/src/com/google/android/iwlan/epdg/EpdgTunnelManager.java
@@ -625,7 +625,7 @@
     }
 
     /**
-     * Gets a epdg tunnel manager instance.
+     * Gets a EpdgTunnelManager instance.
      *
      * @param context application context
      * @param subId subscription ID for the tunnel
@@ -636,6 +636,11 @@
                 subId, k -> new EpdgTunnelManager(context, subId));
     }
 
+    @VisibleForTesting
+    public static void resetAllInstances() {
+        mTunnelManagerInstances.clear();
+    }
+
     public interface TunnelCallback {
         /**
          * Called when the tunnel is opened.
@@ -989,13 +994,17 @@
 
         Ike3gppParams.Builder builder3gppParams = null;
 
-        String imei = getMobileDeviceIdentity();
-        if (imei != null) {
-            if (builder3gppParams == null) {
-                builder3gppParams = new Ike3gppParams.Builder();
+        // 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);
             }
-            Log.d(TAG, "DEVICE_IDENTITY set in Ike3gppParams");
-            builder3gppParams.setMobileDeviceIdentity(imei);
         }
 
         if (setupRequest.pduSessionId() != 0) {
diff --git a/test/com/google/android/iwlan/ErrorPolicyManagerTest.java b/test/com/google/android/iwlan/ErrorPolicyManagerTest.java
index 3bc624b..aee16f1 100644
--- a/test/com/google/android/iwlan/ErrorPolicyManagerTest.java
+++ b/test/com/google/android/iwlan/ErrorPolicyManagerTest.java
@@ -85,6 +85,7 @@
         doReturn(mockAssetManager).when(mMockContext).getAssets();
         doReturn(is).when(mockAssetManager).open(any());
         setupMockForCarrierConfig(null);
+        ErrorPolicyManager.resetAllInstances();
         mErrorPolicyManager = spy(ErrorPolicyManager.getInstance(mMockContext, DEFAULT_SLOT_INDEX));
     }
 
@@ -232,6 +233,20 @@
         assertEquals(160, time);
         time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
         assertEquals(86400, time);
+
+        iwlanError = buildIwlanIkeProtocolError(9002);
+        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);
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(160, time);
+        time = mErrorPolicyManager.reportIwlanError(apn, iwlanError);
+        assertEquals(86400, time);
     }
 
     @Test
@@ -661,6 +676,85 @@
     }
 
     @Test
+    public void testErrorPolicyWithNumAttemptsPerFqdn() throws Exception {
+        String apn = "ims";
+        String config =
+                "[{"
+                        + "\"ApnName\": \""
+                        + apn
+                        + "\","
+                        + "\"ErrorTypes\": [{"
+                        + getErrorTypeInJSON(
+                                "IKE_PROTOCOL_ERROR_TYPE", /* ErrorType */
+                                new String[] {"15500"}, /* ErrorDetails ("CONGESTION") */
+                                "6", /* NumAttemptsPerFqdn */
+                                new String[] {
+                                    "0", "0", "300", "600", "1200", "0", "0", "0", "300", "600",
+                                    "1200", "-1"
+                                }, /* RetryArray */
+                                new String[] {
+                                    "APM_ENABLE_EVENT",
+                                    "WIFI_DISABLE_EVENT",
+                                    "WIFI_CALLING_DISABLE_EVENT"
+                                }) /* UnthrottlingEvents */
+                        + "}]"
+                        + "}]";
+
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putString(ErrorPolicyManager.KEY_ERROR_POLICY_CONFIG_STRING, config);
+        setupMockForCarrierConfig(bundle);
+        mErrorPolicyManager
+                .mHandler
+                .obtainMessage(IwlanEventListener.CARRIER_CONFIG_CHANGED_EVENT)
+                .sendToTarget();
+
+        sleep(1000);
+        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));
+
+        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 testErrorStats() throws Exception {
         String apn1 = "ims";
         String apn2 = "mms";
@@ -720,6 +814,29 @@
                 + "\"]";
     }
 
+    private String getErrorTypeInJSON(
+            String ErrorType,
+            String[] errorDetails,
+            String numAttemptsPerFqdn,
+            String[] retryArray,
+            String[] unthrottlingEvents) {
+        return "\"ErrorType\": \""
+                + ErrorType
+                + "\","
+                + "\"ErrorDetails\": [\""
+                + String.join("\", \"", errorDetails)
+                + "\"],"
+                + "\"NumAttemptsPerFqdn\": \""
+                + numAttemptsPerFqdn
+                + "\","
+                + "\"RetryArray\": [\""
+                + String.join("\", \"", retryArray)
+                + "\"],"
+                + "\"UnthrottlingEvents\": [\""
+                + String.join("\", \"", unthrottlingEvents)
+                + "\"]";
+    }
+
     private void sleep(long time) {
         try {
             Thread.sleep(time);
diff --git a/test/com/google/android/iwlan/IwlanDataServiceTest.java b/test/com/google/android/iwlan/IwlanDataServiceTest.java
index a0414de..607b751 100644
--- a/test/com/google/android/iwlan/IwlanDataServiceTest.java
+++ b/test/com/google/android/iwlan/IwlanDataServiceTest.java
@@ -16,7 +16,6 @@
 
 package com.google.android.iwlan;
 
-import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
@@ -32,6 +31,7 @@
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.os.test.TestLooper;
 import android.telephony.AccessNetworkConstants.AccessNetworkType;
 import android.telephony.DataFailCause;
 import android.telephony.SubscriptionInfo;
@@ -96,7 +96,6 @@
     @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;
@@ -116,6 +115,7 @@
     private IwlanDataService mIwlanDataService;
     private IwlanDataServiceProvider mIwlanDataServiceProvider;
     private IwlanDataServiceProvider mSpyIwlanDataServiceProvider;
+    private TestLooper mTestLooper = new TestLooper();
 
     private final class IwlanDataServiceCallback extends IDataServiceCallback.Stub {
 
@@ -178,10 +178,6 @@
 
         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);
@@ -214,10 +210,12 @@
         when(mMockIPv6LinkAddress.getAddress()).thenReturn(mMockInet6Address);
 
         mIwlanDataService = spy(new IwlanDataService());
+        doReturn(mTestLooper.getLooper()).when(mIwlanDataService).getLooper();
         mIwlanDataService.setAppContext(mMockContext);
         mIwlanDataServiceProvider =
                 (IwlanDataServiceProvider)
                         mIwlanDataService.onCreateDataServiceProvider(DEFAULT_SLOT_INDEX);
+        mTestLooper.dispatchAll();
         mSpyIwlanDataServiceProvider = spy(mIwlanDataServiceProvider);
     }
 
@@ -225,34 +223,64 @@
     public void cleanUp() throws Exception {
         mStaticMockSession.finishMocking();
         mIwlanDataServiceProvider.close();
+        mTestLooper.dispatchAll();
         if (mIwlanDataService != null) {
             mIwlanDataService.onDestroy();
         }
     }
 
-    @Test
-    public void testWifiOnAvailable() {
-        IwlanNetworkMonitorCallback mNetworkMonitorCallback =
+    private void verifiyNetworkConnected(int transportType) {
+        NetworkCapabilities mockNetworkCapabilities = mock(NetworkCapabilities.class);
+
+        when(mockNetworkCapabilities.hasTransport(anyInt())).thenReturn(false);
+        when(mockNetworkCapabilities.hasTransport(eq(transportType))).thenReturn(true);
+
+        IwlanNetworkMonitorCallback networkMonitorCallback =
                 mIwlanDataService.getNetworkMonitorCallback();
+        networkMonitorCallback.onCapabilitiesChanged(mMockNetwork, mockNetworkCapabilities);
+    }
 
-        mNetworkMonitorCallback.onAvailable(mMockNetwork);
-        boolean ret = mIwlanDataService.isNetworkConnected(true, false);
+    private void verifiyNetworkLost(int transportType) {
+        IwlanNetworkMonitorCallback networkMonitorCallback =
+                mIwlanDataService.getNetworkMonitorCallback();
+        networkMonitorCallback.onLost(mMockNetwork);
+    }
 
-        assertTrue(ret);
+    @Test
+    public void testWifionConnected() {
+        verifiyNetworkConnected(TRANSPORT_WIFI);
+        assertTrue(mIwlanDataService.isNetworkConnected(false, false));
     }
 
     @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);
+        verifiyNetworkLost(TRANSPORT_WIFI);
+        assertFalse(mIwlanDataService.isNetworkConnected(false, false));
         verify(mMockIwlanDataServiceProvider).forceCloseTunnelsInDeactivatingState();
         mIwlanDataService.removeDataServiceProvider(mMockIwlanDataServiceProvider);
+        mTestLooper.dispatchAll();
+    }
+
+    @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();
+        assertEquals(mIwlanDataService.mIwlanDataServiceHandler, null);
+        mIwlanDataService.onCreateDataServiceProvider(DEFAULT_SLOT_INDEX);
+        mTestLooper.dispatchAll();
     }
 
     @Test
@@ -275,6 +303,7 @@
                 false,
                 1);
         mIwlanDataServiceProvider.requestDataCallList(new DataServiceCallback(callback));
+        mTestLooper.dispatchAll();
         latch.await(1, TimeUnit.SECONDS);
 
         assertEquals(mResultCode, DataServiceCallback.RESULT_SUCCESS);
@@ -319,6 +348,7 @@
         latch = new CountDownLatch(1);
         IwlanDataServiceCallback callback = new IwlanDataServiceCallback("requestDataCallList");
         mIwlanDataServiceProvider.requestDataCallList(new DataServiceCallback(callback));
+        mTestLooper.dispatchAll();
         latch.await(1, TimeUnit.SECONDS);
 
         assertEquals(mResultCode, DataServiceCallback.RESULT_SUCCESS);
@@ -339,6 +369,7 @@
                 null, /* trafficDescriptor */
                 true, /* matchAllRuleAllowed */
                 mMockDataServiceCallback);
+        mTestLooper.dispatchAll();
 
         verify(mMockDataServiceCallback, timeout(1000).times(1))
                 .onSetupDataCallComplete(
@@ -365,10 +396,12 @@
                 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
@@ -377,6 +410,7 @@
                 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));
@@ -403,6 +437,7 @@
                 null, /* trafficDescriptor */
                 true, /* matchAllRuleAllowed */
                 mMockDataServiceCallback);
+        mTestLooper.dispatchAll();
 
         /* Check bringUpTunnel() is called. */
         verify(mMockEpdgTunnelManager, times(1))
@@ -412,6 +447,7 @@
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onOpened(TEST_APN_NAME, mMockTunnelLinkProperties);
+        mTestLooper.dispatchAll();
         verify(mMockDataServiceCallback, times(1))
                 .onSetupDataCallComplete(
                         eq(DataServiceCallback.RESULT_SUCCESS), any(DataCallResponse.class));
@@ -438,6 +474,7 @@
                 null, /* trafficDescriptor */
                 true, /* matchAllRuleAllowed */
                 mMockDataServiceCallback);
+        mTestLooper.dispatchAll();
 
         /* Check bringUpTunnel() is called. */
         verify(mMockEpdgTunnelManager, times(1))
@@ -450,6 +487,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());
@@ -473,6 +511,7 @@
                 TEST_APN_NAME.hashCode() /* cid: hashcode() of "ims" */,
                 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());
@@ -481,6 +520,7 @@
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.NO_ERROR));
+        mTestLooper.dispatchAll();
         verify(mMockDataServiceCallback, times(1))
                 .onDeactivateDataCallComplete(eq(DataServiceCallback.RESULT_SUCCESS));
     }
@@ -507,6 +547,7 @@
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.NO_ERROR));
+        mTestLooper.dispatchAll();
 
         ArgumentCaptor<DataCallResponse> dataCallResponseCaptor =
                 ArgumentCaptor.forClass(DataCallResponse.class);
@@ -545,6 +586,7 @@
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.NO_ERROR));
+        mTestLooper.dispatchAll();
 
         ArgumentCaptor<DataCallResponse> dataCallResponseCaptor =
                 ArgumentCaptor.forClass(DataCallResponse.class);
@@ -574,17 +616,22 @@
         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);
 
@@ -673,10 +720,12 @@
                 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
@@ -714,6 +763,7 @@
                 null, /* trafficDescriptor */
                 true, /* matchAllRuleAllowed */
                 mMockDataServiceCallback);
+        mTestLooper.dispatchAll();
 
         /* Check bringUpTunnel() is called. */
         verify(mMockEpdgTunnelManager, times(1))
@@ -723,6 +773,7 @@
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onOpened(TEST_APN_NAME, mMockTunnelLinkProperties);
+        mTestLooper.dispatchAll();
         verify(mMockDataServiceCallback, times(1))
                 .onSetupDataCallComplete(
                         eq(DataServiceCallback.RESULT_SUCCESS), any(DataCallResponse.class));
@@ -836,6 +887,7 @@
         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));
@@ -857,12 +909,14 @@
         doReturn(true)
                 .when(mMockEpdgTunnelManager)
                 .bringUpTunnel(any(TunnelSetupRequest.class), any(IwlanTunnelCallback.class));
+        mTestLooper.dispatchAll();
 
         sleep(sleepTime);
 
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onOpened(TEST_APN_NAME, mMockTunnelLinkProperties);
+        mTestLooper.dispatchAll();
         verify(mMockDataServiceCallback, atLeastOnce())
                 .onSetupDataCallComplete(
                         eq(DataServiceCallback.RESULT_SUCCESS), any(DataCallResponse.class));
@@ -872,6 +926,7 @@
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.IKE_INTERNAL_IO_EXCEPTION));
+        mTestLooper.dispatchAll();
     }
 
     private void mockDeactivateTunnelDown(long sleepTime) {
@@ -879,6 +934,7 @@
                 TEST_APN_NAME.hashCode() /* cid: hashcode() of "ims" */,
                 DataService.REQUEST_REASON_NORMAL /* DataService.REQUEST_REASON_NORMAL */,
                 mMockDataServiceCallback);
+        mTestLooper.dispatchAll();
         verify(mMockEpdgTunnelManager, atLeastOnce()).closeTunnel(eq(TEST_APN_NAME), anyBoolean());
 
         sleep(sleepTime);
@@ -886,6 +942,7 @@
         mSpyIwlanDataServiceProvider
                 .getIwlanTunnelCallback()
                 .onClosed(TEST_APN_NAME, new IwlanError(IwlanError.NO_ERROR));
+        mTestLooper.dispatchAll();
         verify(mMockDataServiceCallback, atLeastOnce())
                 .onDeactivateDataCallComplete(eq(DataServiceCallback.RESULT_SUCCESS));
     }
diff --git a/test/com/google/android/iwlan/IwlanEventListenerTest.java b/test/com/google/android/iwlan/IwlanEventListenerTest.java
index 789ab25..4e127ca 100644
--- a/test/com/google/android/iwlan/IwlanEventListenerTest.java
+++ b/test/com/google/android/iwlan/IwlanEventListenerTest.java
@@ -93,15 +93,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 +110,7 @@
         when(mMockTelephonyManager.createForSubscriptionId(eq(0)))
                 .thenReturn(mMockTelephonyManager);
 
+        IwlanEventListener.resetAllInstances();
         mIwlanEventListener = IwlanEventListener.getInstance(mMockContext, DEFAULT_SLOT_INDEX);
     }
 
@@ -121,7 +121,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 +143,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 +167,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 +191,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 +231,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 +262,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>();
diff --git a/test/com/google/android/iwlan/epdg/EpdgSelectorTest.java b/test/com/google/android/iwlan/epdg/EpdgSelectorTest.java
index 9a4af93..804e892 100644
--- a/test/com/google/android/iwlan/epdg/EpdgSelectorTest.java
+++ b/test/com/google/android/iwlan/epdg/EpdgSelectorTest.java
@@ -49,6 +49,7 @@
 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 +76,9 @@
     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_IPV6_ADDRESS = "0000:0000:0000:0000:0000:0000:0000:0001";
 
     private static int testPcoIdIPv6 = 0xFF01;
@@ -86,6 +90,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,8 +113,14 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mStaticMockSession = mockitoSession().mockStatic(DnsResolver.class).startMocking();
+        mStaticMockSession =
+                mockitoSession()
+                        .mockStatic(DnsResolver.class)
+                        .mockStatic(ErrorPolicyManager.class)
+                        .startMocking();
 
+        when(ErrorPolicyManager.getInstance(mMockContext, DEFAULT_SLOT_INDEX))
+                .thenReturn(mMockErrorPolicyManager);
         mEpdgSelector = new EpdgSelector(mMockContext, DEFAULT_SLOT_INDEX);
 
         when(mMockContext.getSystemService(eq(SubscriptionManager.class)))
@@ -229,18 +240,9 @@
     }
 
     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";
+        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,
@@ -249,17 +251,33 @@
                 CarrierConfigManager.Iwlan.KEY_MCC_MNCS_STRING_ARRAY,
                 new String[] {"310-480", "300-120", "311-120"});
 
-        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(expectedFqdnFromHplmn, new String[] {TEST_IP_ADDRESS}, TYPE_A);
+        mFakeDns.setAnswer(expectedFqdnFromEHplmn, new String[] {TEST_IP_ADDRESS_1}, TYPE_A);
+        mFakeDns.setAnswer(expectedFqdnFromConfig, new String[] {TEST_IP_ADDRESS_2}, TYPE_A);
+        mFakeDns.setAnswer(
+                "sos." + expectedFqdnFromHplmn, new String[] {TEST_IP_ADDRESS_3}, TYPE_A);
+        mFakeDns.setAnswer(
+                "sos." + expectedFqdnFromEHplmn, new String[] {TEST_IP_ADDRESS_4}, TYPE_A);
+        mFakeDns.setAnswer(
+                "sos." + expectedFqdnFromConfig, new String[] {TEST_IP_ADDRESS_5}, 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_3), testInetAddresses.get(0));
+            assertEquals(InetAddress.getByName(TEST_IP_ADDRESS), testInetAddresses.get(1));
+            assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_4), testInetAddresses.get(2));
+            assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_1), testInetAddresses.get(3));
+            assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_5), testInetAddresses.get(4));
+            assertEquals(InetAddress.getByName(TEST_IP_ADDRESS_2), testInetAddresses.get(5));
+        } else {
+            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));
+        }
     }
 
     @Test
@@ -298,7 +316,7 @@
                 mEpdgSelector.getValidatedServerList(
                         1234,
                         EpdgSelector.PROTO_FILTER_IPV4V6,
-                        false /*isRoaming*/,
+                        false /* isRoaming */,
                         isEmergency,
                         mMockNetwork,
                         new EpdgSelector.EpdgSelectorCallback() {
@@ -492,7 +510,7 @@
             // 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;
             }
         }
 
diff --git a/test/com/google/android/iwlan/epdg/EpdgTunnelManagerTest.java b/test/com/google/android/iwlan/epdg/EpdgTunnelManagerTest.java
index f2cba73..6277f3f 100644
--- a/test/com/google/android/iwlan/epdg/EpdgTunnelManagerTest.java
+++ b/test/com/google/android/iwlan/epdg/EpdgTunnelManagerTest.java
@@ -63,6 +63,7 @@
 import com.google.android.iwlan.IwlanError;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -150,10 +151,20 @@
     @Mock IpSecTransform mMockedIpSecTransformOut;
     @Mock LinkProperties mMockLinkProperties;
 
-    ArgumentCaptor<ChildSessionCallback> mChildSessionCallbackCaptor;
+    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);
@@ -162,7 +173,7 @@
         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(
@@ -192,8 +203,6 @@
 
         when(mMockIkeSessionConnectionInfo.getNetwork()).thenReturn(mMockNetwork);
 
-        mChildSessionCallbackCaptor = ArgumentCaptor.forClass(ChildSessionCallback.class);
-
         doReturn(EXPECTED_LOCAL_ADDRESSES)
                 .when(mEpdgTunnelManager)
                 .getAddressForNetwork(any(), any());
@@ -339,6 +348,7 @@
     }
 
     @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 +380,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 +410,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);
@@ -465,6 +477,32 @@
     }
 
     @Test
+    public void testInitialContactForFirstTunnelOnly() throws Exception {
+        final String firstApnName = "ims";
+        final String secondApnName = "mms";
+
+        IkeSessionArgumentCaptors firstTunnelArgumentCaptors =
+                verifyBringUpTunnelWithDnsQuery(firstApnName, null);
+        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";
 
@@ -1094,9 +1132,10 @@
         mEpdgTunnelManager.setIsEpdgAddressSelected(true);
     }
 
-    private ChildSessionCallback verifyBringUpTunnelWithDnsQuery(
+    private IkeSessionArgumentCaptors verifyBringUpTunnelWithDnsQuery(
             String apnName, IkeSession ikeSession) {
         reset(mMockIwlanTunnelCallback);
+        IkeSessionArgumentCaptors ikeSessionArgumentCaptors = new IkeSessionArgumentCaptors();
 
         verifyBringUpTunnel(apnName, true /* needPendingBringUpReq */);
 
@@ -1104,11 +1143,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,28 +1156,29 @@
         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));
 
@@ -1152,17 +1192,13 @@
         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();
-    }
-
-    private void verifyTunnelOnOpened(String apnName) {
-        verifyTunnelOnOpened(apnName, mChildSessionCallbackCaptor.getValue());
+        return ikeSessionArgumentCaptors;
     }
 
     private void verifyTunnelOnOpened(String apnName, ChildSessionCallback childSessionCallback) {
@@ -1187,8 +1223,11 @@
 
         setOneTunnelOpened(openedApnName);
 
-        verifyBringUpTunnel(toBeOpenedApnName, false /* needPendingBringUpReq */);
-        verifyTunnelOnOpened(toBeOpenedApnName);
+        IkeSessionArgumentCaptors ikeSessionArgumentCaptors =
+                verifyBringUpTunnel(toBeOpenedApnName, false /* needPendingBringUpReq */);
+        ChildSessionCallback childSessionCallback =
+                ikeSessionArgumentCaptors.mChildSessionCallbackCaptor.getValue();
+        verifyTunnelOnOpened(toBeOpenedApnName, childSessionCallback);
     }
 
     @Test
@@ -1196,10 +1235,18 @@
         final String firstApnName = "ims";
         final String secondApnName = "mms";
 
-        ChildSessionCallback firstCallback = verifyBringUpTunnelWithDnsQuery(firstApnName, null);
-        verifyBringUpTunnel(secondApnName, true /* needPendingBringUpReq */);
+        IkeSessionArgumentCaptors firstTunnelArgumentCaptors =
+                verifyBringUpTunnelWithDnsQuery(firstApnName, null);
+        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
@@ -1286,8 +1333,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,8 +1349,10 @@
         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);
 
@@ -1319,8 +1370,10 @@
         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);