/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.android.iwlan.epdg;

import static android.net.ipsec.ike.ike3gpp.Ike3gppData.DATA_TYPE_NOTIFY_BACKOFF_TIMER;
import static android.net.ipsec.ike.ike3gpp.Ike3gppData.DATA_TYPE_NOTIFY_N1_MODE_INFORMATION;
import static android.net.ipsec.ike.ike3gpp.Ike3gppParams.PDU_SESSION_ID_UNSET;
import static android.system.OsConstants.AF_INET;
import static android.system.OsConstants.AF_INET6;
import static android.telephony.PreciseDataConnectionState.NetworkValidationStatus;

import static com.google.android.iwlan.proto.MetricsAtom.*;

import android.content.Context;
import android.net.ConnectivityDiagnosticsManager;
import android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback;
import android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
import android.net.ConnectivityManager;
import android.net.InetAddresses;
import android.net.IpPrefix;
import android.net.IpSecManager;
import android.net.IpSecTransform;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.eap.EapAkaInfo;
import android.net.eap.EapInfo;
import android.net.eap.EapSessionConfig;
import android.net.ipsec.ike.ChildSessionCallback;
import android.net.ipsec.ike.ChildSessionConfiguration;
import android.net.ipsec.ike.ChildSessionParams;
import android.net.ipsec.ike.IkeFqdnIdentification;
import android.net.ipsec.ike.IkeIdentification;
import android.net.ipsec.ike.IkeKeyIdIdentification;
import android.net.ipsec.ike.IkeRfc822AddrIdentification;
import android.net.ipsec.ike.IkeSession;
import android.net.ipsec.ike.IkeSessionCallback;
import android.net.ipsec.ike.IkeSessionConfiguration;
import android.net.ipsec.ike.IkeSessionConnectionInfo;
import android.net.ipsec.ike.IkeSessionParams;
import android.net.ipsec.ike.IkeTrafficSelector;
import android.net.ipsec.ike.SaProposal;
import android.net.ipsec.ike.TunnelModeChildSessionParams;
import android.net.ipsec.ike.exceptions.IkeException;
import android.net.ipsec.ike.exceptions.IkeIOException;
import android.net.ipsec.ike.exceptions.IkeProtocolException;
import android.net.ipsec.ike.ike3gpp.Ike3gppBackoffTimer;
import android.net.ipsec.ike.ike3gpp.Ike3gppData;
import android.net.ipsec.ike.ike3gpp.Ike3gppExtension;
import android.net.ipsec.ike.ike3gpp.Ike3gppN1ModeInformation;
import android.net.ipsec.ike.ike3gpp.Ike3gppParams;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.telephony.CarrierConfigManager;
import android.telephony.PreciseDataConnectionState;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.data.ApnSetting;
import android.telephony.data.NetworkSliceInfo;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;

import com.google.android.iwlan.ErrorPolicyManager;
import com.google.android.iwlan.IwlanCarrierConfig;
import com.google.android.iwlan.IwlanError;
import com.google.android.iwlan.IwlanHelper;
import com.google.android.iwlan.IwlanStatsLog;
import com.google.android.iwlan.TunnelMetricsInterface.OnClosedMetrics;
import com.google.android.iwlan.TunnelMetricsInterface.OnOpenedMetrics;
import com.google.android.iwlan.exceptions.IwlanSimNotReadyException;
import com.google.android.iwlan.flags.FeatureFlags;
import com.google.android.iwlan.flags.FeatureFlagsImpl;
import com.google.android.iwlan.proto.MetricsAtom;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class EpdgTunnelManager {
    private final FeatureFlags mFeatureFlags;
    private final Context mContext;
    private final int mSlotId;
    private Handler mHandler;
    private ConnectivityDiagnosticsCallback mConnectivityDiagnosticsCallback;

    private static final int EVENT_TUNNEL_BRINGUP_REQUEST = 0;
    private static final int EVENT_TUNNEL_BRINGDOWN_REQUEST = 1;
    private static final int EVENT_CHILD_SESSION_OPENED = 2;
    private static final int EVENT_CHILD_SESSION_CLOSED = 3;
    private static final int EVENT_IKE_SESSION_CLOSED = 5;
    private static final int EVENT_EPDG_ADDRESS_SELECTION_REQUEST_COMPLETE = 6;
    private static final int EVENT_IPSEC_TRANSFORM_CREATED = 7;
    private static final int EVENT_IPSEC_TRANSFORM_DELETED = 8;
    private static final int EVENT_UPDATE_NETWORK = 9;
    private static final int EVENT_IKE_SESSION_OPENED = 10;
    private static final int EVENT_IKE_SESSION_CONNECTION_INFO_CHANGED = 11;
    private static final int EVENT_IKE_3GPP_DATA_RECEIVED = 12;
    private static final int EVENT_IKE_LIVENESS_STATUS_CHANGED = 13;
    private static final int EVENT_REQUEST_NETWORK_VALIDATION_CHECK = 14;
    private static final int IKE_HARD_LIFETIME_SEC_MINIMUM = 300;
    private static final int IKE_HARD_LIFETIME_SEC_MAXIMUM = 86400;
    private static final int IKE_SOFT_LIFETIME_SEC_MINIMUM = 120;
    private static final int CHILD_HARD_LIFETIME_SEC_MINIMUM = 300;
    private static final int CHILD_HARD_LIFETIME_SEC_MAXIMUM = 14400;
    private static final int CHILD_SOFT_LIFETIME_SEC_MINIMUM = 120;
    private static final int LIFETIME_MARGIN_SEC_MINIMUM = (int) TimeUnit.MINUTES.toSeconds(1L);
    private static final int IKE_RETRANS_TIMEOUT_MS_MIN = 500;

    private static final int IKE_RETRANS_TIMEOUT_MS_MAX = (int) TimeUnit.MINUTES.toMillis(30L);

    private static final int IKE_RETRANS_MAX_ATTEMPTS_MAX = 10;
    private static final int IKE_DPD_DELAY_SEC_MIN = 20;
    private static final int IKE_DPD_DELAY_SEC_MAX = 1800; // 30 minutes
    private static final int NATT_KEEPALIVE_DELAY_SEC_MIN = 10;
    private static final int NATT_KEEPALIVE_DELAY_SEC_MAX = 120;

    private static final int DEVICE_IMEI_LEN = 15;
    private static final int DEVICE_IMEISV_SUFFIX_LEN = 2;

    private static final int TRAFFIC_SELECTOR_START_PORT = 0;
    private static final int TRAFFIC_SELECTOR_END_PORT = 65535;
    private static final String TRAFFIC_SELECTOR_IPV4_START_ADDR = "0.0.0.0";
    private static final String TRAFFIC_SELECTOR_IPV4_END_ADDR = "255.255.255.255";
    private static final String TRAFFIC_SELECTOR_IPV6_START_ADDR = "::";
    private static final String TRAFFIC_SELECTOR_IPV6_END_ADDR =
            "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff";

    private static final int NETWORK_VALIDATION_MIN_INTERVAL_MS = 10000;

    private static long sLastUnderlyingNetworkValidationMs = 0;
    private static final Object sLastUnderlyingNetworkValidationLock = new Object();

    // "192.0.2.0" is selected from RFC5737, "IPv4 Address Blocks Reserved for Documentation"
    private static final InetAddress DUMMY_ADDR = InetAddresses.parseNumericAddress("192.0.2.0");

    private static final Map<Integer, EpdgTunnelManager> mTunnelManagerInstances =
            new ConcurrentHashMap<>();
    private final Map<Network, MetricsAtom> mMetricsAtomForNetwork = new ConcurrentHashMap<>();

    private final Queue<TunnelRequestWrapper> mPendingBringUpRequests = new ArrayDeque<>();

    // The most recently updated system default network as seen by IwlanDataService.
    @Nullable private Network mDefaultNetwork;
    // The latest Network provided to the IKE session. Only for debugging purposes.
    @Nullable private Network mIkeSessionNetwork;

    private int mTransactionId = 0;
    private boolean mHasConnectedToEpdg;
    private final IkeSessionCreator mIkeSessionCreator;
    private final IpSecManager mIpSecManager;
    private final EpdgSelector mEpdgSelector;

    private final Map<String, TunnelConfig> mApnNameToTunnelConfig = new ConcurrentHashMap<>();
    private final Map<String, IpsecTransformData> mApnNameToIpsecTransform =
            new ConcurrentHashMap<>();
    private final Map<String, Integer> mApnNameToCurrentToken = new ConcurrentHashMap<>();

    private final String TAG;

    @Nullable private byte[] mNextReauthId = null;
    private long mEpdgServerSelectionDuration = 0;
    private long mEpdgServerSelectionStartTime = 0;
    private long mIkeTunnelEstablishmentStartTime = 0;

    private static final Set<Integer> VALID_ENCRYPTION_ALGOS;
    private static final Set<Integer> VALID_AEAD_ALGOS;

    private static final String CONFIG_TYPE_ENCRYPT_ALGO = "encryption algorithm";

    static {
        VALID_ENCRYPTION_ALGOS =
                Set.of(
                        SaProposal.ENCRYPTION_ALGORITHM_AES_CBC,
                        SaProposal.ENCRYPTION_ALGORITHM_AES_CTR);

        VALID_AEAD_ALGOS =
                Set.of(
                        SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8,
                        SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12,
                        SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_16);
    }

    @VisibleForTesting protected EpdgMonitor mEpdgMonitor = new EpdgMonitor();

    public static final int BRINGDOWN_REASON_UNKNOWN = 0;
    public static final int BRINGDOWN_REASON_DISABLE_N1_MODE = 1;
    public static final int BRINGDOWN_REASON_ENABLE_N1_MODE = 2;
    public static final int BRINGDOWN_REASON_SERVICE_OUT_OF_SYNC = 3;
    public static final int BRINGDOWN_REASON_IN_DEACTIVATING_STATE = 4;
    public static final int BRINGDOWN_REASON_NETWORK_UPDATE_WHEN_TUNNEL_IN_BRINGUP = 5;
    public static final int BRINGDOWN_REASON_DEACTIVATE_DATA_CALL = 6;

    @IntDef({
        BRINGDOWN_REASON_UNKNOWN,
        BRINGDOWN_REASON_DISABLE_N1_MODE,
        BRINGDOWN_REASON_ENABLE_N1_MODE,
        BRINGDOWN_REASON_SERVICE_OUT_OF_SYNC,
        BRINGDOWN_REASON_IN_DEACTIVATING_STATE,
        BRINGDOWN_REASON_NETWORK_UPDATE_WHEN_TUNNEL_IN_BRINGUP,
        BRINGDOWN_REASON_DEACTIVATE_DATA_CALL,
    })
    public @interface TunnelBringDownReason {}

    private static String bringdownReasonToString(@TunnelBringDownReason int reason) {
        return switch (reason) {
            case BRINGDOWN_REASON_UNKNOWN -> "BRINGDOWN_REASON_UNKNOWN";
            case BRINGDOWN_REASON_DISABLE_N1_MODE -> "BRINGDOWN_REASON_DISABLE_N1_MODE";
            case BRINGDOWN_REASON_ENABLE_N1_MODE -> "BRINGDOWN_REASON_ENABLE_N1_MODE";
            case BRINGDOWN_REASON_SERVICE_OUT_OF_SYNC -> "BRINGDOWN_REASON_SERVICE_OUT_OF_SYNC";
            case BRINGDOWN_REASON_IN_DEACTIVATING_STATE -> "BRINGDOWN_REASON_IN_DEACTIVATING_STATE";
            case BRINGDOWN_REASON_NETWORK_UPDATE_WHEN_TUNNEL_IN_BRINGUP ->
                    "BRINGDOWN_REASON_NETWORK_UPDATE_WHEN_TUNNEL_IN_BRINGUP";
            case BRINGDOWN_REASON_DEACTIVATE_DATA_CALL -> "BRINGDOWN_REASON_DEACTIVATE_DATA_CALL";
            default -> "Unknown(" + reason + ")";
        };
    }

    private final EpdgSelector.EpdgSelectorCallback mSelectorCallback =
            new EpdgSelector.EpdgSelectorCallback() {
                @Override
                public void onServerListChanged(int transactionId, List<InetAddress> validIPList) {
                    sendSelectionRequestComplete(
                            validIPList, new IwlanError(IwlanError.NO_ERROR), transactionId);
                }

                @Override
                public void onError(int transactionId, IwlanError epdgSelectorError) {
                    sendSelectionRequestComplete(null, epdgSelectorError, transactionId);
                }
            };

    @VisibleForTesting
    class TunnelConfig {
        @NonNull final EpdgTunnelCallback mEpdgTunnelCallback;
        // TODO: Change this to TunnelLinkProperties after removing autovalue
        private List<InetAddress> mPcscfAddrList;
        private List<InetAddress> mDnsAddrList;
        private List<LinkAddress> mInternalAddrList;

        private final InetAddress mSrcIpv6Address;
        private final int mSrcIpv6AddressPrefixLen;
        private NetworkSliceInfo mSliceInfo;
        private boolean mIsBackoffTimeValid = false;
        private long mBackoffTime;

        @NonNull final IkeSession mIkeSession;

        IwlanError mError;
        private final IpSecManager.IpSecTunnelInterface mIface;
        private IkeSessionState mIkeSessionState;
        private final boolean mIsEmergency;
        private final InetAddress mEpdgAddress;
        private Network mUnderlyingNetwork;

        public TunnelConfig(
                IkeSession ikeSession,
                EpdgTunnelCallback tunnelCallback,
                IpSecManager.IpSecTunnelInterface iface,
                InetAddress srcIpv6Addr,
                int srcIpv6PrefixLength,
                boolean isEmergency,
                InetAddress epdgAddress,
                Network underlyingNetwork) {
            mEpdgTunnelCallback = tunnelCallback;
            mIkeSession = ikeSession;
            mError = new IwlanError(IwlanError.NO_ERROR);
            mSrcIpv6Address = srcIpv6Addr;
            mSrcIpv6AddressPrefixLen = srcIpv6PrefixLength;
            mIface = iface;
            setIkeSessionState(IkeSessionState.IKE_SESSION_INIT_IN_PROGRESS);
            mIsEmergency = isEmergency;
            mEpdgAddress = epdgAddress;
            mUnderlyingNetwork = underlyingNetwork;
        }

        IkeSessionState getIkeSessionState() {
            return mIkeSessionState;
        }

        void setIkeSessionState(IkeSessionState ikeSessionState) {
            mIkeSessionState = ikeSessionState;
        }

        NetworkSliceInfo getSliceInfo() {
            return mSliceInfo;
        }

        void setSliceInfo(NetworkSliceInfo si) {
            mSliceInfo = si;
        }

        boolean isBackoffTimeValid() {
            return mIsBackoffTimeValid;
        }

        long getBackoffTime() {
            return mBackoffTime;
        }

        void setBackoffTime(long backoffTime) {
            mIsBackoffTimeValid = true;
            mBackoffTime = backoffTime;
        }

        @NonNull
        EpdgTunnelCallback getEpdgTunnelCallback() {
            return mEpdgTunnelCallback;
        }

        List<InetAddress> getPcscfAddrList() {
            return mPcscfAddrList;
        }

        void setPcscfAddrList(List<InetAddress> pcscfAddrList) {
            mPcscfAddrList = pcscfAddrList;
        }

        List<InetAddress> getDnsAddrList() {
            return mDnsAddrList;
        }

        void setDnsAddrList(List<InetAddress> dnsAddrList) {
            this.mDnsAddrList = dnsAddrList;
        }

        List<LinkAddress> getInternalAddrList() {
            return mInternalAddrList;
        }

        boolean isPrefixSameAsSrcIP(LinkAddress laddr) {
            if (laddr.isIpv6() && (laddr.getPrefixLength() == mSrcIpv6AddressPrefixLen)) {
                IpPrefix assignedPrefix = new IpPrefix(laddr.getAddress(), laddr.getPrefixLength());
                IpPrefix srcPrefix = new IpPrefix(mSrcIpv6Address, mSrcIpv6AddressPrefixLen);
                return assignedPrefix.equals(srcPrefix);
            }
            return false;
        }

        void setInternalAddrList(List<LinkAddress> internalAddrList) {
            mInternalAddrList = new ArrayList<LinkAddress>(internalAddrList);
            if (getSrcIpv6Address() != null) {
                // check if we can reuse src ipv6 address (i.e. if prefix is same)
                for (LinkAddress assignedAddr : internalAddrList) {
                    if (isPrefixSameAsSrcIP(assignedAddr)) {
                        // the assigned IPv6 address is same as pre-Handover IPv6
                        // addr. Just reuse the pre-Handover Address so the IID is
                        // preserved
                        mInternalAddrList.remove(assignedAddr);

                        // add original address
                        mInternalAddrList.add(
                                new LinkAddress(mSrcIpv6Address, mSrcIpv6AddressPrefixLen));

                        Log.d(
                                TAG,
                                "Network assigned IP replaced OLD:"
                                        + internalAddrList
                                        + " NEW:"
                                        + mInternalAddrList);
                        break;
                    }
                }
            }
        }

        @NonNull
        IkeSession getIkeSession() {
            return mIkeSession;
        }

        IwlanError getError() {
            return mError;
        }

        void setError(IwlanError error) {
            this.mError = error;
        }

        IpSecManager.IpSecTunnelInterface getIface() {
            return mIface;
        }

        InetAddress getSrcIpv6Address() {
            return mSrcIpv6Address;
        }

        boolean isEmergency() {
            return mIsEmergency;
        }

        InetAddress getEpdgAddress() {
            return mEpdgAddress;
        }

        boolean hasTunnelOpened() {
            return mInternalAddrList != null
                    && !mInternalAddrList.isEmpty() /* The child session is opened */
                    && mIface != null; /* The tunnel interface is bring up */
        }

        public void setUnderlyingNetwork(Network network) {
            mUnderlyingNetwork = network;
        }

        public Network getUnderlyingNetwork() {
            return mUnderlyingNetwork;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("TunnelConfig { ");

            if (mSliceInfo != null) {
                sb.append("mSliceInfo: ").append(mSliceInfo).append(", ");
            }

            if (mIsBackoffTimeValid) {
                sb.append("mBackoffTime: ").append(mBackoffTime).append(", ");
            }
            sb.append(" }");
            return sb.toString();
        }
    }

    @VisibleForTesting
    class TmIkeSessionCallback implements IkeSessionCallback {

        private final String mApnName;
        private final int mToken;

        TmIkeSessionCallback(String apnName, int token) {
            this.mApnName = apnName;
            this.mToken = token;
        }

        @Override
        public void onOpened(IkeSessionConfiguration sessionConfiguration) {
            if (mHandler == null) {
                Log.d(TAG, "Handler unavailable");
                return;
            }
            Log.d(TAG, "Ike session opened for apn: " + mApnName + " with token: " + mToken);
            mHandler.obtainMessage(
                            EVENT_IKE_SESSION_OPENED,
                            new IkeSessionOpenedData(mApnName, mToken, sessionConfiguration))
                    .sendToTarget();
        }

        @Override
        public void onClosed() {
            if (mHandler == null) {
                Log.d(TAG, "Handler unavailable");
                return;
            }
            Log.d(TAG, "Ike session closed for apn: " + mApnName + " with token: " + mToken);
            mHandler.obtainMessage(
                            EVENT_IKE_SESSION_CLOSED,
                            new SessionClosedData(mApnName, mToken, null /* ikeException */))
                    .sendToTarget();
        }

        @Override
        public void onClosedWithException(IkeException exception) {
            mNextReauthId = null;
            onSessionClosedWithException(exception, mApnName, mToken, EVENT_IKE_SESSION_CLOSED);
        }

        @Override
        public void onError(IkeProtocolException exception) {
            Log.d(TAG, "Ike session onError for apn: " + mApnName + " with token: " + mToken);

            mNextReauthId = null;

            Log.d(
                    TAG,
                    "ErrorType:"
                            + exception.getErrorType()
                            + " ErrorData:"
                            + exception.getMessage());
        }

        @Override
        public void onIkeSessionConnectionInfoChanged(
                IkeSessionConnectionInfo ikeSessionConnectionInfo) {
            if (mHandler == null) {
                Log.d(TAG, "Handler unavailable");
                return;
            }
            Network network = ikeSessionConnectionInfo.getNetwork();
            Log.d(
                    TAG,
                    "Ike session connection info changed for apn: "
                            + mApnName
                            + " with token: "
                            + mToken
                            + " Network: "
                            + network);
            mHandler.obtainMessage(
                            EVENT_IKE_SESSION_CONNECTION_INFO_CHANGED,
                            new IkeSessionConnectionInfoData(
                                    mApnName, mToken, ikeSessionConnectionInfo))
                    .sendToTarget();
        }

        @Override
        public void onLivenessStatusChanged(int status) {
            if (mHandler == null) {
                Log.d(TAG, "Handler unavailable");
                return;
            }
            Log.d(
                    TAG,
                    "Ike liveness status changed for apn: " + mApnName + " with status: " + status);
            @NetworkValidationStatus
            int validationStatus =
                    switch (status) {
                        case IkeSessionCallback.LIVENESS_STATUS_ON_DEMAND_STARTED,
                                IkeSessionCallback.LIVENESS_STATUS_BACKGROUND_STARTED,
                                IkeSessionCallback.LIVENESS_STATUS_ON_DEMAND_ONGOING,
                                IkeSessionCallback.LIVENESS_STATUS_BACKGROUND_ONGOING ->
                                PreciseDataConnectionState.NETWORK_VALIDATION_IN_PROGRESS;
                        case IkeSessionCallback.LIVENESS_STATUS_SUCCESS ->
                                PreciseDataConnectionState.NETWORK_VALIDATION_SUCCESS;
                        case IkeSessionCallback.LIVENESS_STATUS_FAILURE ->
                                PreciseDataConnectionState.NETWORK_VALIDATION_FAILURE;
                        default -> PreciseDataConnectionState.NETWORK_VALIDATION_SUCCESS;
                    };

            mHandler.obtainMessage(
                            EVENT_IKE_LIVENESS_STATUS_CHANGED,
                            new IkeSessionValidationStatusData(mApnName, mToken, validationStatus))
                    .sendToTarget();
        }
    }

    @VisibleForTesting
    class TmIke3gppCallback implements Ike3gppExtension.Ike3gppDataListener {
        private final String mApnName;
        private final int mToken;

        private TmIke3gppCallback(String apnName, int token) {
            mApnName = apnName;
            mToken = token;
        }

        @Override
        public void onIke3gppDataReceived(List<Ike3gppData> payloads) {
            if (mHandler == null) {
                Log.d(TAG, "Handler unavailable");
                return;
            }
            mHandler.obtainMessage(
                            EVENT_IKE_3GPP_DATA_RECEIVED,
                            new Ike3gppDataReceived(mApnName, mToken, payloads))
                    .sendToTarget();
        }
    }

    @VisibleForTesting
    class TmChildSessionCallback implements ChildSessionCallback {

        private final String mApnName;
        private final int mToken;

        TmChildSessionCallback(String apnName, int token) {
            this.mApnName = apnName;
            this.mToken = token;
        }

        @Override
        public void onOpened(ChildSessionConfiguration sessionConfiguration) {
            if (mHandler == null) {
                Log.d(TAG, "Handler unavailable");
                return;
            }
            Log.d(TAG, "onOpened child session for apn: " + mApnName + " with token: " + mToken);
            mHandler.obtainMessage(
                            EVENT_CHILD_SESSION_OPENED,
                            new TunnelOpenedData(
                                    mApnName,
                                    mToken,
                                    sessionConfiguration.getInternalDnsServers(),
                                    sessionConfiguration.getInternalAddresses()))
                    .sendToTarget();
        }

        @Override
        public void onClosed() {
            if (mHandler == null) {
                Log.d(TAG, "Handler unavailable");
                return;
            }
            Log.d(TAG, "onClosed child session for apn: " + mApnName + " with token: " + mToken);
            mHandler.obtainMessage(
                            EVENT_CHILD_SESSION_CLOSED,
                            new SessionClosedData(mApnName, mToken, null /* ikeException */))
                    .sendToTarget();
        }

        @Override
        public void onClosedWithException(IkeException exception) {
            onSessionClosedWithException(exception, mApnName, mToken, EVENT_CHILD_SESSION_CLOSED);
        }

        @Override
        public void onIpSecTransformsMigrated(
                IpSecTransform inIpSecTransform, IpSecTransform outIpSecTransform) {
            if (mHandler == null) {
                Log.d(TAG, "Handler unavailable");
                return;
            }
            // migration is similar to addition
            Log.d(TAG, "Transforms migrated for apn: " + mApnName + " with token: " + mToken);
            mHandler.obtainMessage(
                            EVENT_IPSEC_TRANSFORM_CREATED,
                            new IpsecTransformData(
                                    inIpSecTransform, IpSecManager.DIRECTION_IN, mApnName, mToken))
                    .sendToTarget();
            mHandler.obtainMessage(
                            EVENT_IPSEC_TRANSFORM_CREATED,
                            new IpsecTransformData(
                                    outIpSecTransform,
                                    IpSecManager.DIRECTION_OUT,
                                    mApnName,
                                    mToken))
                    .sendToTarget();
        }

        @Override
        public void onIpSecTransformCreated(IpSecTransform ipSecTransform, int direction) {
            if (mHandler == null) {
                Log.d(TAG, "Handler unavailable");
                return;
            }
            Log.d(
                    TAG,
                    "Transform created, direction: "
                            + direction
                            + ", apn: "
                            + mApnName
                            + ", token: "
                            + mToken);
            mHandler.obtainMessage(
                            EVENT_IPSEC_TRANSFORM_CREATED,
                            new IpsecTransformData(ipSecTransform, direction, mApnName, mToken))
                    .sendToTarget();
        }

        @Override
        public void onIpSecTransformDeleted(IpSecTransform ipSecTransform, int direction) {
            if (mHandler == null) {
                Log.d(TAG, "Handler unavailable");
                return;
            }
            Log.d(
                    TAG,
                    "Transform deleted, direction: "
                            + direction
                            + ", apn: "
                            + mApnName
                            + ", token: "
                            + mToken);
            mHandler.obtainMessage(
                            EVENT_IPSEC_TRANSFORM_DELETED,
                            new IpsecTransformData(ipSecTransform, direction, mApnName, mToken))
                    .sendToTarget();
        }
    }

    private EpdgTunnelManager(Context context, int slotId, FeatureFlags featureFlags) {
        this(
                context,
                slotId,
                featureFlags,
                new IkeSessionCreator(),
                new EpdgSelector(context, slotId, featureFlags));
    }

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    EpdgTunnelManager(
            Context context,
            int slotIndex,
            FeatureFlags featureFlags,
            IkeSessionCreator ikeSessionCreator,
            EpdgSelector epdgSelector) {
        mContext = context;
        mSlotId = slotIndex;
        mFeatureFlags = featureFlags;
        mIkeSessionCreator = ikeSessionCreator;
        mIpSecManager = mContext.getSystemService(IpSecManager.class);
        // Adding this here is necessary because we need to initialize EpdgSelector at the beginning
        // to ensure no broadcasts are missed.
        mEpdgSelector = epdgSelector;
        TAG = EpdgTunnelManager.class.getSimpleName() + "[" + mSlotId + "]";
        initHandler();
        registerConnectivityDiagnosticsCallback();
    }

    private void registerConnectivityDiagnosticsCallback() {
        ConnectivityDiagnosticsManager connectivityDiagnosticsManager =
                Objects.requireNonNull(mContext)
                        .getSystemService(ConnectivityDiagnosticsManager.class);
        NetworkRequest networkRequest =
                new NetworkRequest.Builder()
                        .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
                        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                        .build();
        mConnectivityDiagnosticsCallback =
                new ConnectivityDiagnosticsCallback() {
                    @Override
                    public void onConnectivityReportAvailable(@NonNull ConnectivityReport report) {
                        Network network = report.getNetwork();
                        int mNetworkValidationResult =
                                report.getAdditionalInfo()
                                        .getInt(ConnectivityReport.KEY_NETWORK_VALIDATION_RESULT);
                        if (!mMetricsAtomForNetwork.containsKey(network)) {
                            return;
                        }
                        reportValidationMetricsAtom(
                                network,
                                getMetricsValidationResult(mNetworkValidationResult),
                                /* validationTriggered */ true);
                    }
                };
        connectivityDiagnosticsManager.registerConnectivityDiagnosticsCallback(
                networkRequest, new HandlerExecutor(mHandler), mConnectivityDiagnosticsCallback);
    }

    private void reportValidationMetricsAtom(
            @NonNull Network network, int validationResult, boolean validationTriggered) {
        if (!mMetricsAtomForNetwork.containsKey(network)) {
            return;
        }
        MetricsAtom metricsAtom = mMetricsAtomForNetwork.get(network);
        metricsAtom.setValidationResult(validationResult);
        metricsAtom.setValidationTriggered(validationTriggered);
        metricsAtom.setValidationDurationMills(
                validationTriggered
                        ? (int)
                                (IwlanHelper.elapsedRealtime()
                                        - metricsAtom.getValidationStartTimeMills())
                        : 0);
        Log.d(
                TAG,
                "reportValidationMetricsAtom: reason="
                        + metricsAtom.getTriggerReason()
                        + " validationResult="
                        + metricsAtom.getValidationResult()
                        + " transportType="
                        + metricsAtom.getValidationTransportType()
                        + " duration="
                        + metricsAtom.getValidationDurationMills()
                        + " validationTriggered="
                        + metricsAtom.getValidationTriggered());
        metricsAtom.sendMetricsData();
        mMetricsAtomForNetwork.remove(network);
    }

    @VisibleForTesting
    MetricsAtom getValidationMetricsAtom(Network network) {
        return mMetricsAtomForNetwork.get(network);
    }

    private void unregisterConnectivityDiagnosticsCallback() {
        ConnectivityDiagnosticsManager connectivityDiagnosticsManager =
                Objects.requireNonNull(mContext)
                        .getSystemService(ConnectivityDiagnosticsManager.class);
        if (connectivityDiagnosticsManager != null) {
            connectivityDiagnosticsManager.unregisterConnectivityDiagnosticsCallback(
                    mConnectivityDiagnosticsCallback);
        }
    }

    @VisibleForTesting
    void initHandler() {
        mHandler = new TmHandler(getLooper());
    }

    @VisibleForTesting
    Looper getLooper() {
        HandlerThread handlerThread = new HandlerThread("EpdgTunnelManagerThread");
        handlerThread.start();
        return handlerThread.getLooper();
    }

    /**
     * Gets a EpdgTunnelManager instance.
     *
     * @param context the context at which EpdgTunnelManager instance to be created
     * @param slotId the slot index at which EpdgTunnelManager instance to be created
     * @return EpdgTunnelManager instance for the specified slot id
     */
    public static EpdgTunnelManager getInstance(@NonNull Context context, int slotId) {
        return mTunnelManagerInstances.computeIfAbsent(
                slotId, k -> new EpdgTunnelManager(context, slotId, new FeatureFlagsImpl()));
    }

    @VisibleForTesting
    public static void resetAllInstances() {
        mTunnelManagerInstances.clear();
        sLastUnderlyingNetworkValidationMs = 0;
    }

    private void reset() {
        if (mHandler != null) {
            mHandler.getLooper().quit();
            mHandler = null;
        }

        mApnNameToTunnelConfig.forEach(
                (apn, config) -> {
                    config.getIkeSession().kill();
                    IpSecManager.IpSecTunnelInterface iface = config.getIface();
                    if (iface != null) {
                        iface.close();
                    }
                    IpsecTransformData transformData = mApnNameToIpsecTransform.get(apn);
                    if (transformData != null) {
                        transformData.getTransform().close();
                        mApnNameToIpsecTransform.remove(apn);
                    }
                });

        mApnNameToTunnelConfig.clear();
    }

    public static void deinit() {
        mTunnelManagerInstances.values().forEach(EpdgTunnelManager::reset);
    }

    /**
     * Close tunnel for an apn. Confirmation of closing will be delivered in EpdgTunnelCallback that
     * was provided in {@link #bringUpTunnel}. If no tunnel was available, callback will be
     * delivered using client-provided provided tunnelCallback and iwlanTunnelMetrics
     *
     * @param apnName APN name
     * @param forceClose if {@code true}, triggers a local cleanup of the tunnel; if {@code false},
     *     performs a normal closure procedure
     * @param tunnelCallback The tunnelCallback for tunnel to be closed
     * @param reason The reason for tunnel to be closed
     */
    public void closeTunnel(
            @NonNull String apnName,
            boolean forceClose,
            @NonNull EpdgTunnelCallback tunnelCallback,
            @TunnelBringDownReason int reason) {
        mHandler.obtainMessage(
                        EVENT_TUNNEL_BRINGDOWN_REQUEST,
                        new TunnelBringdownRequest(apnName, forceClose, tunnelCallback, reason))
                .sendToTarget();
    }

    /**
     * Update the local Network. This will trigger a revaluation for every tunnel for which tunnel
     * manager has state.
     *
     * @param network the network to be updated
     * @param linkProperties the linkProperties to be updated
     */
    public void updateNetwork(Network network, LinkProperties linkProperties) {
        UpdateNetworkWrapper updateNetworkWrapper =
                new UpdateNetworkWrapper(network, linkProperties);
        mHandler.obtainMessage(EVENT_UPDATE_NETWORK, updateNetworkWrapper).sendToTarget();
    }

    /**
     * Bring up epdg tunnel. Only one bring up request per apn is expected. All active tunnel
     * requests and tunnels are expected to be on the same network.
     *
     * @param setupRequest {@link TunnelSetupRequest} tunnel configurations
     * @param tunnelCallback {@link EpdgTunnelCallback} interface to notify clients about the tunnel
     *     state
     * @return true if params are valid and no existing tunnel. False otherwise.
     */
    public boolean bringUpTunnel(
            @NonNull TunnelSetupRequest setupRequest, @NonNull EpdgTunnelCallback tunnelCallback) {
        String apnName = setupRequest.apnName();

        if (getTunnelSetupRequestApnName(setupRequest) == null) {
            Log.e(TAG, "APN is null.");
            return false;
        }

        if (isTunnelConfigContainExistApn(apnName)) {
            Log.e(TAG, "Tunnel exists for apn:" + apnName);
            return false;
        }

        if (!isValidApnProtocol(setupRequest.apnIpProtocol())) {
            Log.e(TAG, "Invalid protocol for APN");
            return false;
        }

        int pduSessionId = setupRequest.pduSessionId();
        if (pduSessionId < 0 || pduSessionId > 15) {
            Log.e(TAG, "Invalid pduSessionId: " + pduSessionId);
            return false;
        }

        TunnelRequestWrapper tunnelRequestWrapper =
                new TunnelRequestWrapper(setupRequest, tunnelCallback);

        mHandler.obtainMessage(EVENT_TUNNEL_BRINGUP_REQUEST, tunnelRequestWrapper).sendToTarget();

        return true;
    }

    private IkeSessionParams tryBuildIkeSessionParams(
            TunnelSetupRequest setupRequest, String apnName, int token, InetAddress epdgAddress) {
        try {
            return buildIkeSessionParams(setupRequest, apnName, token, epdgAddress);
        } catch (IwlanSimNotReadyException e) {
            return null;
        }
    }

    private IpSecManager.IpSecTunnelInterface tryCreateIpSecTunnelInterface() {
        try {
            return mIpSecManager.createIpSecTunnelInterface(
                    DUMMY_ADDR /* unused */, DUMMY_ADDR /* unused */, mDefaultNetwork);
        } catch (IpSecManager.ResourceUnavailableException | IOException e) {
            Log.e(TAG, "Failed to create tunnel interface. " + e);
            return null;
        }
    }

    private void onBringUpTunnel(
            TunnelRequestWrapper tunnelRequestWrapper, InetAddress epdgAddress) {
        TunnelSetupRequest setupRequest = tunnelRequestWrapper.getSetupRequest();
        EpdgTunnelCallback tunnelCallback = tunnelRequestWrapper.getEpdgTunnelCallback();
        String apnName = setupRequest.apnName();
        IkeSessionParams ikeSessionParams;
        IpSecManager.IpSecTunnelInterface iface;

        Log.d(
                TAG,
                "Bringing up tunnel for apn: "
                        + apnName
                        + " ePDG: "
                        + epdgAddress.getHostAddress());

        final int token = incrementAndGetCurrentTokenForApn(apnName);

        ikeSessionParams = tryBuildIkeSessionParams(setupRequest, apnName, token, epdgAddress);
        if (Objects.isNull(ikeSessionParams)) {
            IwlanError iwlanError = new IwlanError(IwlanError.SIM_NOT_READY_EXCEPTION);
            reportIwlanError(apnName, iwlanError);
            tunnelCallback.onClosed(
                    apnName, iwlanError, new OnClosedMetrics.Builder().setApnName(apnName).build());
            return;
        }

        iface = tryCreateIpSecTunnelInterface();
        if (Objects.isNull(iface)) {
            IwlanError iwlanError = new IwlanError(IwlanError.TUNNEL_TRANSFORM_FAILED);
            reportIwlanError(apnName, iwlanError);
            tunnelCallback.onClosed(
                    apnName, iwlanError, new OnClosedMetrics.Builder().setApnName(apnName).build());
            return;
        }

        mIkeTunnelEstablishmentStartTime = System.currentTimeMillis();
        IkeSession ikeSession =
                getIkeSessionCreator()
                        .createIkeSession(
                                mContext,
                                ikeSessionParams,
                                buildChildSessionParams(setupRequest),
                                Executors.newSingleThreadExecutor(),
                                getTmIkeSessionCallback(apnName, token),
                                new TmChildSessionCallback(apnName, token));

        boolean isSrcIpv6Present = setupRequest.srcIpv6Address().isPresent();
        putApnNameToTunnelConfig(
                apnName,
                ikeSession,
                tunnelCallback,
                iface,
                isSrcIpv6Present ? setupRequest.srcIpv6Address().get() : null,
                setupRequest.srcIpv6AddressPrefixLength(),
                setupRequest.isEmergency(),
                epdgAddress,
                ikeSessionParams.getNetwork());
    }

    /**
     * Proxy to allow testing
     *
     * @hide
     */
    @VisibleForTesting
    static class IkeSessionCreator {
        /** Creates a IKE session */
        public IkeSession createIkeSession(
                @NonNull Context context,
                @NonNull IkeSessionParams ikeSessionParams,
                @NonNull ChildSessionParams firstChildSessionParams,
                @NonNull Executor userCbExecutor,
                @NonNull IkeSessionCallback ikeSessionCallback,
                @NonNull ChildSessionCallback firstChildSessionCallback) {
            return new IkeSession(
                    context,
                    ikeSessionParams,
                    firstChildSessionParams,
                    userCbExecutor,
                    ikeSessionCallback,
                    firstChildSessionCallback);
        }
    }

    private ChildSessionParams buildChildSessionParams(TunnelSetupRequest setupRequest) {
        int proto = setupRequest.apnIpProtocol();
        int hardTimeSeconds =
                IwlanCarrierConfig.getConfigInt(
                        mContext,
                        mSlotId,
                        CarrierConfigManager.Iwlan.KEY_CHILD_SA_REKEY_HARD_TIMER_SEC_INT);
        int softTimeSeconds =
                IwlanCarrierConfig.getConfigInt(
                        mContext,
                        mSlotId,
                        CarrierConfigManager.Iwlan.KEY_CHILD_SA_REKEY_SOFT_TIMER_SEC_INT);
        if (!isValidChildSessionLifetime(hardTimeSeconds, softTimeSeconds)) {
            if (hardTimeSeconds > CHILD_HARD_LIFETIME_SEC_MAXIMUM
                    && softTimeSeconds > CHILD_SOFT_LIFETIME_SEC_MINIMUM) {
                hardTimeSeconds = CHILD_HARD_LIFETIME_SEC_MAXIMUM;
                softTimeSeconds = CHILD_HARD_LIFETIME_SEC_MAXIMUM - LIFETIME_MARGIN_SEC_MINIMUM;
            } else {
                hardTimeSeconds =
                        IwlanCarrierConfig.getDefaultConfigInt(
                                CarrierConfigManager.Iwlan.KEY_CHILD_SA_REKEY_HARD_TIMER_SEC_INT);
                softTimeSeconds =
                        IwlanCarrierConfig.getDefaultConfigInt(
                                CarrierConfigManager.Iwlan.KEY_CHILD_SA_REKEY_SOFT_TIMER_SEC_INT);
            }
            Log.d(
                    TAG,
                    "Invalid child session lifetime values, set hard: "
                            + hardTimeSeconds
                            + ", soft: "
                            + softTimeSeconds);
        }

        TunnelModeChildSessionParams.Builder childSessionParamsBuilder =
                new TunnelModeChildSessionParams.Builder()
                        .setLifetimeSeconds(hardTimeSeconds, softTimeSeconds);

        EpdgChildSaProposal epdgChildSaProposal = createEpdgChildSaProposal();

        if (IwlanCarrierConfig.getConfigBoolean(
                mContext,
                mSlotId,
                CarrierConfigManager.Iwlan.KEY_ADD_KE_TO_CHILD_SESSION_REKEY_BOOL)) {
            epdgChildSaProposal.enableAddChildSessionRekeyKePayload();
        }
        // Adds single SA proposal, priority is for AEAD if configured else non-AEAD proposal.
        if (isChildSessionAeadAlgosAvailable()) {
            childSessionParamsBuilder.addChildSaProposal(
                    epdgChildSaProposal.buildProposedChildSaAeadProposal());
        } else {
            childSessionParamsBuilder.addChildSaProposal(
                    epdgChildSaProposal.buildProposedChildSaProposal());
        }
        // Adds multiple proposals. If AEAD proposal already added then adds
        // configured non-AEAD proposal followed by supported AEAD and non-AEAD proposals.
        if (IwlanCarrierConfig.getConfigBoolean(
                mContext,
                mSlotId,
                CarrierConfigManager.Iwlan.KEY_SUPPORTS_CHILD_SESSION_MULTIPLE_SA_PROPOSALS_BOOL)) {
            if (isChildSessionAeadAlgosAvailable() && isChildSessionNonAeadAlgosAvailable()) {
                childSessionParamsBuilder.addChildSaProposal(
                        epdgChildSaProposal.buildProposedChildSaProposal());
            }
            childSessionParamsBuilder.addChildSaProposal(
                    epdgChildSaProposal.buildSupportedChildSaAeadProposal());
            childSessionParamsBuilder.addChildSaProposal(
                    epdgChildSaProposal.buildSupportedChildSaProposal());
        }

        boolean handoverIPv4Present = setupRequest.srcIpv4Address().isPresent();
        boolean handoverIPv6Present = setupRequest.srcIpv6Address().isPresent();
        if (handoverIPv4Present || handoverIPv6Present) {
            if (handoverIPv4Present) {
                childSessionParamsBuilder.addInternalAddressRequest(
                        (Inet4Address) setupRequest.srcIpv4Address().get());
                childSessionParamsBuilder.addInternalDnsServerRequest(AF_INET);
                childSessionParamsBuilder.addInboundTrafficSelectors(
                        getDefaultTrafficSelectorIpv4());
                childSessionParamsBuilder.addOutboundTrafficSelectors(
                        getDefaultTrafficSelectorIpv4());
            }
            if (handoverIPv6Present) {
                childSessionParamsBuilder.addInternalAddressRequest(
                        (Inet6Address) setupRequest.srcIpv6Address().get(),
                        setupRequest.srcIpv6AddressPrefixLength());
                childSessionParamsBuilder.addInternalDnsServerRequest(AF_INET6);
                childSessionParamsBuilder.addInboundTrafficSelectors(
                        getDefaultTrafficSelectorIpv6());
                childSessionParamsBuilder.addOutboundTrafficSelectors(
                        getDefaultTrafficSelectorIpv6());
            }
        } else {
            // non-handover case
            if (proto == ApnSetting.PROTOCOL_IP || proto == ApnSetting.PROTOCOL_IPV4V6) {
                childSessionParamsBuilder.addInternalAddressRequest(AF_INET);
                childSessionParamsBuilder.addInternalDnsServerRequest(AF_INET);
                childSessionParamsBuilder.addInboundTrafficSelectors(
                        getDefaultTrafficSelectorIpv4());
                childSessionParamsBuilder.addOutboundTrafficSelectors(
                        getDefaultTrafficSelectorIpv4());
            }
            if (proto == ApnSetting.PROTOCOL_IPV6 || proto == ApnSetting.PROTOCOL_IPV4V6) {
                childSessionParamsBuilder.addInternalAddressRequest(AF_INET6);
                childSessionParamsBuilder.addInternalDnsServerRequest(AF_INET6);
                childSessionParamsBuilder.addInboundTrafficSelectors(
                        getDefaultTrafficSelectorIpv6());
                childSessionParamsBuilder.addOutboundTrafficSelectors(
                        getDefaultTrafficSelectorIpv6());
            }
        }

        return childSessionParamsBuilder.build();
    }

    private static IkeTrafficSelector getDefaultTrafficSelectorIpv4() {
        return new IkeTrafficSelector(
                TRAFFIC_SELECTOR_START_PORT,
                TRAFFIC_SELECTOR_END_PORT,
                InetAddresses.parseNumericAddress(TRAFFIC_SELECTOR_IPV4_START_ADDR),
                InetAddresses.parseNumericAddress(TRAFFIC_SELECTOR_IPV4_END_ADDR));
    }

    private static IkeTrafficSelector getDefaultTrafficSelectorIpv6() {
        return new IkeTrafficSelector(
                TRAFFIC_SELECTOR_START_PORT,
                TRAFFIC_SELECTOR_END_PORT,
                InetAddresses.parseNumericAddress(TRAFFIC_SELECTOR_IPV6_START_ADDR),
                InetAddresses.parseNumericAddress(TRAFFIC_SELECTOR_IPV6_END_ADDR));
    }

    private boolean needIncludeInitialContact(InetAddress epdgAddress) {
        return !mEpdgMonitor.isConnectedEpdg(epdgAddress);
    }

    // Returns the IMEISV or device IMEI, in that order of priority.
    private @Nullable String getMobileDeviceIdentity() {
        TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
        telephonyManager =
                Objects.requireNonNull(telephonyManager)
                        .createForSubscriptionId(IwlanHelper.getSubId(mContext, mSlotId));
        if (telephonyManager == null) {
            return null;
        }
        // Queries the 15-digit device IMEI.
        String imei = telephonyManager.getImei();
        if (imei == null || imei.length() != DEVICE_IMEI_LEN) {
            Log.i(TAG, "Unable to query valid Mobile Device Identity (IMEI)!");
            return null;
        }
        String imeisv_suffix = telephonyManager.getDeviceSoftwareVersion();
        if (imeisv_suffix == null || imeisv_suffix.length() != DEVICE_IMEISV_SUFFIX_LEN) {
            // Unable to retrieve 2-digit suffix for IMEISV, so returns device IMEI.
            return imei;
        }
        // Splices the first 14 digit of device IMEI with 2-digit SV suffix to form IMEISV.
        return imei.substring(0, imei.length() - 1) + imeisv_suffix;
    }

    private IkeSessionParams buildIkeSessionParams(
            TunnelSetupRequest setupRequest, String apnName, int token, InetAddress epdgAddress)
            throws IwlanSimNotReadyException {
        int hardTimeSeconds =
                IwlanCarrierConfig.getConfigInt(
                        mContext,
                        mSlotId,
                        CarrierConfigManager.Iwlan.KEY_IKE_REKEY_HARD_TIMER_SEC_INT);
        int softTimeSeconds =
                IwlanCarrierConfig.getConfigInt(
                        mContext,
                        mSlotId,
                        CarrierConfigManager.Iwlan.KEY_IKE_REKEY_SOFT_TIMER_SEC_INT);
        if (!isValidIkeSessionLifetime(hardTimeSeconds, softTimeSeconds)) {
            if (hardTimeSeconds > IKE_HARD_LIFETIME_SEC_MAXIMUM
                    && softTimeSeconds > IKE_SOFT_LIFETIME_SEC_MINIMUM) {
                hardTimeSeconds = IKE_HARD_LIFETIME_SEC_MAXIMUM;
                softTimeSeconds = IKE_HARD_LIFETIME_SEC_MAXIMUM - LIFETIME_MARGIN_SEC_MINIMUM;
            } else {
                hardTimeSeconds =
                        IwlanCarrierConfig.getDefaultConfigInt(
                                CarrierConfigManager.Iwlan.KEY_IKE_REKEY_HARD_TIMER_SEC_INT);
                softTimeSeconds =
                        IwlanCarrierConfig.getDefaultConfigInt(
                                CarrierConfigManager.Iwlan.KEY_IKE_REKEY_SOFT_TIMER_SEC_INT);
            }
            Log.d(
                    TAG,
                    "Invalid ike session lifetime values, set hard: "
                            + hardTimeSeconds
                            + ", soft: "
                            + softTimeSeconds);
        }

        IkeSessionParams.Builder builder =
                new IkeSessionParams.Builder()
                        // permanently hardcode DSCP to 46 (Expedited Forwarding class)
                        // See https://www.iana.org/assignments/dscp-registry/dscp-registry.xhtml
                        // This will make WiFi prioritize IKE signallig under WMM AC_VO
                        .setDscp(46)
                        .setServerHostname(epdgAddress.getHostAddress())
                        .setLocalIdentification(getLocalIdentification())
                        .setRemoteIdentification(getId(setupRequest.apnName(), false))
                        .setAuthEap(null, getEapConfig())
                        .setNetwork(mDefaultNetwork)
                        .addIkeOption(IkeSessionParams.IKE_OPTION_ACCEPT_ANY_REMOTE_ID)
                        .addIkeOption(IkeSessionParams.IKE_OPTION_MOBIKE)
                        .addIkeOption(IkeSessionParams.IKE_OPTION_REKEY_MOBILITY)
                        .setLifetimeSeconds(hardTimeSeconds, softTimeSeconds)
                        .setRetransmissionTimeoutsMillis(getRetransmissionTimeoutsFromConfig())
                        .setDpdDelaySeconds(getDpdDelayFromConfig());

        EpdgIkeSaProposal epdgIkeSaProposal = createEpdgIkeSaProposal();

        // Adds single SA proposal, priority is for AEAD if configured else non-AEAD proposal.
        if (isIkeSessionAeadAlgosAvailable()) {
            builder.addIkeSaProposal(epdgIkeSaProposal.buildProposedIkeSaAeadProposal());
        } else {
            builder.addIkeSaProposal(epdgIkeSaProposal.buildProposedIkeSaProposal());
        }
        // Adds multiple proposals. If AEAD proposal already added then adds
        // configured non-AEAD proposal followed by supported AEAD and non-AEAD proposals.
        if (IwlanCarrierConfig.getConfigBoolean(
                mContext,
                mSlotId,
                CarrierConfigManager.Iwlan.KEY_SUPPORTS_IKE_SESSION_MULTIPLE_SA_PROPOSALS_BOOL)) {
            if (isIkeSessionAeadAlgosAvailable() && isIkeSessionNonAeadAlgosAvailable()) {
                builder.addIkeSaProposal(epdgIkeSaProposal.buildProposedIkeSaProposal());
            }
            builder.addIkeSaProposal(epdgIkeSaProposal.buildSupportedIkeSaAeadProposal());
            builder.addIkeSaProposal(epdgIkeSaProposal.buildSupportedIkeSaProposal());
        }

        if (needIncludeInitialContact(epdgAddress)) {
            builder.addIkeOption(IkeSessionParams.IKE_OPTION_INITIAL_CONTACT);
            Log.d(TAG, "IKE_OPTION_INITIAL_CONTACT");
        }

        if (IwlanCarrierConfig.getConfigInt(
                        mContext,
                        mSlotId,
                        CarrierConfigManager.Iwlan.KEY_EPDG_AUTHENTICATION_METHOD_INT)
                == CarrierConfigManager.Iwlan.AUTHENTICATION_METHOD_EAP_ONLY) {
            builder.addIkeOption(IkeSessionParams.IKE_OPTION_EAP_ONLY_AUTH);
        }

        if (setupRequest.requestPcscf()) {
            int proto = setupRequest.apnIpProtocol();
            if (proto == ApnSetting.PROTOCOL_IP || proto == ApnSetting.PROTOCOL_IPV4V6) {
                builder.addPcscfServerRequest(AF_INET);
            }
            if (proto == ApnSetting.PROTOCOL_IPV6 || proto == ApnSetting.PROTOCOL_IPV4V6) {
                builder.addPcscfServerRequest(AF_INET6);
            }
        }

        // If MOBIKE is configured, ePDGs may force IPv6 UDP encapsulation- as specified by
        // RFC 4555- which Android connectivity stack presently does not support.
        if (epdgAddress instanceof Inet6Address) {
            builder.removeIkeOption(IkeSessionParams.IKE_OPTION_MOBIKE);
        }

        builder.setIke3gppExtension(buildIke3gppExtension(setupRequest, apnName, token));

        int nattKeepAliveTimer =
                IwlanCarrierConfig.getConfigInt(
                        mContext,
                        mSlotId,
                        CarrierConfigManager.Iwlan.KEY_NATT_KEEP_ALIVE_TIMER_SEC_INT);
        if (nattKeepAliveTimer < NATT_KEEPALIVE_DELAY_SEC_MIN
                || nattKeepAliveTimer > NATT_KEEPALIVE_DELAY_SEC_MAX) {
            Log.d(TAG, "Falling back to default natt keep alive timer" + nattKeepAliveTimer);
            nattKeepAliveTimer =
                    IwlanCarrierConfig.getDefaultConfigInt(
                            CarrierConfigManager.Iwlan.KEY_NATT_KEEP_ALIVE_TIMER_SEC_INT);
        }
        builder.setNattKeepAliveDelaySeconds(nattKeepAliveTimer);

        return builder.build();
    }

    private Ike3gppExtension buildIke3gppExtension(
            TunnelSetupRequest setupRequest, String apnName, int token) {
        Ike3gppParams.Builder builder3gppParams = new Ike3gppParams.Builder();

        if (IwlanCarrierConfig.getConfigBoolean(
                mContext, mSlotId, IwlanCarrierConfig.KEY_IKE_DEVICE_IDENTITY_SUPPORTED_BOOL)) {
            String imei = getMobileDeviceIdentity();
            if (imei != null) {
                Log.d(TAG, "DEVICE_IDENTITY set in Ike3gppParams");
                builder3gppParams.setMobileDeviceIdentity(imei);
            }
        }

        if (setupRequest.pduSessionId() != PDU_SESSION_ID_UNSET) {
            // Includes N1_MODE_CAPABILITY NOTIFY payload in IKE_AUTH exchange when PDU session ID
            // is set; otherwise, do not include.
            builder3gppParams.setPduSessionId((byte) setupRequest.pduSessionId());
        }

        return new Ike3gppExtension(
                builder3gppParams.build(), new TmIke3gppCallback(apnName, token));
    }

    private boolean isChildSessionAeadAlgosAvailable() {
        int[] encryptionAlgos =
                IwlanCarrierConfig.getConfigIntArray(
                        mContext,
                        mSlotId,
                        CarrierConfigManager.Iwlan
                                .KEY_SUPPORTED_CHILD_SESSION_AEAD_ALGORITHMS_INT_ARRAY);
        for (int encryptionAlgo : encryptionAlgos) {
            if (validateConfig(encryptionAlgo, VALID_AEAD_ALGOS, CONFIG_TYPE_ENCRYPT_ALGO)) {
                return true;
            }
        }
        return false;
    }

    private boolean isChildSessionNonAeadAlgosAvailable() {
        int[] encryptionAlgos =
                IwlanCarrierConfig.getConfigIntArray(
                        mContext,
                        mSlotId,
                        CarrierConfigManager.Iwlan
                                .KEY_SUPPORTED_CHILD_SESSION_ENCRYPTION_ALGORITHMS_INT_ARRAY);
        for (int encryptionAlgo : encryptionAlgos) {
            if (validateConfig(encryptionAlgo, VALID_ENCRYPTION_ALGOS, CONFIG_TYPE_ENCRYPT_ALGO)) {
                return true;
            }
        }
        return false;
    }

    private boolean isIkeSessionAeadAlgosAvailable() {
        int[] encryptionAlgos =
                IwlanCarrierConfig.getConfigIntArray(
                        mContext,
                        mSlotId,
                        CarrierConfigManager.Iwlan
                                .KEY_SUPPORTED_IKE_SESSION_AEAD_ALGORITHMS_INT_ARRAY);
        for (int encryptionAlgo : encryptionAlgos) {
            if (validateConfig(encryptionAlgo, VALID_AEAD_ALGOS, CONFIG_TYPE_ENCRYPT_ALGO)) {
                return true;
            }
        }
        return false;
    }

    private boolean isIkeSessionNonAeadAlgosAvailable() {
        int[] encryptionAlgos =
                IwlanCarrierConfig.getConfigIntArray(
                        mContext,
                        mSlotId,
                        CarrierConfigManager.Iwlan
                                .KEY_SUPPORTED_IKE_SESSION_ENCRYPTION_ALGORITHMS_INT_ARRAY);
        for (int encryptionAlgo : encryptionAlgos) {
            if (validateConfig(encryptionAlgo, VALID_ENCRYPTION_ALGOS, CONFIG_TYPE_ENCRYPT_ALGO)) {
                return true;
            }
        }
        return false;
    }

    private boolean isValidChildSessionLifetime(int hardLifetimeSeconds, int softLifetimeSeconds) {
        return hardLifetimeSeconds >= CHILD_HARD_LIFETIME_SEC_MINIMUM
                && hardLifetimeSeconds <= CHILD_HARD_LIFETIME_SEC_MAXIMUM
                && softLifetimeSeconds >= CHILD_SOFT_LIFETIME_SEC_MINIMUM
                && hardLifetimeSeconds - softLifetimeSeconds >= LIFETIME_MARGIN_SEC_MINIMUM;
    }

    private boolean isValidIkeSessionLifetime(int hardLifetimeSeconds, int softLifetimeSeconds) {
        return hardLifetimeSeconds >= IKE_HARD_LIFETIME_SEC_MINIMUM
                && hardLifetimeSeconds <= IKE_HARD_LIFETIME_SEC_MAXIMUM
                && softLifetimeSeconds >= IKE_SOFT_LIFETIME_SEC_MINIMUM
                && hardLifetimeSeconds - softLifetimeSeconds >= LIFETIME_MARGIN_SEC_MINIMUM;
    }

    private void createEpdgSaProposal(EpdgSaProposal epdgSaProposal, boolean isChildProposal) {
        epdgSaProposal.addProposedDhGroups(
                IwlanCarrierConfig.getConfigIntArray(
                        mContext,
                        mSlotId,
                        CarrierConfigManager.Iwlan.KEY_DIFFIE_HELLMAN_GROUPS_INT_ARRAY));

        String encryptionAlgosConfigKey =
                isChildProposal
                        ? CarrierConfigManager.Iwlan
                                .KEY_SUPPORTED_CHILD_SESSION_ENCRYPTION_ALGORITHMS_INT_ARRAY
                        : CarrierConfigManager.Iwlan
                                .KEY_SUPPORTED_IKE_SESSION_ENCRYPTION_ALGORITHMS_INT_ARRAY;

        int[] encryptionAlgos =
                IwlanCarrierConfig.getConfigIntArray(mContext, mSlotId, encryptionAlgosConfigKey);

        for (int encryptionAlgo : encryptionAlgos) {
            if (encryptionAlgo == SaProposal.ENCRYPTION_ALGORITHM_AES_CBC) {
                String aesCbcKeyLensConfigKey =
                        isChildProposal
                                ? CarrierConfigManager.Iwlan
                                        .KEY_CHILD_SESSION_AES_CBC_KEY_SIZE_INT_ARRAY
                                : CarrierConfigManager.Iwlan
                                        .KEY_IKE_SESSION_AES_CBC_KEY_SIZE_INT_ARRAY;

                int[] aesCbcKeyLens =
                        IwlanCarrierConfig.getConfigIntArray(
                                mContext, mSlotId, aesCbcKeyLensConfigKey);
                epdgSaProposal.addProposedEncryptionAlgorithm(encryptionAlgo, aesCbcKeyLens);
            }

            if (encryptionAlgo == SaProposal.ENCRYPTION_ALGORITHM_AES_CTR) {
                String aesCtrKeyLensConfigKey =
                        isChildProposal
                                ? CarrierConfigManager.Iwlan
                                        .KEY_CHILD_SESSION_AES_CTR_KEY_SIZE_INT_ARRAY
                                : CarrierConfigManager.Iwlan
                                        .KEY_IKE_SESSION_AES_CTR_KEY_SIZE_INT_ARRAY;
                int[] aesCtrKeyLens =
                        IwlanCarrierConfig.getConfigIntArray(
                                mContext, mSlotId, aesCtrKeyLensConfigKey);
                epdgSaProposal.addProposedEncryptionAlgorithm(encryptionAlgo, aesCtrKeyLens);
            }
        }

        if (encryptionAlgos.length > 0) {
            epdgSaProposal.addProposedIntegrityAlgorithm(
                    IwlanCarrierConfig.getConfigIntArray(
                            mContext,
                            mSlotId,
                            CarrierConfigManager.Iwlan
                                    .KEY_SUPPORTED_INTEGRITY_ALGORITHMS_INT_ARRAY));
        }

        String aeadAlgosConfigKey =
                isChildProposal
                        ? CarrierConfigManager.Iwlan
                                .KEY_SUPPORTED_CHILD_SESSION_AEAD_ALGORITHMS_INT_ARRAY
                        : CarrierConfigManager.Iwlan
                                .KEY_SUPPORTED_IKE_SESSION_AEAD_ALGORITHMS_INT_ARRAY;
        int[] aeadAlgos =
                IwlanCarrierConfig.getConfigIntArray(mContext, mSlotId, aeadAlgosConfigKey);
        for (int aeadAlgo : aeadAlgos) {
            if (!validateConfig(aeadAlgo, VALID_AEAD_ALGOS, CONFIG_TYPE_ENCRYPT_ALGO)) {
                continue;
            }
            if ((aeadAlgo == SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8)
                    || (aeadAlgo == SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12)
                    || (aeadAlgo == SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_16)) {
                String aesGcmKeyLensConfigKey =
                        isChildProposal
                                ? CarrierConfigManager.Iwlan
                                        .KEY_CHILD_SESSION_AES_GCM_KEY_SIZE_INT_ARRAY
                                : CarrierConfigManager.Iwlan
                                        .KEY_IKE_SESSION_AES_GCM_KEY_SIZE_INT_ARRAY;
                int[] aesGcmKeyLens =
                        IwlanCarrierConfig.getConfigIntArray(
                                mContext, mSlotId, aesGcmKeyLensConfigKey);
                epdgSaProposal.addProposedAeadAlgorithm(aeadAlgo, aesGcmKeyLens);
            }
        }

        if (IwlanCarrierConfig.getConfigBoolean(
                mContext, mSlotId, IwlanCarrierConfig.KEY_IKE_SA_TRANSFORMS_REORDER_BOOL)) {
            epdgSaProposal.enableReorderingSaferProposals();
        }
    }

    private EpdgChildSaProposal createEpdgChildSaProposal() {
        EpdgChildSaProposal epdgChildSaProposal = new EpdgChildSaProposal();
        createEpdgSaProposal(epdgChildSaProposal, true);
        return epdgChildSaProposal;
    }

    private EpdgIkeSaProposal createEpdgIkeSaProposal() {
        EpdgIkeSaProposal epdgIkeSaProposal = new EpdgIkeSaProposal();

        createEpdgSaProposal(epdgIkeSaProposal, false);

        epdgIkeSaProposal.addProposedPrfAlgorithm(
                IwlanCarrierConfig.getConfigIntArray(
                        mContext,
                        mSlotId,
                        CarrierConfigManager.Iwlan.KEY_SUPPORTED_PRF_ALGORITHMS_INT_ARRAY));
        return epdgIkeSaProposal;
    }

    private boolean validateConfig(int config, Set<Integer> validConfigValues, String configType) {
        if (validConfigValues.contains(config)) {
            return true;
        }

        Log.e(TAG, "Invalid config value for " + configType + ":" + config);
        return false;
    }

    private IkeIdentification getLocalIdentification() throws IwlanSimNotReadyException {
        String nai;

        nai = IwlanHelper.getNai(mContext, mSlotId, mNextReauthId);

        if (nai == null) {
            throw new IwlanSimNotReadyException("Nai is null.");
        }

        Log.d(TAG, "getLocalIdentification: Nai: " + nai);
        return getId(nai, true);
    }

    private IkeIdentification getId(String id, boolean isLocal) {
        String idTypeConfig =
                isLocal
                        ? CarrierConfigManager.Iwlan.KEY_IKE_LOCAL_ID_TYPE_INT
                        : CarrierConfigManager.Iwlan.KEY_IKE_REMOTE_ID_TYPE_INT;
        int idType = IwlanCarrierConfig.getConfigInt(mContext, mSlotId, idTypeConfig);
        return switch (idType) {
            case CarrierConfigManager.Iwlan.ID_TYPE_FQDN -> new IkeFqdnIdentification(id);
            case CarrierConfigManager.Iwlan.ID_TYPE_KEY_ID ->
                    new IkeKeyIdIdentification(id.getBytes(StandardCharsets.US_ASCII));
            case CarrierConfigManager.Iwlan.ID_TYPE_RFC822_ADDR ->
                    new IkeRfc822AddrIdentification(id);
            default -> throw new IllegalArgumentException("Invalid local Identity type: " + idType);
        };
    }

    private EapSessionConfig getEapConfig() throws IwlanSimNotReadyException {
        int subId = IwlanHelper.getSubId(mContext, mSlotId);
        String nai = IwlanHelper.getNai(mContext, mSlotId, null);

        if (nai == null) {
            throw new IwlanSimNotReadyException("Nai is null.");
        }

        EapSessionConfig.EapAkaOption option = null;
        if (mNextReauthId != null) {
            option = new EapSessionConfig.EapAkaOption.Builder().setReauthId(mNextReauthId).build();
        }

        Log.d(TAG, "getEapConfig: Nai: " + nai);
        return new EapSessionConfig.Builder()
                .setEapAkaConfig(subId, TelephonyManager.APPTYPE_USIM, option)
                .setEapIdentity(nai.getBytes(StandardCharsets.US_ASCII))
                .build();
    }

    private void onSessionClosedWithException(
            IkeException exception, String apnName, int token, int sessionType) {
        Log.e(
                TAG,
                "Closing tunnel with exception for apn: "
                        + apnName
                        + " with token: "
                        + token
                        + " sessionType:"
                        + sessionType);
        exception.printStackTrace();

        mHandler.obtainMessage(sessionType, new SessionClosedData(apnName, token, exception))
                .sendToTarget();
    }

    private boolean isEpdgSelectionOrFirstTunnelBringUpInProgress() {
        // Tunnel config is created but not connected to an ePDG. i.e., The first bring-up request
        // in progress.
        // No bring-up request in progress but pending queue is not empty. i.e. ePDG selection in
        // progress
        return (!mHasConnectedToEpdg && !mApnNameToTunnelConfig.isEmpty())
                || !mPendingBringUpRequests.isEmpty();
    }

    private IwlanError getErrorFromIkeException(
            IkeException ikeException, IkeSessionState ikeSessionState) {
        IwlanError error;
        if (ikeException instanceof IkeIOException) {
            error = new IwlanError(ikeSessionState.getErrorType(), ikeException);
        } else {
            error = new IwlanError(ikeException);
        }
        Log.e(TAG, "Closing tunnel: error: " + error + " state: " + ikeSessionState);
        return error;
    }

    private final class TmHandler extends Handler {

        TunnelLinkProperties buildTunnelLinkProperties(TunnelConfig tunnelConfig) {
            return TunnelLinkProperties.builder()
                    .setInternalAddresses(tunnelConfig.getInternalAddrList())
                    .setDnsAddresses(tunnelConfig.getDnsAddrList())
                    .setPcscfAddresses(tunnelConfig.getPcscfAddrList())
                    .setIfaceName(tunnelConfig.getIface().getInterfaceName())
                    .setSliceInfo(tunnelConfig.getSliceInfo())
                    .setUnderlyingNetwork(tunnelConfig.getUnderlyingNetwork())
                    .build();
        }

        @Override
        public void handleMessage(Message msg) {
            Log.d(TAG, "msg.what = " + eventToString(msg.what));

            String apnName;
            TunnelConfig tunnelConfig;
            OnClosedMetrics.Builder onClosedMetricsBuilder;
            TunnelRequestWrapper tunnelRequestWrapper;
            ConnectivityManager connectivityManager;
            boolean isNetworkValidated;
            switch (msg.what) {
                case EVENT_CHILD_SESSION_OPENED,
                        EVENT_IKE_SESSION_CLOSED,
                        EVENT_IPSEC_TRANSFORM_CREATED,
                        EVENT_IPSEC_TRANSFORM_DELETED,
                        EVENT_CHILD_SESSION_CLOSED,
                        EVENT_IKE_SESSION_OPENED,
                        EVENT_IKE_SESSION_CONNECTION_INFO_CHANGED,
                        EVENT_IKE_3GPP_DATA_RECEIVED,
                        EVENT_IKE_LIVENESS_STATUS_CHANGED -> {
                    IkeEventData ikeEventData = (IkeEventData) msg.obj;
                    if (isObsoleteToken(ikeEventData.mApnName, ikeEventData.mToken)) {
                        Log.d(
                                TAG,
                                eventToString(msg.what)
                                        + " for obsolete token "
                                        + ikeEventData.mToken);
                        return;
                    }
                }
            }

            long mIkeTunnelEstablishmentDuration;
            switch (msg.what) {
                case EVENT_TUNNEL_BRINGUP_REQUEST ->
                        handleTunnelBringUpRequest((TunnelRequestWrapper) msg.obj);
                case EVENT_EPDG_ADDRESS_SELECTION_REQUEST_COMPLETE -> {
                    EpdgSelectorResult selectorResult = (EpdgSelectorResult) msg.obj;
                    printRequestQueue("EVENT_EPDG_ADDRESS_SELECTION_REQUEST_COMPLETE");
                    if (selectorResult.getTransactionId() != mTransactionId) {
                        Log.e(TAG, "Mismatched transactionId");
                        break;
                    }
                    if (mPendingBringUpRequests.isEmpty()) {
                        Log.d(TAG, "Empty request queue");
                        break;
                    }
                    if (selectorResult.getEpdgError().getErrorType() == IwlanError.NO_ERROR
                            && selectorResult.getValidIpList() != null) {
                        tunnelRequestWrapper = mPendingBringUpRequests.remove();
                        onBringUpTunnel(
                                tunnelRequestWrapper,
                                validateAndSetEpdgAddress(selectorResult.getValidIpList()));
                    } else {
                        IwlanError error =
                                (selectorResult.getEpdgError().getErrorType()
                                                == IwlanError.NO_ERROR)
                                        ? new IwlanError(
                                                IwlanError.EPDG_SELECTOR_SERVER_SELECTION_FAILED)
                                        : selectorResult.getEpdgError();
                        failAllPendingRequests(error);
                    }
                }
                case EVENT_CHILD_SESSION_OPENED -> {
                    TunnelOpenedData tunnelOpenedData = (TunnelOpenedData) msg.obj;
                    apnName = tunnelOpenedData.mApnName;
                    tunnelConfig = mApnNameToTunnelConfig.get(apnName);
                    tunnelConfig.setDnsAddrList(tunnelOpenedData.mInternalDnsServers);
                    tunnelConfig.setInternalAddrList(tunnelOpenedData.mInternalAddresses);
                    IpSecManager.IpSecTunnelInterface tunnelInterface = tunnelConfig.getIface();
                    for (LinkAddress address : tunnelConfig.getInternalAddrList()) {
                        try {
                            tunnelInterface.addAddress(
                                    address.getAddress(), address.getPrefixLength());
                        } catch (IOException e) {
                            Log.e(TAG, "Adding internal addresses to interface failed.");
                        }
                    }

                    mIkeTunnelEstablishmentDuration =
                            System.currentTimeMillis() - mIkeTunnelEstablishmentStartTime;
                    mIkeTunnelEstablishmentStartTime = 0;
                    isNetworkValidated = isUnderlyingNetworkValidated(mIkeSessionNetwork);
                    OnOpenedMetrics onOpenedMetrics =
                            new OnOpenedMetrics.Builder()
                                    .setApnName(apnName)
                                    .setEpdgServerAddress(tunnelConfig.getEpdgAddress())
                                    .setEpdgServerSelectionDuration(
                                            (int) mEpdgServerSelectionDuration)
                                    .setIkeTunnelEstablishmentDuration(
                                            (int) mIkeTunnelEstablishmentDuration)
                                    .setIsNetworkValidated(isNetworkValidated)
                                    .build();
                    tunnelConfig
                            .getEpdgTunnelCallback()
                            .onOpened(
                                    apnName,
                                    buildTunnelLinkProperties(tunnelConfig),
                                    onOpenedMetrics);

                    reportIwlanError(apnName, new IwlanError(IwlanError.NO_ERROR));
                    mEpdgSelector.onEpdgConnectedSuccessfully();
                    mEpdgMonitor.onApnConnectToEpdg(apnName, tunnelConfig.getEpdgAddress());
                    onConnectedToEpdg(true);
                    printRequestQueue("EVENT_CHILD_SESSION_OPENED");
                    serviceAllPendingRequests();
                    tunnelConfig.setIkeSessionState(IkeSessionState.CHILD_SESSION_OPENED);
                }
                case EVENT_IKE_SESSION_CLOSED -> {
                    printRequestQueue("EVENT_IKE_SESSION_CLOSED");
                    SessionClosedData sessionClosedData = (SessionClosedData) msg.obj;
                    apnName = sessionClosedData.mApnName;
                    tunnelConfig = mApnNameToTunnelConfig.get(apnName);
                    if (tunnelConfig == null) {
                        Log.e(TAG, "No callback found for apn: " + apnName);
                        return;
                    }
                    // If IKE session closed exceptionally, we retrieve IwlanError directly from the
                    // exception; otherwise, it is still possible that we triggered an IKE session
                    // close due to an error (e.g. IwlanError.TUNNEL_TRANSFORM_FAILED), or because
                    // the Child session closed exceptionally; in which case, we attempt to retrieve
                    // the stored error (if any) from TunnelConfig.
                    IwlanError iwlanError;
                    if (sessionClosedData.mIkeException != null) {
                        iwlanError =
                                getErrorFromIkeException(
                                        sessionClosedData.mIkeException,
                                        tunnelConfig.getIkeSessionState());
                    } else {
                        // If IKE session opened, then closed before child session (and IWLAN
                        // tunnel) opened.
                        // Iwlan reports IKE_SESSION_CLOSED_BEFORE_CHILD_SESSION_OPENED
                        // instead of NO_ERROR
                        if (!tunnelConfig.hasTunnelOpened()) {
                            int errorType =
                                    IwlanError.IKE_SESSION_CLOSED_BEFORE_CHILD_SESSION_OPENED;
                            iwlanError = new IwlanError(errorType);
                        } else {
                            iwlanError = tunnelConfig.getError();
                        }
                    }
                    IpSecManager.IpSecTunnelInterface iface = tunnelConfig.getIface();
                    if (iface != null) {
                        iface.close();
                    }
                    if (!tunnelConfig.hasTunnelOpened()) {
                        if (tunnelConfig.isBackoffTimeValid()) {
                            reportIwlanError(apnName, iwlanError, tunnelConfig.getBackoffTime());
                        } else {
                            reportIwlanError(apnName, iwlanError);
                        }
                        mEpdgMonitor.onEpdgConnectionFailed(
                                tunnelConfig.isEmergency(), tunnelConfig.getEpdgAddress());
                        if (sessionClosedData.mIkeException != null) {
                            mEpdgSelector.onEpdgConnectionFailed(
                                    tunnelConfig.getEpdgAddress(), sessionClosedData.mIkeException);
                        }
                    } else {
                        /* PDN disconnected case */
                        triggerUnderlyingNetworkValidationOnError(iwlanError);
                    }
                    Log.d(TAG, "Tunnel Closed: " + iwlanError);
                    tunnelConfig.setIkeSessionState(IkeSessionState.NO_IKE_SESSION);
                    onClosedMetricsBuilder = new OnClosedMetrics.Builder().setApnName(apnName);
                    if (!mEpdgMonitor.hasEpdgConnected()) {
                        failAllPendingRequests(iwlanError);
                    } else {
                        mIkeTunnelEstablishmentDuration =
                                mIkeTunnelEstablishmentStartTime > 0
                                        ? System.currentTimeMillis()
                                                - mIkeTunnelEstablishmentStartTime
                                        : 0;
                        mIkeTunnelEstablishmentStartTime = 0;
                        isNetworkValidated = isUnderlyingNetworkValidated(mIkeSessionNetwork);
                        onClosedMetricsBuilder
                                .setEpdgServerAddress(tunnelConfig.getEpdgAddress())
                                .setEpdgServerSelectionDuration((int) mEpdgServerSelectionDuration)
                                .setIkeTunnelEstablishmentDuration(
                                        (int) mIkeTunnelEstablishmentDuration)
                                .setIsNetworkValidated(isNetworkValidated);
                    }
                    tunnelConfig
                            .getEpdgTunnelCallback()
                            .onClosed(apnName, iwlanError, onClosedMetricsBuilder.build());
                    mApnNameToTunnelConfig.remove(apnName);
                    mEpdgMonitor.onApnDisconnectFromEpdg(apnName);
                    if (mApnNameToTunnelConfig.isEmpty() && mPendingBringUpRequests.isEmpty()) {
                        onConnectedToEpdg(false);
                    }
                }
                case EVENT_UPDATE_NETWORK -> {
                    UpdateNetworkWrapper updatedNetwork = (UpdateNetworkWrapper) msg.obj;
                    mDefaultNetwork = updatedNetwork.getNetwork();
                    LinkProperties defaultLinkProperties = updatedNetwork.getLinkProperties();
                    String paraString = "Network: " + mDefaultNetwork;
                    if (mEpdgMonitor.hasEpdgConnected()) {
                        if (Objects.isNull(mDefaultNetwork)) {
                            Log.w(TAG, "The default network has been removed.");
                        } else if (Objects.isNull(defaultLinkProperties)) {
                            Log.w(
                                    TAG,
                                    "The default network's LinkProperties is not ready ."
                                            + paraString);
                        } else if (Objects.equals(mDefaultNetwork, mIkeSessionNetwork)) {
                            Log.w(
                                    TAG,
                                    "The default network has not changed from the IKE session"
                                            + " network. "
                                            + paraString);
                        } else {
                            for (var entry : mApnNameToTunnelConfig.entrySet()) {
                                String apn = entry.getKey();
                                TunnelConfig config = entry.getValue();
                                if (!defaultLinkProperties.isReachable(config.getEpdgAddress())) {
                                    Log.w(
                                            TAG,
                                            "The default network link "
                                                    + defaultLinkProperties
                                                    + " doesn't have a route to the ePDG "
                                                    + config.getEpdgAddress()
                                                    + paraString);
                                } else {
                                    Log.d(
                                            TAG,
                                            "The Underlying Network is updating for APN (+"
                                                    + apn
                                                    + "). "
                                                    + paraString);
                                    config.getIkeSession().setNetwork(mDefaultNetwork);
                                    config.setIkeSessionState(
                                            IkeSessionState.IKE_MOBILITY_IN_PROGRESS);
                                    mIkeSessionNetwork = mDefaultNetwork;
                                }
                            }
                        }
                    }
                }
                case EVENT_TUNNEL_BRINGDOWN_REQUEST -> {
                    TunnelBringdownRequest bringdownRequest = (TunnelBringdownRequest) msg.obj;
                    apnName = bringdownRequest.mApnName;
                    boolean forceClose = bringdownRequest.mForceClose;
                    int reason = bringdownRequest.mBringDownReason;
                    tunnelConfig = mApnNameToTunnelConfig.get(apnName);
                    if (tunnelConfig == null) {
                        Log.w(
                                TAG,
                                "Bringdown request: No tunnel exists for apn: "
                                        + apnName
                                        + ", forced: "
                                        + forceClose
                                        + ", bringdown reason: "
                                        + bringdownReasonToString(reason));
                    } else {
                        if (forceClose) {
                            tunnelConfig.getIkeSession().kill();
                        } else {
                            tunnelConfig.getIkeSession().close();
                        }
                    }
                    // TODO(b/309867892): Include tunnel bring down reason in metrics.
                    int numClosed = closePendingRequestsForApn(apnName);
                    if (numClosed > 0) {
                        Log.d(
                                TAG,
                                "Closed "
                                        + numClosed
                                        + " pending requests for apn: "
                                        + apnName
                                        + ", bringdown reason: "
                                        + bringdownReasonToString(reason));
                    }
                    if (tunnelConfig == null && numClosed == 0) {
                        // IwlanDataService expected to close a (pending or up) tunnel but was not
                        // found. Recovers state in IwlanDataService through TunnelCallback.
                        IwlanError iwlanError = new IwlanError(IwlanError.TUNNEL_NOT_FOUND);
                        reportIwlanError(apnName, iwlanError);
                        bringdownRequest.mEpdgTunnelCallback.onClosed(
                                apnName,
                                iwlanError,
                                new OnClosedMetrics.Builder().setApnName(apnName).build());
                    }
                }
                case EVENT_IPSEC_TRANSFORM_CREATED -> {
                    IpsecTransformData transformData = (IpsecTransformData) msg.obj;
                    apnName = transformData.getApnName();
                    tunnelConfig = mApnNameToTunnelConfig.get(apnName);
                    try {
                        mIpSecManager.applyTunnelModeTransform(
                                tunnelConfig.getIface(),
                                transformData.getDirection(),
                                transformData.getTransform());
                    } catch (IOException | IllegalArgumentException e) {
                        // If the IKE session was closed before the transform could be applied, the
                        // IpSecService will throw an IAE on processing the IpSecTunnelInterface id.
                        Log.e(TAG, "Failed to apply tunnel transform." + e);
                        closeIkeSession(
                                apnName, new IwlanError(IwlanError.TUNNEL_TRANSFORM_FAILED));
                    }
                    mApnNameToIpsecTransform.put(apnName, transformData);
                    if (tunnelConfig.getIkeSessionState()
                            == IkeSessionState.IKE_MOBILITY_IN_PROGRESS) {
                        tunnelConfig.setIkeSessionState(IkeSessionState.CHILD_SESSION_OPENED);
                    }
                }
                case EVENT_IPSEC_TRANSFORM_DELETED -> {
                    IpsecTransformData transformData = (IpsecTransformData) msg.obj;
                    IpSecTransform transform = transformData.getTransform();
                    transform.close();
                    mApnNameToIpsecTransform.remove(transformData.getApnName());
                }
                case EVENT_CHILD_SESSION_CLOSED -> {
                    SessionClosedData sessionClosedData = (SessionClosedData) msg.obj;
                    apnName = sessionClosedData.mApnName;
                    tunnelConfig = mApnNameToTunnelConfig.get(apnName);
                    if (tunnelConfig == null) {
                        Log.d(TAG, "No tunnel callback for apn: " + apnName);
                        return;
                    }
                    if (sessionClosedData.mIkeException != null) {
                        tunnelConfig.setError(
                                getErrorFromIkeException(
                                        sessionClosedData.mIkeException,
                                        tunnelConfig.getIkeSessionState()));
                    }
                    tunnelConfig.getIkeSession().close();
                }
                case EVENT_IKE_SESSION_OPENED -> {
                    IkeSessionOpenedData ikeSessionOpenedData = (IkeSessionOpenedData) msg.obj;
                    apnName = ikeSessionOpenedData.mApnName;
                    IkeSessionConfiguration sessionConfiguration =
                            ikeSessionOpenedData.mIkeSessionConfiguration;
                    tunnelConfig = mApnNameToTunnelConfig.get(apnName);
                    tunnelConfig.setPcscfAddrList(sessionConfiguration.getPcscfServers());
                    boolean enabledFastReauth =
                            IwlanCarrierConfig.getConfigBoolean(
                                    mContext,
                                    mSlotId,
                                    CarrierConfigManager.Iwlan
                                            .KEY_SUPPORTS_EAP_AKA_FAST_REAUTH_BOOL);
                    Log.d(
                            TAG,
                            "CarrierConfigManager.Iwlan.KEY_SUPPORTS_EAP_AKA_FAST_REAUTH_BOOL "
                                    + enabledFastReauth);
                    if (enabledFastReauth) {
                        EapInfo eapInfo = sessionConfiguration.getEapInfo();
                        if (eapInfo instanceof EapAkaInfo eapAkaInfo) {
                            mNextReauthId = eapAkaInfo.getReauthId();
                            Log.d(TAG, "Update ReauthId: " + Arrays.toString(mNextReauthId));
                        } else {
                            mNextReauthId = null;
                        }
                    }
                }
                case EVENT_IKE_SESSION_CONNECTION_INFO_CHANGED -> {
                    IkeSessionConnectionInfoData ikeSessionConnectionInfoData =
                            (IkeSessionConnectionInfoData) msg.obj;
                    Network network =
                            ikeSessionConnectionInfoData.mIkeSessionConnectionInfo.getNetwork();
                    apnName = ikeSessionConnectionInfoData.mApnName;
                    connectivityManager = mContext.getSystemService(ConnectivityManager.class);
                    if (Objects.requireNonNull(connectivityManager).getLinkProperties(network)
                            == null) {
                        Log.e(TAG, "Network " + network + " has null LinkProperties!");
                        return;
                    }
                    tunnelConfig = mApnNameToTunnelConfig.get(apnName);
                    tunnelConfig.setUnderlyingNetwork(network);
                    IpSecManager.IpSecTunnelInterface tunnelInterface = tunnelConfig.getIface();

                    try {
                        tunnelInterface.setUnderlyingNetwork(network);
                    } catch (IOException | IllegalArgumentException e) {
                        Log.e(
                                TAG,
                                "Failed to update underlying network for apn: "
                                        + apnName
                                        + " exception: "
                                        + e);
                    }

                    tunnelConfig
                            .getEpdgTunnelCallback()
                            .onTunnelLinkPropertiesChanged(
                                    apnName, buildTunnelLinkProperties(tunnelConfig));
                }
                case EVENT_IKE_3GPP_DATA_RECEIVED -> {
                    Ike3gppDataReceived ike3gppDataReceived = (Ike3gppDataReceived) msg.obj;
                    apnName = ike3gppDataReceived.mApnName;
                    List<Ike3gppData> ike3gppData = ike3gppDataReceived.mIke3gppData;
                    if (ike3gppData != null && !ike3gppData.isEmpty()) {
                        tunnelConfig = mApnNameToTunnelConfig.get(apnName);
                        for (Ike3gppData payload : ike3gppData) {
                            if (payload.getDataType() == DATA_TYPE_NOTIFY_N1_MODE_INFORMATION) {
                                Log.d(TAG, "Got payload DATA_TYPE_NOTIFY_N1_MODE_INFORMATION");
                                NetworkSliceInfo si =
                                        NetworkSliceSelectionAssistanceInformation.getSliceInfo(
                                                ((Ike3gppN1ModeInformation) payload).getSnssai());
                                if (si != null) {
                                    tunnelConfig.setSliceInfo(si);
                                    Log.d(TAG, "SliceInfo: " + si);
                                }
                            } else if (payload.getDataType() == DATA_TYPE_NOTIFY_BACKOFF_TIMER) {
                                Log.d(TAG, "Got payload DATA_TYPE_NOTIFY_BACKOFF_TIMER");
                                long backoffTime =
                                        decodeBackoffTime(
                                                ((Ike3gppBackoffTimer) payload).getBackoffTimer());
                                if (backoffTime > 0) {
                                    tunnelConfig.setBackoffTime(backoffTime);
                                    Log.d(TAG, "Backoff Timer: " + backoffTime);
                                }
                            }
                        }
                    } else {
                        Log.e(TAG, "Null or empty payloads received:");
                    }
                }
                case EVENT_IKE_LIVENESS_STATUS_CHANGED -> {
                    IkeSessionValidationStatusData ikeLivenessData =
                            (IkeSessionValidationStatusData) msg.obj;
                    @NetworkValidationStatus int validationStatus = ikeLivenessData.mStatus;
                    apnName = ikeLivenessData.mApnName;
                    tunnelConfig = mApnNameToTunnelConfig.get(apnName);
                    if (tunnelConfig == null) {
                        Log.e(TAG, "No tunnel config found for apn: " + apnName);
                        return;
                    }
                    tunnelConfig
                            .getEpdgTunnelCallback()
                            .onNetworkValidationStatusChanged(apnName, validationStatus);
                }
                case EVENT_REQUEST_NETWORK_VALIDATION_CHECK -> {
                    apnName = (String) msg.obj;
                    tunnelConfig = mApnNameToTunnelConfig.get(apnName);
                    if (tunnelConfig == null) {
                        Log.e(TAG, "No tunnel config found for apn: " + apnName);
                        return;
                    }
                    tunnelConfig.getIkeSession().requestLivenessCheck();
                }
                default -> throw new IllegalStateException("Unexpected value: " + msg.what);
            }
        }

        private void handleTunnelBringUpRequest(TunnelRequestWrapper tunnelRequestWrapper) {
            TunnelSetupRequest setupRequest = tunnelRequestWrapper.getSetupRequest();
            String apnName = setupRequest.apnName();

            IwlanError bringUpError = canBringUpTunnel(apnName, setupRequest.isEmergency());
            if (Objects.nonNull(bringUpError)) {
                tunnelRequestWrapper
                        .getEpdgTunnelCallback()
                        .onClosed(
                                apnName,
                                bringUpError,
                                new OnClosedMetrics.Builder().setApnName(apnName).build());
                return;
            }
            serviceTunnelBringUpRequest(tunnelRequestWrapper);
        }

        private void serviceTunnelBringUpRequest(TunnelRequestWrapper tunnelRequestWrapper) {
            if (mEpdgMonitor.hasEpdgConnected()) {
                InetAddress epdgAddress = selectConnectedEpdgForTunnelBringUp(tunnelRequestWrapper);
                if (epdgAddress != null) {
                    onBringUpTunnel(tunnelRequestWrapper, epdgAddress);
                    return;
                }
            }

            if (!isEpdgSelectionOrFirstTunnelBringUpInProgress()) {
                // No tunnel bring-up in progress Or emergency request has attempted ePDG for normal
                // session but failed, select the ePDG address.
                selectEpdgAddress(tunnelRequestWrapper.getSetupRequest());
            }

            // Another bring-up or ePDG selection is in progress, pending this request.
            mPendingBringUpRequests.add(tunnelRequestWrapper);
        }

        private InetAddress selectConnectedEpdgForTunnelBringUp(
                TunnelRequestWrapper tunnelRequestWrapper) {
            if (!mFeatureFlags.distinctEpdgSelectionForEmergencySessions()
                    || !IwlanCarrierConfig.getConfigBoolean(
                            mContext,
                            mSlotId,
                            IwlanCarrierConfig.KEY_DISTINCT_EPDG_FOR_EMERGENCY_ALLOWED_BOOL)) {
                // Attempt on ePDG for normal session since feature not enabled
                return mEpdgMonitor.getEpdgAddressForNormalSession();
            }

            if (!tunnelRequestWrapper.getSetupRequest().isEmergency()) {
                if (mEpdgMonitor.hasSeparateEpdgConnectedForEmergencySession()) {
                    // Attempt on ePDG for emergency session
                    return mEpdgMonitor.getEpdgAddressForEmergencySession();
                } else {
                    // Attempt on ePDG for normal session.
                    return mEpdgMonitor.getEpdgAddressForNormalSession();
                }
            }

            if (!mEpdgMonitor.hasEmergencyPdnFailedWithConnectedEpdg()) {
                // First emergnecy attempt on ePDG for normal session.
                return mEpdgMonitor.getEpdgAddressForNormalSession();
            }
            return null; // no suitable ePDG address found. Select new ePDG address needed.
        }

        TmHandler(Looper looper) {
            super(looper);
        }
    }

    private void closeIkeSession(String apnName, IwlanError error) {
        TunnelConfig tunnelConfig = mApnNameToTunnelConfig.get(apnName);
        tunnelConfig.setError(error);
        tunnelConfig.getIkeSession().close();
    }

    private void selectEpdgAddress(TunnelSetupRequest setupRequest) {
        ++mTransactionId;
        mEpdgServerSelectionStartTime = System.currentTimeMillis();

        final int ipPreference =
                IwlanCarrierConfig.getConfigInt(
                        mContext,
                        mSlotId,
                        CarrierConfigManager.Iwlan.KEY_EPDG_ADDRESS_IP_TYPE_PREFERENCE_INT);

        IpPreferenceConflict ipPreferenceConflict =
                isIpPreferenceConflictsWithNetwork(ipPreference);
        if (ipPreferenceConflict.mIsConflict) {
            sendSelectionRequestComplete(
                    null, new IwlanError(ipPreferenceConflict.mErrorType), mTransactionId);
            return;
        }

        int protoFilter = EpdgSelector.PROTO_FILTER_IPV4V6;
        int epdgAddressOrder = EpdgSelector.SYSTEM_PREFERRED;
        switch (ipPreference) {
            case CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV4_PREFERRED ->
                    epdgAddressOrder = EpdgSelector.IPV4_PREFERRED;
            case CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV6_PREFERRED ->
                    epdgAddressOrder = EpdgSelector.IPV6_PREFERRED;
            case CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV4_ONLY ->
                    protoFilter = EpdgSelector.PROTO_FILTER_IPV4;
            case CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV6_ONLY ->
                    protoFilter = EpdgSelector.PROTO_FILTER_IPV6;
            case CarrierConfigManager.Iwlan.EPDG_ADDRESS_SYSTEM_PREFERRED -> {
                /* Do nothing */
            }
            default -> Log.w(TAG, "Invalid Ip preference : " + ipPreference);
        }

        IwlanError epdgError =
                mEpdgSelector.getValidatedServerList(
                        mTransactionId,
                        protoFilter,
                        epdgAddressOrder,
                        setupRequest.isRoaming(),
                        setupRequest.isEmergency(),
                        mDefaultNetwork,
                        mSelectorCallback);

        if (epdgError.getErrorType() != IwlanError.NO_ERROR) {
            Log.e(TAG, "Epdg address selection failed with error:" + epdgError);
            sendSelectionRequestComplete(null, epdgError, mTransactionId);
        }
    }

    @VisibleForTesting
    int closePendingRequestsForApn(String apnName) {
        int numRequestsClosed = 0;
        int queueSize = mPendingBringUpRequests.size();
        if (queueSize == 0) {
            return numRequestsClosed;
        }

        for (int count = 0; count < queueSize; count++) {
            TunnelRequestWrapper requestWrapper = mPendingBringUpRequests.remove();
            if (requestWrapper.getSetupRequest().apnName().equals(apnName)) {
                requestWrapper
                        .getEpdgTunnelCallback()
                        .onClosed(
                                apnName,
                                new IwlanError(IwlanError.NO_ERROR),
                                new OnClosedMetrics.Builder()
                                        .setApnName(apnName)
                                        .setEpdgServerAddress(null)
                                        .build());
                numRequestsClosed++;
            } else {
                mPendingBringUpRequests.add(requestWrapper);
            }
        }
        return numRequestsClosed;
    }

    @VisibleForTesting
    InetAddress validateAndSetEpdgAddress(List<InetAddress> selectorResultList) {
        if (mEpdgMonitor.hasEmergencyPdnFailedWithConnectedEpdg()
                && selectorResultList
                        .get(0)
                        .equals(mEpdgMonitor.getEpdgAddressForNormalSession())) {
            List<InetAddress> sublist = selectorResultList.subList(1, selectorResultList.size());
            Log.d(
                    TAG,
                    "Selected separate ePDG address for emergency session "
                            + sublist.get(0)
                            + " from available ePDG address list: "
                            + Arrays.toString(selectorResultList.toArray()));
            return sublist.get(0);
        }

        Log.d(
                TAG,
                "Selected first ePDG address "
                        + selectorResultList.get(0)
                        + " from available ePDG address list: "
                        + Arrays.toString(selectorResultList.toArray()));
        return selectorResultList.get(0);
    }

    private void serviceAllPendingRequests() {
        while (!mPendingBringUpRequests.isEmpty()) {
            Log.d(TAG, "serviceAllPendingRequests");
            TunnelRequestWrapper requestWrapper = mPendingBringUpRequests.remove();
            onBringUpTunnel(requestWrapper, mEpdgMonitor.getEpdgAddressForNormalSession());
        }
    }

    private void failAllPendingRequests(IwlanError error) {
        while (!mPendingBringUpRequests.isEmpty()) {
            Log.d(TAG, "failAllPendingRequests");
            TunnelRequestWrapper request = mPendingBringUpRequests.remove();
            TunnelSetupRequest setupRequest = request.getSetupRequest();
            String apnName = setupRequest.apnName();
            reportIwlanError(apnName, error);
            request.getEpdgTunnelCallback()
                    .onClosed(
                            apnName,
                            error,
                            new OnClosedMetrics.Builder()
                                    .setApnName(apnName)
                                    .setEpdgServerAddress(null)
                                    .build());
        }
    }

    // Prints mPendingBringUpRequests
    private void printRequestQueue(String info) {
        Log.d(TAG, info);
        Log.d(
                TAG,
                "mPendingBringUpRequests: " + Arrays.toString(mPendingBringUpRequests.toArray()));
    }

    // Update Network wrapper
    private static final class UpdateNetworkWrapper {
        private final Network mNetwork;
        private final LinkProperties mLinkProperties;

        private UpdateNetworkWrapper(Network network, LinkProperties linkProperties) {
            mNetwork = network;
            mLinkProperties = linkProperties;
        }

        Network getNetwork() {
            return mNetwork;
        }

        LinkProperties getLinkProperties() {
            return mLinkProperties;
        }
    }

    // Tunnel request + tunnel callback
    private static final class TunnelRequestWrapper {
        private final TunnelSetupRequest mSetupRequest;
        private final EpdgTunnelCallback mEpdgTunnelCallback;

        private TunnelRequestWrapper(
                TunnelSetupRequest setupRequest, EpdgTunnelCallback tunnelCallback) {
            mEpdgTunnelCallback = tunnelCallback;
            mSetupRequest = setupRequest;
        }

        TunnelSetupRequest getSetupRequest() {
            return mSetupRequest;
        }

        EpdgTunnelCallback getEpdgTunnelCallback() {
            return mEpdgTunnelCallback;
        }
    }

    private static final class TunnelBringdownRequest {
        final String mApnName;
        final boolean mForceClose;
        final EpdgTunnelCallback mEpdgTunnelCallback;
        final int mBringDownReason;

        private TunnelBringdownRequest(
                String apnName,
                boolean forceClose,
                EpdgTunnelCallback tunnelCallback,
                @TunnelBringDownReason int reason) {
            mApnName = apnName;
            mForceClose = forceClose;
            mEpdgTunnelCallback = tunnelCallback;
            mBringDownReason = reason;
        }
    }

    private static final class EpdgSelectorResult {
        private final List<InetAddress> mValidIpList;

        List<InetAddress> getValidIpList() {
            return mValidIpList;
        }

        IwlanError getEpdgError() {
            return mEpdgError;
        }

        int getTransactionId() {
            return mTransactionId;
        }

        private final IwlanError mEpdgError;
        private final int mTransactionId;

        private EpdgSelectorResult(
                List<InetAddress> validIpList, IwlanError epdgError, int transactionId) {
            mValidIpList = validIpList;
            mEpdgError = epdgError;
            mTransactionId = transactionId;
        }
    }

    // Data received from IkeSessionStateMachine on successful EVENT_CHILD_SESSION_OPENED.
    private static final class TunnelOpenedData extends IkeEventData {
        final List<InetAddress> mInternalDnsServers;
        final List<LinkAddress> mInternalAddresses;

        private TunnelOpenedData(
                String apnName,
                int token,
                List<InetAddress> internalDnsServers,
                List<LinkAddress> internalAddresses) {
            super(apnName, token);
            mInternalDnsServers = internalDnsServers;
            mInternalAddresses = internalAddresses;
        }
    }

    // Data received from IkeSessionStateMachine on successful EVENT_IKE_SESSION_OPENED.
    private static final class IkeSessionOpenedData extends IkeEventData {
        final IkeSessionConfiguration mIkeSessionConfiguration;

        private IkeSessionOpenedData(
                String apnName, int token, IkeSessionConfiguration ikeSessionConfiguration) {
            super(apnName, token);
            mIkeSessionConfiguration = ikeSessionConfiguration;
        }
    }

    private static final class IkeSessionConnectionInfoData extends IkeEventData {
        final IkeSessionConnectionInfo mIkeSessionConnectionInfo;

        private IkeSessionConnectionInfoData(
                String apnName, int token, IkeSessionConnectionInfo ikeSessionConnectionInfo) {
            super(apnName, token);
            mIkeSessionConnectionInfo = ikeSessionConnectionInfo;
        }
    }

    private static final class Ike3gppDataReceived extends IkeEventData {
        final List<Ike3gppData> mIke3gppData;

        private Ike3gppDataReceived(String apnName, int token, List<Ike3gppData> ike3gppData) {
            super(apnName, token);
            mIke3gppData = ike3gppData;
        }
    }

    // Data received from IkeSessionStateMachine if either IKE session or Child session have been
    // closed, normally or exceptionally.
    private static final class SessionClosedData extends IkeEventData {
        final IkeException mIkeException;

        private SessionClosedData(String apnName, int token, IkeException ikeException) {
            super(apnName, token);
            mIkeException = ikeException;
        }
    }

    private static final class IkeSessionValidationStatusData extends IkeEventData {
        @NetworkValidationStatus final int mStatus;

        private IkeSessionValidationStatusData(String apnName, int token, int status) {
            super(apnName, token);
            mStatus = status;
        }
    }

    private static final class IpsecTransformData extends IkeEventData {
        private final IpSecTransform mTransform;
        private final int mDirection;

        private IpsecTransformData(
                IpSecTransform transform, int direction, String apnName, int token) {
            super(apnName, token);
            mTransform = transform;
            mDirection = direction;
        }

        IpSecTransform getTransform() {
            return mTransform;
        }

        int getDirection() {
            return mDirection;
        }

        String getApnName() {
            return super.mApnName;
        }
    }

    private abstract static class IkeEventData {
        final String mApnName;
        final int mToken;

        private IkeEventData(String apnName, int token) {
            mApnName = apnName;
            mToken = token;
        }
    }

    private static class IpPreferenceConflict {
        final boolean mIsConflict;
        final int mErrorType;

        private IpPreferenceConflict(boolean isConflict, int errorType) {
            mIsConflict = isConflict;
            mErrorType = errorType;
        }

        private IpPreferenceConflict() {
            mIsConflict = false;
            mErrorType = IwlanError.NO_ERROR;
        }
    }

    private int[] getRetransmissionTimeoutsFromConfig() {
        int[] timeList =
                IwlanCarrierConfig.getConfigIntArray(
                        mContext,
                        mSlotId,
                        CarrierConfigManager.Iwlan.KEY_RETRANSMIT_TIMER_MSEC_INT_ARRAY);
        boolean isValid =
                timeList != null
                        && timeList.length != 0
                        && timeList.length <= IKE_RETRANS_MAX_ATTEMPTS_MAX;
        for (int time : Objects.requireNonNull(timeList)) {
            if (time < IKE_RETRANS_TIMEOUT_MS_MIN || time > IKE_RETRANS_TIMEOUT_MS_MAX) {
                isValid = false;
                break;
            }
        }
        if (!isValid) {
            timeList =
                    IwlanCarrierConfig.getDefaultConfigIntArray(
                            CarrierConfigManager.Iwlan.KEY_RETRANSMIT_TIMER_MSEC_INT_ARRAY);
        }
        Log.d(TAG, "getRetransmissionTimeoutsFromConfig: " + Arrays.toString(timeList));
        return timeList;
    }

    private int getDpdDelayFromConfig() {
        int dpdDelay =
                IwlanCarrierConfig.getConfigInt(
                        mContext, mSlotId, CarrierConfigManager.Iwlan.KEY_DPD_TIMER_SEC_INT);
        if (dpdDelay < IKE_DPD_DELAY_SEC_MIN || dpdDelay > IKE_DPD_DELAY_SEC_MAX) {
            dpdDelay =
                    IwlanCarrierConfig.getDefaultConfigInt(
                            CarrierConfigManager.Iwlan.KEY_DPD_TIMER_SEC_INT);
        }
        return dpdDelay;
    }

    /**
     * Decodes backoff time as per TS 124 008 10.5.7.4a Bits 5 to 1 represent the binary coded timer
     * value
     *
     * <p>Bits 6 to 8 defines the timer value unit for the GPRS timer as follows: Bits 8 7 6 0 0 0
     * value is incremented in multiples of 10 minutes 0 0 1 value is incremented in multiples of 1
     * hour 0 1 0 value is incremented in multiples of 10 hours 0 1 1 value is incremented in
     * multiples of 2 seconds 1 0 0 value is incremented in multiples of 30 seconds 1 0 1 value is
     * incremented in multiples of 1 minute 1 1 0 value is incremented in multiples of 1 hour 1 1 1
     * value indicates that the timer is deactivated.
     *
     * @param backoffTimeByte Byte value obtained from ike
     * @return long time value in seconds. -1 if the timer needs to be deactivated.
     */
    private static long decodeBackoffTime(byte backoffTimeByte) {
        final int BACKOFF_TIME_VALUE_MASK = 0x1F;
        final int BACKOFF_TIMER_UNIT_MASK = 0xE0;
        final Long[] BACKOFF_TIMER_UNIT_INCREMENT_SECS = {
            10L * 60L, // 10 minutes
            60L * 60L, // 1 hour
            10L * 60L * 60L, // 10 hours
            2L, // 2 seconds
            30L, // 30 seconds
            60L, // 1 minute
            60L * 60L, // 1 hour
        };

        long time = backoffTimeByte & BACKOFF_TIME_VALUE_MASK;
        int timerUnit = (backoffTimeByte & BACKOFF_TIMER_UNIT_MASK) >> 5;
        if (timerUnit >= BACKOFF_TIMER_UNIT_INCREMENT_SECS.length) {
            return -1;
        }
        time *= BACKOFF_TIMER_UNIT_INCREMENT_SECS[timerUnit];
        return time;
    }

    @VisibleForTesting
    String getTunnelSetupRequestApnName(TunnelSetupRequest setupRequest) {
        return setupRequest.apnName();
    }

    @VisibleForTesting
    void putApnNameToTunnelConfig(
            String apnName,
            IkeSession ikeSession,
            EpdgTunnelCallback tunnelCallback,
            IpSecManager.IpSecTunnelInterface iface,
            InetAddress srcIpv6Addr,
            int srcIPv6AddrPrefixLen,
            boolean isEmergency,
            InetAddress epdgAddress,
            Network underlyingNetwork) {
        mApnNameToTunnelConfig.put(
                apnName,
                new TunnelConfig(
                        ikeSession,
                        tunnelCallback,
                        iface,
                        srcIpv6Addr,
                        srcIPv6AddrPrefixLen,
                        isEmergency,
                        epdgAddress,
                        underlyingNetwork));
        Log.d(TAG, "Added APN: " + apnName + " to TunnelConfig");
    }

    @VisibleForTesting
    int incrementAndGetCurrentTokenForApn(String apnName) {
        final int currentToken =
                mApnNameToCurrentToken.compute(
                        apnName, (apn, token) -> token == null ? 0 : token + 1);
        Log.d(TAG, "Added token: " + currentToken + " for apn: " + apnName);
        return currentToken;
    }

    @VisibleForTesting
    boolean isTunnelConfigContainExistApn(String apnName) {
        return mApnNameToTunnelConfig.containsKey(apnName);
    }

    @VisibleForTesting
    List<InetAddress> getAddressForNetwork(Network network) {
        return IwlanHelper.getAllAddressesForNetwork(mContext, network);
    }

    @VisibleForTesting
    IkeSessionCreator getIkeSessionCreator() {
        return mIkeSessionCreator;
    }

    @VisibleForTesting
    void sendSelectionRequestComplete(
            List<InetAddress> validIPList, IwlanError result, int transactionId) {
        mEpdgServerSelectionDuration = System.currentTimeMillis() - mEpdgServerSelectionStartTime;
        mEpdgServerSelectionStartTime = 0;
        EpdgSelectorResult epdgSelectorResult =
                new EpdgSelectorResult(validIPList, result, transactionId);
        mHandler.obtainMessage(EVENT_EPDG_ADDRESS_SELECTION_REQUEST_COMPLETE, epdgSelectorResult)
                .sendToTarget();
    }

    static boolean isValidApnProtocol(int proto) {
        return (proto == ApnSetting.PROTOCOL_IP
                || proto == ApnSetting.PROTOCOL_IPV4V6
                || proto == ApnSetting.PROTOCOL_IPV6);
    }

    boolean isObsoleteToken(String apnName, int token) {
        if (!mApnNameToCurrentToken.containsKey(apnName)) {
            return true;
        }
        return token != mApnNameToCurrentToken.get(apnName);
    }

    private static String eventToString(int event) {
        return switch (event) {
            case EVENT_TUNNEL_BRINGUP_REQUEST -> "EVENT_TUNNEL_BRINGUP_REQUEST";
            case EVENT_TUNNEL_BRINGDOWN_REQUEST -> "EVENT_TUNNEL_BRINGDOWN_REQUEST";
            case EVENT_CHILD_SESSION_OPENED -> "EVENT_CHILD_SESSION_OPENED";
            case EVENT_CHILD_SESSION_CLOSED -> "EVENT_CHILD_SESSION_CLOSED";
            case EVENT_IKE_SESSION_CLOSED -> "EVENT_IKE_SESSION_CLOSED";
            case EVENT_EPDG_ADDRESS_SELECTION_REQUEST_COMPLETE ->
                    "EVENT_EPDG_ADDRESS_SELECTION_REQUEST_COMPLETE";
            case EVENT_IPSEC_TRANSFORM_CREATED -> "EVENT_IPSEC_TRANSFORM_CREATED";
            case EVENT_IPSEC_TRANSFORM_DELETED -> "EVENT_IPSEC_TRANSFORM_DELETED";
            case EVENT_UPDATE_NETWORK -> "EVENT_UPDATE_NETWORK";
            case EVENT_IKE_SESSION_OPENED -> "EVENT_IKE_SESSION_OPENED";
            case EVENT_IKE_SESSION_CONNECTION_INFO_CHANGED ->
                    "EVENT_IKE_SESSION_CONNECTION_INFO_CHANGED";
            case EVENT_IKE_3GPP_DATA_RECEIVED -> "EVENT_IKE_3GPP_DATA_RECEIVED";
            case EVENT_IKE_LIVENESS_STATUS_CHANGED -> "EVENT_IKE_LIVENESS_STATUS_CHANGED";
            case EVENT_REQUEST_NETWORK_VALIDATION_CHECK -> "EVENT_REQUEST_NETWORK_VALIDATION_CHECK";
            default -> "Unknown(" + event + ")";
        };
    }

    @VisibleForTesting
    TmIkeSessionCallback getTmIkeSessionCallback(String apnName, int token) {
        return new TmIkeSessionCallback(apnName, token);
    }

    @VisibleForTesting
    void onConnectedToEpdg(boolean hasConnected) {
        mHasConnectedToEpdg = hasConnected;
        if (mHasConnectedToEpdg) {
            mIkeSessionNetwork = mDefaultNetwork;
        } else {
            mIkeSessionNetwork = null;
        }
    }

    @VisibleForTesting
    TunnelConfig getTunnelConfigForApn(String apnName) {
        return mApnNameToTunnelConfig.get(apnName);
    }

    @VisibleForTesting
    int getCurrentTokenForApn(String apnName) {
        if (!mApnNameToCurrentToken.containsKey(apnName)) {
            throw new IllegalArgumentException("There is no token for apn: " + apnName);
        }
        return mApnNameToCurrentToken.get(apnName);
    }

    @VisibleForTesting
    long reportIwlanError(String apnName, IwlanError error) {
        triggerUnderlyingNetworkValidationOnError(error);
        return ErrorPolicyManager.getInstance(mContext, mSlotId).reportIwlanError(apnName, error);
    }

    @VisibleForTesting
    long reportIwlanError(String apnName, IwlanError error, long backOffTime) {
        triggerUnderlyingNetworkValidationOnError(error);
        return ErrorPolicyManager.getInstance(mContext, mSlotId)
                .reportIwlanError(apnName, error, backOffTime);
    }

    @VisibleForTesting
    IwlanError getLastError(String apnName) {
        return ErrorPolicyManager.getInstance(mContext, mSlotId).getLastError(apnName);
    }

    @VisibleForTesting
    IwlanError canBringUpTunnel(String apnName, boolean isEmergency) {
        IwlanError bringUpError = null;
        if (IwlanHelper.getSubId(mContext, mSlotId)
                == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
            Log.e(TAG, "SIM isn't ready");
            bringUpError = new IwlanError(IwlanError.SIM_NOT_READY_EXCEPTION);
            reportIwlanError(apnName, bringUpError);
        } else if (Objects.isNull(mDefaultNetwork)) {
            Log.e(TAG, "The default network is not ready");
            bringUpError = new IwlanError(IwlanError.IKE_INTERNAL_IO_EXCEPTION);
            reportIwlanError(apnName, bringUpError);
        } else if (!isEmergency
                && !ErrorPolicyManager.getInstance(mContext, mSlotId).canBringUpTunnel(apnName)) {
            // TODO(b/343962773): Need to refactor emergency condition into ErrorPolicyManager
            Log.d(TAG, "Cannot bring up tunnel as retry time has not passed");
            bringUpError = getLastError(apnName);
        }
        return bringUpError;
    }

    @VisibleForTesting
    IpPreferenceConflict isIpPreferenceConflictsWithNetwork(
            @CarrierConfigManager.Iwlan.EpdgAddressIpPreference int ipPreference) {
        List<InetAddress> localAddresses = getAddressForNetwork(mDefaultNetwork);
        if (localAddresses == null || localAddresses.size() == 0) {
            Log.e(TAG, "No local addresses available for Network " + mDefaultNetwork);
            return new IpPreferenceConflict(true, IwlanError.EPDG_SELECTOR_SERVER_SELECTION_FAILED);
        } else if (!IwlanHelper.hasIpv6Address(localAddresses)
                && ipPreference == CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV6_ONLY) {
            Log.e(
                    TAG,
                    "ePDG IP preference: "
                            + ipPreference
                            + " conflicts with source IP type: "
                            + EpdgSelector.PROTO_FILTER_IPV4);
            return new IpPreferenceConflict(true, IwlanError.EPDG_ADDRESS_ONLY_IPV6_ALLOWED);
        } else if (!IwlanHelper.hasIpv4Address(localAddresses)
                && ipPreference == CarrierConfigManager.Iwlan.EPDG_ADDRESS_IPV4_ONLY) {
            // b/209938719 allows Iwlan to support VoWiFi for IPv4 ePDG server while on IPv6 WiFi.
            // Iwlan will receive a IPv4 address which is embedded in stacked IPv6 address. By using
            // this IPv4 address, UE will connect to IPv4 ePDG server through XLAT. However, there
            // are issues on connecting ePDG server through XLAT. Will allow IPV4_ONLY on IPv6 WiFi
            // after the issues are resolved.
            Log.e(
                    TAG,
                    "ePDG IP preference: "
                            + ipPreference
                            + " conflicts with source IP type: "
                            + EpdgSelector.PROTO_FILTER_IPV6);
            return new IpPreferenceConflict(true, IwlanError.EPDG_ADDRESS_ONLY_IPV4_ALLOWED);
        }
        return new IpPreferenceConflict();
    }

    private boolean isUnderlyingNetworkValidated(Network network) {
        if (network == null) return false;

        ConnectivityManager connectivityManager =
                Objects.requireNonNull(mContext).getSystemService(ConnectivityManager.class);
        NetworkCapabilities networkCapabilities =
                connectivityManager.getNetworkCapabilities(network);
        return (networkCapabilities != null)
                && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
    }

    /**
     * Trigger network validation on the underlying network if needed to possibly update validation
     * status and cause system switch default network.
     */
    void triggerUnderlyingNetworkValidationOnError(IwlanError error) {
        if (!isUnderlyingNetworkValidationRequired(error.getErrorType())) {
            return;
        }

        Log.d(TAG, "On triggering underlying network validation. Cause: " + error);
        validateUnderlyingNetwork(IwlanCarrierConfig.NETWORK_VALIDATION_EVENT_NO_RESPONSE);
    }

    public void validateUnderlyingNetwork(@IwlanCarrierConfig.NetworkValidationEvent int event) {
        int[] networkValidationEvents =
                IwlanCarrierConfig.getConfigIntArray(
                        mContext,
                        mSlotId,
                        IwlanCarrierConfig.KEY_UNDERLYING_NETWORK_VALIDATION_EVENTS_INT_ARRAY);
        if (Arrays.stream(networkValidationEvents)
                .noneMatch(validationEvent -> validationEvent == event)) {
            return;
        }
        synchronized (sLastUnderlyingNetworkValidationLock) {
            long now = IwlanHelper.elapsedRealtime();
            // TODO (b/356791418): Consolidate underlying network handling into a single centralized
            //  subcomponent to prevent duplicate processing across different threads and classes
            //  . Until then, we will prevent sending duplicate network validations by checking
            //  the recent trigger time.
            if (now - sLastUnderlyingNetworkValidationMs > NETWORK_VALIDATION_MIN_INTERVAL_MS) {
                sLastUnderlyingNetworkValidationMs = now;
                Log.d(
                        TAG,
                        "On triggering underlying network validation. Event: "
                                + IwlanCarrierConfig.getUnderlyingNetworkValidationEventString(
                                        event));
                mHandler.post(() -> onTriggerUnderlyingNetworkValidation(event));
            }
        }
    }

    private void onTriggerUnderlyingNetworkValidation(int event) {
        if (mDefaultNetwork == null) return;

        setupValidationMetricsAtom(mDefaultNetwork, event);

        if (!isUnderlyingNetworkValidated(mDefaultNetwork)) {
            Log.d(TAG, "Network " + mDefaultNetwork + " is already not validated.");
            reportValidationMetricsAtom(
                    mDefaultNetwork,
                    NETWORK_VALIDATION_RESULT_INVALID,
                    /* validationTriggered */ false);
            return;
        }

        ConnectivityManager connectivityManager =
                Objects.requireNonNull(mContext).getSystemService(ConnectivityManager.class);
        Log.d(TAG, "Trigger underlying network validation on network: " + mDefaultNetwork);
        Objects.requireNonNull(connectivityManager)
                .reportNetworkConnectivity(mDefaultNetwork, false);
    }

    private void setupValidationMetricsAtom(@NonNull Network network, int event) {
        MetricsAtom metricsAtom = new MetricsAtom();
        metricsAtom.setMessageId(IwlanStatsLog.IWLAN_UNDERLYING_NETWORK_VALIDATION_RESULT_REPORTED);
        metricsAtom.setTriggerReason(getMetricsTriggerReason(event));

        ConnectivityManager connectivityManager =
                Objects.requireNonNull(mContext).getSystemService(ConnectivityManager.class);
        NetworkCapabilities networkCapabilities =
                Objects.requireNonNull(connectivityManager).getNetworkCapabilities(mDefaultNetwork);
        int validationTransportType = NETWORK_VALIDATION_TRANSPORT_TYPE_UNSPECIFIED;
        if (networkCapabilities != null) {
            if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
                validationTransportType = NETWORK_VALIDATION_TRANSPORT_TYPE_CELLULAR;
            } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
                validationTransportType = NETWORK_VALIDATION_TRANSPORT_TYPE_WIFI;
            }
        }
        metricsAtom.setValidationTransportType(validationTransportType);

        metricsAtom.setValidationStartTimeMills(IwlanHelper.elapsedRealtime());
        mMetricsAtomForNetwork.put(network, metricsAtom);
    }

    boolean isUnderlyingNetworkValidationRequired(int error) {
        return switch (error) {
            case IwlanError.EPDG_SELECTOR_SERVER_SELECTION_FAILED,
                    IwlanError.IKE_NETWORK_LOST_EXCEPTION,
                    IwlanError.IKE_INIT_TIMEOUT,
                    IwlanError.IKE_MOBILITY_TIMEOUT,
                    IwlanError.IKE_DPD_TIMEOUT ->
                    true;
            default -> false;
        };
    }

    private int getMetricsValidationResult(int validationResult) {
        return switch (validationResult) {
            case ConnectivityReport.NETWORK_VALIDATION_RESULT_INVALID ->
                    NETWORK_VALIDATION_RESULT_INVALID;
            case ConnectivityReport.NETWORK_VALIDATION_RESULT_VALID ->
                    NETWORK_VALIDATION_RESULT_VALID;
            case ConnectivityReport.NETWORK_VALIDATION_RESULT_PARTIALLY_VALID ->
                    NETWORK_VALIDATION_RESULT_PARTIALLY_VALID;
            case ConnectivityReport.NETWORK_VALIDATION_RESULT_SKIPPED ->
                    NETWORK_VALIDATION_RESULT_SKIPPED;
            default -> NETWORK_VALIDATION_RESULT_UNSPECIFIED;
        };
    }

    private int getMetricsTriggerReason(int reason) {
        return switch (reason) {
            case IwlanCarrierConfig.NETWORK_VALIDATION_EVENT_MAKING_CALL ->
                    NETWORK_VALIDATION_EVENT_MAKING_CALL;
            case IwlanCarrierConfig.NETWORK_VALIDATION_EVENT_SCREEN_ON ->
                    NETWORK_VALIDATION_EVENT_SCREEN_ON;
            case IwlanCarrierConfig.NETWORK_VALIDATION_EVENT_NO_RESPONSE ->
                    NETWORK_VALIDATION_EVENT_NO_RESPONSE;
            default -> NETWORK_VALIDATION_EVENT_UNSPECIFIED;
        };
    }

    /**
     * Performs network validation check
     *
     * @param apnName APN for which to perform validation check
     */
    public void requestNetworkValidationForApn(String apnName) {
        mHandler.obtainMessage(EVENT_REQUEST_NETWORK_VALIDATION_CHECK, apnName).sendToTarget();
    }

    @VisibleForTesting
    protected void removeApnNameInTunnelConfig(String apnName) {
        mApnNameToTunnelConfig.remove(apnName);
    }

    public void prefetchEpdgServerList(Network network, boolean isRoaming) {
        mEpdgSelector.getValidatedServerList(
                0 /* transactionId */,
                EpdgSelector.PROTO_FILTER_IPV4V6,
                EpdgSelector.SYSTEM_PREFERRED,
                isRoaming,
                false /* isEmergency */,
                network,
                null /* selectorCallback */);
        mEpdgSelector.getValidatedServerList(
                0 /* transactionId */,
                EpdgSelector.PROTO_FILTER_IPV4V6,
                EpdgSelector.SYSTEM_PREFERRED,
                isRoaming,
                true /* isEmergency */,
                network,
                null /* selectorCallback */);
    }

    public void close() {
        mTunnelManagerInstances.remove(mSlotId);
        unregisterConnectivityDiagnosticsCallback();
    }

    public void dump(PrintWriter pw) {
        pw.println("---- EpdgTunnelManager ----");
        pw.println(
                "Has ePDG connected for normal session: "
                        + mEpdgMonitor.hasEpdgConnectedForNormalSession());
        pw.println(
                "Has Separate ePDG connected for emergency session: "
                        + mEpdgMonitor.hasSeparateEpdgConnectedForEmergencySession());
        pw.println("mIkeSessionNetwork: " + mIkeSessionNetwork);
        if (mEpdgMonitor.getEpdgAddressForNormalSession() != null) {
            pw.println(
                    "EpdgAddressForNormalSession: "
                            + mEpdgMonitor.getEpdgAddressForNormalSession());
        }
        if (mEpdgMonitor.getEpdgAddressForEmergencySession() != null) {
            pw.println(
                    "SeparateEpdgAddressForEmergencySession: "
                            + mEpdgMonitor.getEpdgAddressForEmergencySession());
        }
        pw.println("mApnNameToTunnelConfig:\n");
        for (Map.Entry<String, TunnelConfig> entry : mApnNameToTunnelConfig.entrySet()) {
            pw.println("APN: " + entry.getKey());
            pw.println("TunnelConfig: " + entry.getValue());
            pw.println();
        }
        pw.println("---------------------------");
    }
}
