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

import static android.net.ipsec.ike.IkeManager.getIkeLog;
import static android.net.ipsec.ike.SaProposal.DH_GROUP_NONE;
import static android.net.ipsec.ike.exceptions.IkeException.wrapAsIkeException;
import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE;

import static com.android.internal.net.ipsec.ike.IkeSessionStateMachine.BUNDLE_KEY_CHILD_REMOTE_SPI;
import static com.android.internal.net.ipsec.ike.IkeSessionStateMachine.CMD_ALARM_FIRED;
import static com.android.internal.net.ipsec.ike.message.IkeHeader.EXCHANGE_TYPE_CREATE_CHILD_SA;
import static com.android.internal.net.ipsec.ike.message.IkeHeader.EXCHANGE_TYPE_IKE_AUTH;
import static com.android.internal.net.ipsec.ike.message.IkeHeader.EXCHANGE_TYPE_INFORMATIONAL;
import static com.android.internal.net.ipsec.ike.message.IkeHeader.ExchangeType;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.IKE_EXCHANGE_SUBTYPE_DELETE_CHILD;
import static com.android.internal.net.ipsec.ike.message.IkeMessage.IKE_EXCHANGE_SUBTYPE_REKEY_CHILD;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_REKEY_SA;
import static com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NOTIFY_TYPE_USE_TRANSPORT_MODE;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_CP;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_DELETE;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_KE;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_NONCE;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_NOTIFY;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_SA;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_TS_INITIATOR;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PAYLOAD_TYPE_TS_RESPONDER;
import static com.android.internal.net.ipsec.ike.message.IkePayload.PROTOCOL_ID_ESP;
import static com.android.internal.net.ipsec.ike.utils.IkeAlarm.IkeAlarmConfig;
import static com.android.internal.net.ipsec.ike.utils.IkeAlarm.buildIkeAlarmIntent;
import static com.android.internal.net.ipsec.ike.utils.IkeAlarmReceiver.ACTION_DELETE_CHILD;
import static com.android.internal.net.ipsec.ike.utils.IkeAlarmReceiver.ACTION_REKEY_CHILD;

import android.annotation.IntDef;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.net.IpSecManager;
import android.net.IpSecManager.ResourceUnavailableException;
import android.net.IpSecManager.SecurityParameterIndex;
import android.net.IpSecManager.SpiUnavailableException;
import android.net.IpSecManager.UdpEncapsulationSocket;
import android.net.IpSecTransform;
import android.net.ipsec.ike.ChildSaProposal;
import android.net.ipsec.ike.ChildSessionCallback;
import android.net.ipsec.ike.ChildSessionConfiguration;
import android.net.ipsec.ike.ChildSessionParams;
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.IkeProtocolException;
import android.net.ipsec.ike.exceptions.InvalidKeException;
import android.net.ipsec.ike.exceptions.InvalidSyntaxException;
import android.net.ipsec.ike.exceptions.NoValidProposalChosenException;
import android.net.ipsec.ike.exceptions.TemporaryFailureException;
import android.net.ipsec.ike.exceptions.TsUnacceptableException;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Pair;
import android.util.SparseArray;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.net.ipsec.ike.IkeLocalRequestScheduler.ChildLocalRequest;
import com.android.internal.net.ipsec.ike.IkeLocalRequestScheduler.LocalRequestFactory;
import com.android.internal.net.ipsec.ike.SaRecord.ChildSaRecord;
import com.android.internal.net.ipsec.ike.SaRecord.SaLifetimeAlarmScheduler;
import com.android.internal.net.ipsec.ike.crypto.IkeCipher;
import com.android.internal.net.ipsec.ike.crypto.IkeMacIntegrity;
import com.android.internal.net.ipsec.ike.crypto.IkeMacPrf;
import com.android.internal.net.ipsec.ike.message.IkeConfigPayload;
import com.android.internal.net.ipsec.ike.message.IkeConfigPayload.ConfigAttribute;
import com.android.internal.net.ipsec.ike.message.IkeDeletePayload;
import com.android.internal.net.ipsec.ike.message.IkeKePayload;
import com.android.internal.net.ipsec.ike.message.IkeMessage.IkeExchangeSubType;
import com.android.internal.net.ipsec.ike.message.IkeNoncePayload;
import com.android.internal.net.ipsec.ike.message.IkeNotifyPayload;
import com.android.internal.net.ipsec.ike.message.IkeNotifyPayload.NotifyType;
import com.android.internal.net.ipsec.ike.message.IkePayload;
import com.android.internal.net.ipsec.ike.message.IkeSaPayload;
import com.android.internal.net.ipsec.ike.message.IkeSaPayload.ChildProposal;
import com.android.internal.net.ipsec.ike.message.IkeSaPayload.DhGroupTransform;
import com.android.internal.net.ipsec.ike.message.IkeTsPayload;
import com.android.internal.net.ipsec.ike.shim.ShimUtils;
import com.android.internal.net.ipsec.ike.utils.IkeMetrics;
import com.android.internal.net.ipsec.ike.utils.IpSecSpiGenerator;
import com.android.internal.net.ipsec.ike.utils.RandomnessFactory;
import com.android.internal.util.State;

import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.InetAddress;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;

/**
 * ChildSessionStateMachine tracks states and manages exchanges of this Child Session.
 *
 * <p>ChildSessionStateMachine has two types of states. One type are states where there is no
 * ongoing procedure affecting Child Session (non-procedure state), including Initial, Idle and
 * Receiving. All other states are "procedure" states which are named as follows:
 *
 * <pre>
 * State Name = [Procedure Type] + [Exchange Initiator] + [Exchange Type].
 * - An IKE procedure consists of one or two IKE exchanges:
 *      Procedure Type = {CreateChild | DeleteChild | Info | RekeyChild | SimulRekeyChild}.
 * - Exchange Initiator indicates whether local or remote peer is the exchange initiator:
 *      Exchange Initiator = {Local | Remote}
 * - Exchange type defines the function of this exchange.
 *      Exchange Type = {Create | Delete}
 * </pre>
 */
public class ChildSessionStateMachine extends AbstractSessionStateMachine {
    private static final String TAG = "ChildSessionStateMachine";

    private static final int SPI_NOT_REGISTERED = 0;

    private static final int CMD_GENERAL_BASE = CMD_PRIVATE_BASE;

    /** Receive request for negotiating first Child SA. */
    private static final int CMD_HANDLE_FIRST_CHILD_EXCHANGE = CMD_GENERAL_BASE + 1;
    /** Receive a request from the remote. */
    private static final int CMD_HANDLE_RECEIVED_REQUEST = CMD_GENERAL_BASE + 2;
    /** Receive a reponse from the remote. */
    private static final int CMD_HANDLE_RECEIVED_RESPONSE = CMD_GENERAL_BASE + 3;

    private static final SparseArray<String> CMD_TO_STR;

    static {
        CMD_TO_STR = new SparseArray<>();
        CMD_TO_STR.put(CMD_HANDLE_FIRST_CHILD_EXCHANGE, "Handle First Child");
        CMD_TO_STR.put(CMD_HANDLE_RECEIVED_REQUEST, "Rcv request");
        CMD_TO_STR.put(CMD_HANDLE_RECEIVED_RESPONSE, "Rcv response");
    }

    private final int mIkeSessionId;
    private final Handler mIkeHandler;
    private final IpSecManager mIpSecManager;

    /**
     * mIpSecSpiGenerator will be used by all Child SA creations in this Child Session to avoid SPI
     * collision in test mode.
     */
    private final IpSecSpiGenerator mIpSecSpiGenerator;

    private final LocalRequestFactory mLocalRequestFactory = new LocalRequestFactory();

    /** User provided configurations. */
    @VisibleForTesting final ChildSessionParams mChildSessionParams;

    private final ChildSessionCallback mUserCallback;

    /** Callback to notify IKE Session the state changes. */
    private final IChildSessionSmCallback mChildSmCallback;

    // TODO: Also store ChildSessionCallback for notifying users.

    /** Local address assigned on device. */
    @VisibleForTesting InetAddress mLocalAddress;
    /** Remote address configured by users. */
    @VisibleForTesting InetAddress mRemoteAddress;

    /**
     * UDP-Encapsulated socket that allows IPsec traffic to pass through a NAT. Null if UDP
     * encapsulation is not needed.
     */
    @VisibleForTesting @Nullable UdpEncapsulationSocket mUdpEncapSocket;

    /** Crypto parameters. Updated upon initial negotiation or IKE SA rekey. */
    @VisibleForTesting IkeMacPrf mIkePrf;

    @VisibleForTesting byte[] mSkD;

    /**
     * Negotiated IKE DH group
     *
     * <p>First Child SA, and all additional Child SAs that do not have user specified DH group are
     * set up with crypto keys that are implicitly generated by the negotiated IKE DH group. For
     * those Child SAs, incoming rekey requests that match the negotiated IKE DH group should also
     * be acceptable. This for improving the interoperability with other IKE implementations.
     */
    @VisibleForTesting int mIkeDhGroup;

    /** Package private ChildSaProposal that represents the negotiated Child SA proposal. */
    @VisibleForTesting ChildSaProposal mSaProposal;

    /** Negotiated local Traffic Selector. */
    @VisibleForTesting IkeTrafficSelector[] mLocalTs;
    /** Negotiated remote Traffic Selector. */
    @VisibleForTesting IkeTrafficSelector[] mRemoteTs;

    @VisibleForTesting IkeCipher mChildCipher;
    @VisibleForTesting IkeMacIntegrity mChildIntegrity;

    /** Package private */
    @VisibleForTesting ChildSaRecord mCurrentChildSaRecord;
    /** Package private */
    @VisibleForTesting ChildSaRecord mLocalInitNewChildSaRecord;
    /** Package private */
    @VisibleForTesting ChildSaRecord mRemoteInitNewChildSaRecord;

    /** Package private */
    @VisibleForTesting ChildSaRecord mChildSaRecordSurviving;

    @VisibleForTesting final State mKillChildSessionParent = new KillChildSessionParent();

    @VisibleForTesting final State mInitial = new Initial();
    @VisibleForTesting final State mCreateChildLocalCreate = new CreateChildLocalCreate();
    @VisibleForTesting final State mIdle = new Idle();
    @VisibleForTesting final State mIdleWithDeferredRequest = new IdleWithDeferredRequest();
    @VisibleForTesting final State mClosedAndAwaitResponse = new ClosedAndAwaitResponse();
    @VisibleForTesting final State mDeleteChildLocalDelete = new DeleteChildLocalDelete();
    @VisibleForTesting final State mDeleteChildRemoteDelete = new DeleteChildRemoteDelete();
    @VisibleForTesting final State mRekeyChildLocalCreate = new RekeyChildLocalCreate();
    @VisibleForTesting final State mMobikeRekeyChildLocalCreate = new MobikeRekeyChildLocalCreate();
    @VisibleForTesting final State mRekeyChildRemoteCreate = new RekeyChildRemoteCreate();
    @VisibleForTesting final State mRekeyChildLocalDelete = new RekeyChildLocalDelete();
    @VisibleForTesting final State mRekeyChildRemoteDelete = new RekeyChildRemoteDelete();
    @VisibleForTesting boolean mIsFirstChild;

    /**
     * Builds a new uninitialized ChildSessionStateMachine
     *
     * <p>Upon creation, this state machine will await either the handleFirstChildExchange
     * (IKE_AUTH), or the createChildSession (Additional child creation beyond the first child) to
     * be called, both of which must pass keying and SA information.
     *
     * <p>This two-stage initialization is required to allow race-free user interaction with the IKE
     * Session keyed on the child state machine callbacks.
     *
     * <p>Package private
     */
    ChildSessionStateMachine(
            IkeContext ikeContext,
            ChildSessionStateMachine.Config childSmConfig,
            ChildSessionCallback userCallback,
            IChildSessionSmCallback childSmCallback) {
        super(TAG, ikeContext, childSmConfig.userCbExecutor);

        mIkeSessionId = childSmConfig.ikeSessionId;
        mIkeHandler = childSmConfig.ikeHandler;
        mIpSecManager = childSmConfig.ipSecManager;
        mIpSecSpiGenerator = childSmConfig.ipSecSpiGenerator;

        mChildSessionParams = childSmConfig.sessionParams;
        mUserCallback = userCallback;
        mChildSmCallback = childSmCallback;

        addState(mKillChildSessionParent);

        addState(mInitial, mKillChildSessionParent);
        addState(mCreateChildLocalCreate, mKillChildSessionParent);
        addState(mIdle, mKillChildSessionParent);
        addState(mIdleWithDeferredRequest, mKillChildSessionParent);
        addState(mClosedAndAwaitResponse, mKillChildSessionParent);
        addState(mDeleteChildLocalDelete, mKillChildSessionParent);
        addState(mDeleteChildRemoteDelete, mKillChildSessionParent);
        addState(mRekeyChildLocalCreate, mKillChildSessionParent);
        addState(mMobikeRekeyChildLocalCreate, mKillChildSessionParent);
        addState(mRekeyChildRemoteCreate, mKillChildSessionParent);
        addState(mRekeyChildLocalDelete, mKillChildSessionParent);
        addState(mRekeyChildRemoteDelete, mKillChildSessionParent);

        setInitialState(mInitial);
    }

    // Configurations provided by an IKE Session for building the Child Session
    static class Config {
        public final int ikeSessionId;
        public final Handler ikeHandler;
        public final ChildSessionParams sessionParams;
        public final IpSecManager ipSecManager;
        public final IpSecSpiGenerator ipSecSpiGenerator;
        public final Executor userCbExecutor;

        Config(
                int ikeSessionId,
                Handler ikeHandler,
                ChildSessionParams sessionParams,
                IpSecManager ipSecManager,
                IpSecSpiGenerator ipSecSpiGenerator,
                Executor userCbExecutor) {
            this.ikeSessionId = ikeSessionId;
            this.ikeHandler = ikeHandler;
            this.sessionParams = sessionParams;
            this.ipSecManager = ipSecManager;
            this.ipSecSpiGenerator = ipSecSpiGenerator;
            this.userCbExecutor = userCbExecutor;
        }
    }

    /**
     * Interface for ChildSessionStateMachine to notify IkeSessionStateMachine of state changes.
     */
    interface IChildSessionSmCallback {
        /** Notify that new Child SA is created. */
        void onChildSaCreated(int remoteSpi, ChildSessionStateMachine childSession);

        /** Notify that a Child SA is deleted. */
        void onChildSaDeleted(int remoteSpi);

        /** Schedule retry for a Create Child Request on the LocalRequestScheduler. */
        void scheduleRetryLocalRequest(ChildLocalRequest futureRequest);

        /** Notify the IKE Session to send out IKE message for this Child Session. */
        void onOutboundPayloadsReady(
                @ExchangeType int exchangeType,
                boolean isResp,
                List<IkePayload> payloadList,
                ChildSessionStateMachine childSession);

        /** Notify that a Child procedure has been finished. */
        void onProcedureFinished(ChildSessionStateMachine childSession);

        /**
         * Notify the IKE Session State Machine that this Child has been fully shut down.
         *
         * <p>This method MUST be called after the user callbacks have been fired, and MUST always
         * be called before the state machine can shut down.
         */
        void onChildSessionClosed(ChildSessionCallback userCallbacks);

        /**
         * Notify that a Child procedure has been finished and the IKE Session should close itself
         * because of a fatal error.
         *
         * <p>Child Session may encounter an IKE Session fatal error in three cases with different
         * handling rules:
         *
         * <pre>
         * - When there is a fatal error in an inbound request, onOutboundPayloadsReady will be
         *   called first to send out an error notification, and then onFatalIkeSessionError() will
         *   be called to locally close the IKE Session.
         * - When there is a fatal error in an inbound response, onOutboundPayloadsReady will be
         *   called first to send out an IKE Delete message, and then onFatalIkeSessionError() will
         *   be called to locally close the IKE Session.
         * - When there is a fatal error notification in an inbound response,
         *   onFatalIkeSessionError() will be called to locally close the IKE Session"
         * </pre>
         */
        void onFatalIkeSessionError(Exception exception);
    }

    /**
     * Receive requesting and responding payloads for negotiating first Child SA.
     *
     * <p>This method is called synchronously from IkeStateMachine. It proxies the synchronous call
     * as an asynchronous job to the ChildStateMachine handler.
     *
     * @param reqPayloads SA negotiation related payloads in IKE_AUTH request.
     * @param respPayloads SA negotiation related payloads in IKE_AUTH response.
     * @param localAddress The local (outer) address of the Child Session.
     * @param remoteAddress The remote (outer) address of the Child Session.
     * @param udpEncapSocket The socket to use for UDP encapsulation, or NULL if no encap needed.
     * @param ikePrf The pseudo-random function to use for key derivation
     * @param ikeDh The negotiated IKE DH group
     * @param skD The key for which to derive new keying information from.
     */
    public void handleFirstChildExchange(
            List<IkePayload> reqPayloads,
            List<IkePayload> respPayloads,
            InetAddress localAddress,
            InetAddress remoteAddress,
            UdpEncapsulationSocket udpEncapSocket,
            IkeMacPrf ikePrf,
            int ikeDh,
            byte[] skD) {

        this.mLocalAddress = localAddress;
        this.mRemoteAddress = remoteAddress;
        this.mUdpEncapSocket = udpEncapSocket;
        this.mIkePrf = ikePrf;
        this.mIkeDhGroup = ikeDh;
        this.mSkD = skD;
        mIsFirstChild = true;

        int spi = registerProvisionalChildAndGetSpi(respPayloads);
        sendMessage(
                CMD_HANDLE_FIRST_CHILD_EXCHANGE,
                new FirstChildNegotiationData(reqPayloads, respPayloads, spi));
    }

    /**
     * Initiate Create Child procedure.
     *
     * <p>This method is called synchronously from IkeStateMachine. It proxies the synchronous call
     * as an asynchronous job to the ChildStateMachine handler.
     *
     * @param localAddress The local (outer) address from which traffic will originate.
     * @param remoteAddress The remote (outer) address to which traffic will be sent.
     * @param udpEncapSocket The socket to use for UDP encapsulation, or NULL if no encap needed.
     * @param ikePrf The pseudo-random function to use for key derivation
     * @param ikeDh The negotiated IKE DH group
     * @param skD The key for which to derive new keying information from.
     */
    public void createChildSession(
            InetAddress localAddress,
            InetAddress remoteAddress,
            UdpEncapsulationSocket udpEncapSocket,
            IkeMacPrf ikePrf,
            int ikeDh,
            byte[] skD) {
        this.mLocalAddress = localAddress;
        this.mRemoteAddress = remoteAddress;
        this.mUdpEncapSocket = udpEncapSocket;
        this.mIkePrf = ikePrf;
        this.mIkeDhGroup = ikeDh;
        this.mSkD = skD;
        mIsFirstChild = false;

        sendMessage(CMD_LOCAL_REQUEST_CREATE_CHILD);
    }

    /**
     * Initiate Delete Child procedure.
     *
     * <p>This method is called synchronously from IkeStateMachine. It proxies the synchronous call
     * as an asynchronous job to the ChildStateMachine handler.
     */
    public void deleteChildSession() {
        sendMessage(CMD_LOCAL_REQUEST_DELETE_CHILD);
    }

    /**
     * Initiate Rekey Child procedure.
     *
     * <p>This method is called synchronously from IkeStateMachine. It proxies the synchronous call
     * as an asynchronous job to the ChildStateMachine handler.
     */
    public void rekeyChildSession() {
        sendMessage(CMD_LOCAL_REQUEST_REKEY_CHILD);
    }

    /**
     * Update IPsec SAs via MOBIKE.
     *
     * <p>This method is called synchronously from IkeStateMachine, and may complete synchronously
     * if kernel MOBIKE can be used. Otherwise, it will fall back to using rekeys to synchronize
     * IPsec state.
     *
     * @param localAddress The local (outer) address from which traffic will originate.
     * @param remoteAddress The remote (outer) address to which traffic will be sent.
     * @param udpEncapSocket The socket to use for UDP encapsulation, or NULL if no encap needed.
     */
    public void performMigration(
            InetAddress localAddress,
            InetAddress remoteAddress,
            UdpEncapsulationSocket udpEncapSocket) {

        final UdpEncapsulationSocket oldEncapSocket = mUdpEncapSocket;

        this.mLocalAddress = localAddress;
        this.mRemoteAddress = remoteAddress;
        this.mUdpEncapSocket = udpEncapSocket;

        if (oldEncapSocket == mUdpEncapSocket
                && ShimUtils.getInstance()
                        .supportsSameSocketKernelMigration(mIkeContext.getContext())) {
            mIpSecManager.startTunnelModeTransformMigration(
                    mCurrentChildSaRecord.getInboundIpSecTransform(),
                    mRemoteAddress,
                    mLocalAddress);
            mIpSecManager.startTunnelModeTransformMigration(
                    mCurrentChildSaRecord.getOutboundIpSecTransform(),
                    mLocalAddress,
                    mRemoteAddress);
            executeUserCallback(() -> {
                mUserCallback.onIpSecTransformsMigrated(
                        mCurrentChildSaRecord.getInboundIpSecTransform(),
                        mCurrentChildSaRecord.getOutboundIpSecTransform());
            });

            mChildSmCallback.onProcedureFinished(ChildSessionStateMachine.this);
        } else {
            performRekeyMigration(localAddress, remoteAddress, udpEncapSocket);
        }
    }

    /**
     * Initiate Rekey Child procedure for MOBIKE (instead of migrating IPsec SAs).
     *
     * <p>This method should only be used as a fallback mode for devices that do not have
     * XFRM_MIGRATE kernel support.
     *
     * <p>This method is called synchronously from IkeStateMachine. It proxies the synchronous call
     * as an asynchronous job to the ChildStateMachine handler.
     *
     * <p>This method works similarly to {@link #rekeyChildSession()} in that it rekeys the Child
     * SAs associated with this state machine. However, the caller is notified of Child SA creation
     * via {@link ChildSessionCallback#onIpSecTransformsMigrated(android.net.IpSecTransform,
     * android.net.IpSecTransform)};
     *
     * @param localAddress The local (outer) address from which traffic will originate.
     * @param remoteAddress The remote (outer) address to which traffic will be sent.
     * @param udpEncapSocket The socket to use for UDP encapsulation, or NULL if no encap needed.
     */
    public void performRekeyMigration(
            InetAddress localAddress,
            InetAddress remoteAddress,
            UdpEncapsulationSocket udpEncapSocket) {

        this.mLocalAddress = localAddress;
        this.mRemoteAddress = remoteAddress;
        this.mUdpEncapSocket = udpEncapSocket;

        sendMessage(CMD_LOCAL_REQUEST_REKEY_CHILD_MOBIKE);
    }

    /**
     * Receive a request
     *
     * <p>This method is called synchronously from IkeStateMachine. It proxies the synchronous call
     * as an asynchronous job to the ChildStateMachine handler.
     *
     * @param exchangeSubtype the exchange subtype of this inbound request.
     * @param exchangeType the exchange type in the request message.
     * @param payloadList the Child-procedure-related payload list in the request message that needs
     *     validation.
     */
    public void receiveRequest(
            @IkeExchangeSubType int exchangeSubtype,
            @ExchangeType int exchangeType,
            List<IkePayload> payloadList) {
        sendMessage(
                CMD_HANDLE_RECEIVED_REQUEST,
                new ReceivedRequest(exchangeSubtype, exchangeType, payloadList));
    }

    /**
     * Receive a response.
     *
     * <p>This method is called synchronously from IkeStateMachine. It proxies the synchronous call
     * as an asynchronous job to the ChildStateMachine handler.
     *
     * @param exchangeType the exchange type in the response message that needs validation.
     * @param payloadList the Child-procedure-related payload list in the response message that
     *     needs validation.
     */
    public void receiveResponse(@ExchangeType int exchangeType, List<IkePayload> payloadList) {
        if (!isAwaitingCreateResp()) {
            sendMessage(
                    CMD_HANDLE_RECEIVED_RESPONSE, new ReceivedResponse(exchangeType, payloadList));
        }

        // If we are waiting for a Create/RekeyCreate response and the received message contains SA
        // payload we need to register for this provisional Child.
        int spi = registerProvisionalChildAndGetSpi(payloadList);
        sendMessage(
                CMD_HANDLE_RECEIVED_RESPONSE,
                new ReceivedCreateResponse(exchangeType, payloadList, spi));
    }

    private boolean isAwaitingCreateResp() {
        return (getCurrentState() == mCreateChildLocalCreate
                || getCurrentState() == mMobikeRekeyChildLocalCreate
                || getCurrentState() == mRekeyChildLocalCreate);
    }

    /**
     * Update SK_d with provided value when IKE SA is rekeyed.
     *
     * <p>It MUST be only called at the end of Rekey IKE procedure, which guarantees this Child
     * Session is not in Create Child or Rekey Child procedure.
     *
     * @param skD the new skD in byte array.
     */
    public void setSkD(byte[] skD) {
        mSkD = skD;
    }

    /**
     * Register provisioning ChildSessionStateMachine in IChildSessionSmCallback
     *
     * <p>This method is for avoiding CHILD_SA_NOT_FOUND error in IkeSessionStateMachine when remote
     * peer sends request for delete/rekey this Child SA before ChildSessionStateMachine sends
     * FirstChildNegotiationData or Create response to itself.
     */
    private int registerProvisionalChildAndGetSpi(List<IkePayload> respPayloads) {
        IkeSaPayload saPayload =
                IkePayload.getPayloadForTypeInProvidedList(
                        PAYLOAD_TYPE_SA, IkeSaPayload.class, respPayloads);

        if (saPayload == null) return SPI_NOT_REGISTERED;

        // IkeSaPayload.Proposal stores SPI in long type so as to be applied to both 8-byte IKE SPI
        // and 4-byte Child SPI. Here we cast the stored SPI to int to represent a Child SPI.
        int remoteGenSpi = (int) (saPayload.proposalList.get(0).spi);
        mChildSmCallback.onChildSaCreated(remoteGenSpi, this);
        return remoteGenSpi;
    }

    private void replyErrorNotification(@NotifyType int notifyType) {
        replyErrorNotification(notifyType, new byte[0]);
    }

    private void replyErrorNotification(@NotifyType int notifyType, byte[] notifyData) {
        List<IkePayload> outPayloads = new ArrayList<>(1);
        IkeNotifyPayload notifyPayload = new IkeNotifyPayload(notifyType, notifyData);
        outPayloads.add(notifyPayload);

        mChildSmCallback.onOutboundPayloadsReady(
                EXCHANGE_TYPE_INFORMATIONAL, true /*isResp*/, outPayloads, this);
    }

    private void sendDeleteIkeRequest() {
        List<IkePayload> outIkePayloads = new ArrayList<>(1);
        outIkePayloads.add(new IkeDeletePayload());

        mChildSmCallback.onOutboundPayloadsReady(
                EXCHANGE_TYPE_INFORMATIONAL,
                false /*isResp*/,
                outIkePayloads,
                ChildSessionStateMachine.this);
    }

    class OnIpSecSaPairCreatedRunnable implements Runnable {
        private final IpSecTransform mOut;
        private final IpSecTransform mIn;

        OnIpSecSaPairCreatedRunnable(ChildSaRecord childSaRecord) {
            mOut = childSaRecord.getOutboundIpSecTransform();
            mIn = childSaRecord.getInboundIpSecTransform();
        }

        @Override
        public void run() {
            mUserCallback.onIpSecTransformCreated(mOut, IpSecManager.DIRECTION_OUT);
            mUserCallback.onIpSecTransformCreated(mIn, IpSecManager.DIRECTION_IN);
        }
    }

    class OnIpSecSaPairDeletedRunnable implements Runnable {
        private final IpSecTransform mOut;
        private final IpSecTransform mIn;

        OnIpSecSaPairDeletedRunnable(ChildSaRecord childSaRecord) {
            mOut = childSaRecord.getOutboundIpSecTransform();
            mIn = childSaRecord.getInboundIpSecTransform();
        }

        @Override
        public void run() {
            mUserCallback.onIpSecTransformDeleted(mOut, IpSecManager.DIRECTION_OUT);
            mUserCallback.onIpSecTransformDeleted(mIn, IpSecManager.DIRECTION_IN);
        }
    }

    /**
     * ReceivedRequest contains exchange subtype and payloads that are extracted from a request
     * message to the current Child procedure.
     */
    private static class ReceivedRequest {
        @IkeExchangeSubType public final int exchangeSubtype;
        @ExchangeType public final int exchangeType;
        public final List<IkePayload> requestPayloads;

        ReceivedRequest(
                @IkeExchangeSubType int eSubtype,
                @ExchangeType int eType,
                List<IkePayload> reqPayloads) {
            exchangeSubtype = eSubtype;
            exchangeType = eType;
            requestPayloads = reqPayloads;
        }
    }

    /**
     * ReceivedResponse contains exchange type and payloads that are extracted from a response
     * message to the current Child procedure.
     */
    private static class ReceivedResponse {
        @ExchangeType public final int exchangeType;
        public final List<IkePayload> responsePayloads;

        ReceivedResponse(@ExchangeType int eType, List<IkePayload> respPayloads) {
            exchangeType = eType;
            responsePayloads = respPayloads;
        }
    }

    private static class ReceivedCreateResponse extends ReceivedResponse {
        public final int registeredSpi;

        ReceivedCreateResponse(@ExchangeType int eType, List<IkePayload> respPayloads, int spi) {
            super(eType, respPayloads);
            registeredSpi = spi;
        }
    }

    /**
     * FirstChildNegotiationData contains payloads for negotiating first Child SA in IKE_AUTH
     * request and IKE_AUTH response and callback to notify IkeSessionStateMachine the SA
     * negotiation result.
     */
    private static class FirstChildNegotiationData extends ReceivedCreateResponse {
        public final List<IkePayload> requestPayloads;

        FirstChildNegotiationData(
                List<IkePayload> reqPayloads, List<IkePayload> respPayloads, int spi) {
            super(EXCHANGE_TYPE_IKE_AUTH, respPayloads, spi);
            requestPayloads = reqPayloads;
        }
    }

    /** Top level state for handling uncaught exceptions for all subclasses. */
    abstract class ExceptionHandler extends ExceptionHandlerBase {
        @Override
        protected void cleanUpAndQuit(RuntimeException e) {
            // TODO: b/140123526 Send a response if exception was caught when processing a request.

            // Clean up all SaRecords.
            closeAllSaRecords(false /*expectSaClosed*/);

            executeUserCallback(
                    () -> {
                        mUserCallback.onClosedWithException(wrapAsIkeException(e));
                    });

            recordMetricsEvent_sessionTerminated(wrapAsIkeException(e));
            logWtf("Unexpected exception in " + getCurrentStateName(), e);
            quitSessionNow();
        }

        @Override
        protected String getCmdString(int cmd) {
            return CMD_TO_STR.get(cmd);
        }
    }

    /** Called when this StateMachine quits. */
    @Override
    protected void onQuitting() {
        // Clean up all SaRecords.
        closeAllSaRecords(true /*expectSaClosed*/);

        mChildSmCallback.onProcedureFinished(this);
        mChildSmCallback.onChildSessionClosed(mUserCallback);
    }

    private void closeAllSaRecords(boolean expectSaClosed) {
        closeChildSaRecord(mCurrentChildSaRecord, expectSaClosed);
        closeChildSaRecord(mLocalInitNewChildSaRecord, expectSaClosed);
        closeChildSaRecord(mRemoteInitNewChildSaRecord, expectSaClosed);

        mCurrentChildSaRecord = null;
        mLocalInitNewChildSaRecord = null;
        mRemoteInitNewChildSaRecord = null;
    }

    private void closeChildSaRecord(ChildSaRecord childSaRecord, boolean expectSaClosed) {
        if (childSaRecord == null) return;

        OnIpSecSaPairDeletedRunnable delRunnable = new OnIpSecSaPairDeletedRunnable(childSaRecord);
        executeUserCallback(delRunnable);

        mChildSmCallback.onChildSaDeleted(childSaRecord.getRemoteSpi());
        childSaRecord.close();

        if (!expectSaClosed) return;

        logWtf(
                "ChildSaRecord with local SPI: "
                        + childSaRecord.getLocalSpi()
                        + " is not correctly closed.");
    }

    private void handleChildFatalError(Exception error) {
        IkeException ikeException = wrapAsIkeException(error);
        loge("Child Session fatal error", ikeException);

        // Clean up all SaRecords and quit
        closeAllSaRecords(false /*expectSaClosed*/);
        executeUserCallback(
                () -> {
                    mUserCallback.onClosedWithException(ikeException);
                });

        recordMetricsEvent_sessionTerminated(ikeException);
        quitSessionNow();
    }

    /**
     * This state handles the request to close Child Session immediately without initiating any
     * exchange.
     *
     * <p>Request for closing Child Session immediately is usually caused by the closing of IKE
     * Session. All states MUST be a child state of KillChildSessionParent to handle the closing
     * request.
     */
    private class KillChildSessionParent extends ExceptionHandler {
        @Override
        public boolean processStateMessage(Message message) {
            switch (message.what) {
                case CMD_KILL_SESSION:
                    closeAllSaRecords(false /*expectSaClosed*/);
                    executeUserCallback(
                            () -> {
                                mUserCallback.onClosed();
                            });

                    // ChildSessionTerminated Metrics not recorded; this is a result of the parent
                    // session tearing down.
                    quitSessionNow();
                    return HANDLED;
                default:
                    return NOT_HANDLED;
            }
        }

        @Override
        protected @IkeMetrics.IkeState int getMetricsStateCode() {
            return IkeMetrics.IKE_STATE_CHILD_KILL;
        }
    }

    /**
     * CreateChildLocalCreateBase represents the common information for a locally-initiated initial
     * Child SA negotiation for setting up this Child Session.
     */
    private abstract class CreateChildLocalCreateBase extends ExceptionHandler {
        protected void validateAndBuildChild(
                List<IkePayload> reqPayloads,
                List<IkePayload> respPayloads,
                @ExchangeType int exchangeType,
                @ExchangeType int expectedExchangeType,
                int registeredSpi) {
            validateAndBuildChild(
                    reqPayloads,
                    respPayloads,
                    registeredSpi,
                    CreateChildSaHelper.validateAndNegotiateInitChild(
                            reqPayloads,
                            respPayloads,
                            exchangeType,
                            expectedExchangeType,
                            mChildSessionParams.isTransportMode(),
                            mIpSecSpiGenerator,
                            mRemoteAddress));
        }

        protected void validateAndBuildChild(
                List<IkePayload> reqPayloads,
                List<IkePayload> respPayloads,
                int registeredSpi,
                CreateChildResult createChildResult) {
            switch (createChildResult.status) {
                case CREATE_STATUS_OK:
                    try {
                        setUpNegotiatedResult(createChildResult);

                        mCurrentChildSaRecord =
                                ChildSaRecord.makeChildSaRecord(
                                        mIkeContext.getContext(),
                                        reqPayloads,
                                        respPayloads,
                                        createChildResult.initSpi,
                                        createChildResult.respSpi,
                                        mLocalAddress,
                                        mRemoteAddress,
                                        mUdpEncapSocket,
                                        mIkePrf,
                                        mChildIntegrity,
                                        mChildCipher,
                                        mSkD,
                                        mChildSessionParams.isTransportMode(),
                                        true /*isLocalInit*/,
                                        buildSaLifetimeAlarmSched(
                                                createChildResult.respSpi.getSpi()));

                        ChildSessionConfiguration sessionConfig =
                                buildChildSessionConfigFromResp(createChildResult, respPayloads);

                        OnIpSecSaPairCreatedRunnable createRunnable =
                                new OnIpSecSaPairCreatedRunnable(mCurrentChildSaRecord);
                        executeUserCallback(
                                () -> {
                                    createRunnable.run();
                                    mUserCallback.onOpened(sessionConfig);
                                });

                        List<Integer> integrityAlgorithms = mSaProposal.getIntegrityAlgorithms();
                        List<Integer> dhGroups = mSaProposal.getDhGroups();

                        recordMetricsEvent_SaNegotiation(
                                dhGroups.isEmpty()
                                        ? IkeMetrics.DH_GROUP_UNSPECIFIED
                                        : dhGroups.get(0),
                                mSaProposal.getEncryptionTransforms()[0].id,
                                mSaProposal.getEncryptionTransforms()[0].getSpecifiedKeyLength(),
                                integrityAlgorithms.isEmpty()
                                        ? IkeMetrics.INTEGRITY_ALGORITHM_NONE
                                        : integrityAlgorithms.get(0),
                                IkeMetrics.PSEUDORANDOM_FUNCTION_UNSPECIFIED,
                                null);

                        transitionTo(mIdle);
                    } catch (GeneralSecurityException
                            | ResourceUnavailableException
                            | SpiUnavailableException
                            | IOException e) {
                        // #makeChildSaRecord failed.

                        // TODO: Initiate deletion
                        mChildSmCallback.onChildSaDeleted(createChildResult.respSpi.getSpi());
                        handleChildFatalError(e);
                    } finally {
                        // In the successful case the transform in ChildSaRecord has taken ownership
                        // of the SPI (in IpSecService), and will keep it alive.
                        createChildResult.initSpi.close();
                        createChildResult.respSpi.close();
                    }
                    break;
                case CREATE_STATUS_CHILD_ERROR_INVALID_MSG:
                    // TODO: Initiate deletion
                    handleCreationFailAndQuit(registeredSpi, createChildResult.exception);
                    break;
                case CREATE_STATUS_CHILD_ERROR_RCV_NOTIFY:
                    handleCreationFailAndQuit(registeredSpi, createChildResult.exception);
                    break;
                default:
                    cleanUpAndQuit(
                            new IllegalStateException(
                                    "Unrecognized status: " + createChildResult.status));
            }
        }

        private void setUpNegotiatedResult(CreateChildResult createChildResult) {
            // Build crypto tools using negotiated ChildSaProposal. It is ensured by {@link
            // IkeSaPayload#getVerifiedNegotiatedChildProposalPair} that the negotiated
            // ChildSaProposal is valid. The negotiated ChildSaProposal has exactly one encryption
            // algorithm. When it has a combined-mode encryption algorithm, it either does not have
            // integrity algorithm or only has one NONE value integrity algorithm. When the
            // negotiated ChildSaProposal has a normal encryption algorithm, it either does not have
            // integrity algorithm or has one integrity algorithm with any supported value.

            mSaProposal = createChildResult.negotiatedProposal;
            mChildCipher = IkeCipher.create(mSaProposal.getEncryptionTransforms()[0]);
            if (mSaProposal.getIntegrityTransforms().length != 0
                    && mSaProposal.getIntegrityTransforms()[0].id
                            != SaProposal.INTEGRITY_ALGORITHM_NONE) {
                mChildIntegrity = IkeMacIntegrity.create(mSaProposal.getIntegrityTransforms()[0]);
            }

            mLocalTs = createChildResult.initTs;
            mRemoteTs = createChildResult.respTs;
        }

        private ChildSessionConfiguration buildChildSessionConfigFromResp(
                CreateChildResult createChildResult, List<IkePayload> respPayloads) {
            IkeConfigPayload configPayload =
                    IkePayload.getPayloadForTypeInProvidedList(
                            PAYLOAD_TYPE_CP, IkeConfigPayload.class, respPayloads);

            if (mChildSessionParams.isTransportMode()
                    || configPayload == null
                    || configPayload.configType != IkeConfigPayload.CONFIG_TYPE_REPLY) {
                if (configPayload != null) {
                    logw("Unexpected config payload. Config Type: " + configPayload.configType);
                }

                return new ChildSessionConfiguration(
                        Arrays.asList(createChildResult.initTs),
                        Arrays.asList(createChildResult.respTs));
            } else {
                return new ChildSessionConfiguration(
                        Arrays.asList(createChildResult.initTs),
                        Arrays.asList(createChildResult.respTs),
                        configPayload);
            }
        }

        private void handleCreationFailAndQuit(int registeredSpi, IkeException exception) {
            if (registeredSpi != SPI_NOT_REGISTERED) {
                mChildSmCallback.onChildSaDeleted(registeredSpi);
            }
            handleChildFatalError(exception);
        }
    }

    private String getIntentIdentifier(int remoteSpi) {
        return IkeSessionStateMachine.TAG + "_" + mIkeSessionId + "_" + TAG + "_" + remoteSpi;
    }

    private Message getIntentIkeSmMsg(int localRequestType, int remoteSpi) {
        Bundle spiBundle = new Bundle();
        spiBundle.putInt(BUNDLE_KEY_CHILD_REMOTE_SPI, remoteSpi);

        return mIkeHandler.obtainMessage(
                CMD_ALARM_FIRED, mIkeSessionId, localRequestType, spiBundle);
    }

    private SaLifetimeAlarmScheduler buildSaLifetimeAlarmSched(int remoteSpi) {
        Message deleteMsg = getIntentIkeSmMsg(CMD_LOCAL_REQUEST_DELETE_CHILD, remoteSpi);
        Message rekeyMsg = getIntentIkeSmMsg(CMD_LOCAL_REQUEST_REKEY_CHILD, remoteSpi);

        PendingIntent deleteSaIntent =
                buildIkeAlarmIntent(
                        mIkeContext.getContext(),
                        ACTION_DELETE_CHILD,
                        getIntentIdentifier(remoteSpi),
                        deleteMsg);
        PendingIntent rekeySaIntent =
                buildIkeAlarmIntent(
                        mIkeContext.getContext(),
                        ACTION_REKEY_CHILD,
                        getIntentIdentifier(remoteSpi),
                        rekeyMsg);

        return new SaLifetimeAlarmScheduler(
                new IkeAlarmConfig(
                        mIkeContext.getContext(),
                        ACTION_DELETE_CHILD,
                        mChildSessionParams.getHardLifetimeMsInternal(),
                        deleteSaIntent,
                        deleteMsg),
                new IkeAlarmConfig(
                        mIkeContext.getContext(),
                        ACTION_REKEY_CHILD,
                        mChildSessionParams.getSoftLifetimeMsInternal(),
                        rekeySaIntent,
                        rekeyMsg));
    }

    /** Initial state of ChildSessionStateMachine. */
    class Initial extends CreateChildLocalCreateBase {
        List<IkePayload> mRequestPayloads;

        @Override
        public boolean processStateMessage(Message message) {
            switch (message.what) {
                case CMD_HANDLE_FIRST_CHILD_EXCHANGE:
                    FirstChildNegotiationData childNegotiationData =
                            (FirstChildNegotiationData) message.obj;
                    mRequestPayloads = childNegotiationData.requestPayloads;
                    List<IkePayload> respPayloads = childNegotiationData.responsePayloads;

                    // Negotiate Child SA. The exchangeType has been validated in
                    // IkeSessionStateMachine. Won't validate it again here.
                    validateAndBuildChild(
                            mRequestPayloads,
                            respPayloads,
                            EXCHANGE_TYPE_IKE_AUTH,
                            EXCHANGE_TYPE_IKE_AUTH,
                            childNegotiationData.registeredSpi);

                    return HANDLED;
                case CMD_LOCAL_REQUEST_CREATE_CHILD:
                    transitionTo(mCreateChildLocalCreate);
                    return HANDLED;
                case CMD_LOCAL_REQUEST_DELETE_CHILD:
                    // This may happen when creation has been rescheduled to be after deletion.
                    executeUserCallback(
                            () -> {
                                mUserCallback.onClosed();
                            });

                    recordMetricsEvent_sessionTerminated(null);
                    quitSessionNow();
                    return HANDLED;
                case CMD_FORCE_TRANSITION:
                    transitionTo((State) message.obj);
                    return HANDLED;
                default:
                    return NOT_HANDLED;
            }
        }

        @Override
        public void exitState() {
            CreateChildSaHelper.releaseSpiResources(mRequestPayloads);
        }

        @Override
        protected @IkeMetrics.IkeState int getMetricsStateCode() {
            return IkeMetrics.IKE_STATE_CHILD_INITIAL;
        }
    }

    /**
     * CreateChildLocalCreate represents the state where Child Session initiates the Create Child
     * exchange.
     */
    class CreateChildLocalCreate extends CreateChildLocalCreateBase {
        private List<IkePayload> mRequestPayloads;

        @Override
        public void enterState() {
            try {
                mRequestPayloads =
                        CreateChildSaHelper.getInitChildCreateReqPayloads(
                                mIkeContext.getRandomnessFactory(),
                                mIpSecSpiGenerator,
                                mLocalAddress,
                                mChildSessionParams,
                                false /*isFirstChildSa*/);

                final ConfigAttribute[] configAttributes =
                        CreateChildSaHelper.getConfigAttributes(mChildSessionParams);
                if (configAttributes.length > 0) {
                    mRequestPayloads.add(
                            new IkeConfigPayload(
                                    false /*isReply*/, Arrays.asList(configAttributes)));
                }

                mChildSmCallback.onOutboundPayloadsReady(
                        EXCHANGE_TYPE_CREATE_CHILD_SA,
                        false /*isResp*/,
                        mRequestPayloads,
                        ChildSessionStateMachine.this);
            } catch (SpiUnavailableException | ResourceUnavailableException e) {
                // Fail to assign SPI
                handleChildFatalError(e);
            }
        }

        @Override
        public boolean processStateMessage(Message message) {
            switch (message.what) {
                case CMD_HANDLE_RECEIVED_RESPONSE:
                    ReceivedCreateResponse rcvResp = (ReceivedCreateResponse) message.obj;
                    CreateChildResult createChildResult =
                            CreateChildSaHelper.validateAndNegotiateInitChild(
                                    mRequestPayloads,
                                    rcvResp.responsePayloads,
                                    rcvResp.exchangeType,
                                    EXCHANGE_TYPE_CREATE_CHILD_SA,
                                    mChildSessionParams.isTransportMode(),
                                    mIpSecSpiGenerator,
                                    mRemoteAddress);

                    // If the response includes the error notification for TEMPORARY_FAILURE, retry
                    // creating the Child.
                    if (isTemporaryFailure(createChildResult)) {
                        transitionTo(mInitial);

                        mChildSmCallback.scheduleRetryLocalRequest(
                                mLocalRequestFactory.getChildLocalRequest(
                                        CMD_LOCAL_REQUEST_CREATE_CHILD,
                                        mUserCallback,
                                        mChildSessionParams));
                        return HANDLED;
                    }

                    validateAndBuildChild(
                            mRequestPayloads,
                            rcvResp.responsePayloads,
                            rcvResp.registeredSpi,
                            createChildResult);
                    return HANDLED;
                default:
                    return NOT_HANDLED;
            }
        }

        @Override
        public void exitState() {
            CreateChildSaHelper.releaseSpiResources(mRequestPayloads);
        }

        @Override
        protected @IkeMetrics.IkeState int getMetricsStateCode() {
            return IkeMetrics.IKE_STATE_CHILD_CREATE_LOCAL_CREATE;
        }

        private boolean isTemporaryFailure(CreateChildResult createChildResult) {
            if (createChildResult.status != CREATE_STATUS_CHILD_ERROR_RCV_NOTIFY) {
                return false;
            }
            return createChildResult.exception instanceof TemporaryFailureException;
        }
    }

    /**
     * Idle represents a state when there is no ongoing IKE exchange affecting established Child SA.
     */
    class Idle extends ExceptionHandler {
        @Override
        public void enterState() {
            maybeNotifyIkeSessionStateMachine();
        }

        protected void maybeNotifyIkeSessionStateMachine() {
            mChildSmCallback.onProcedureFinished(ChildSessionStateMachine.this);
        }

        @Override
        public boolean processStateMessage(Message message) {
            switch (message.what) {
                case CMD_LOCAL_REQUEST_DELETE_CHILD:
                    transitionTo(mDeleteChildLocalDelete);
                    return HANDLED;
                case CMD_LOCAL_REQUEST_REKEY_CHILD:
                    transitionTo(mRekeyChildLocalCreate);
                    return HANDLED;
                case CMD_LOCAL_REQUEST_REKEY_CHILD_MOBIKE:
                    transitionTo(mMobikeRekeyChildLocalCreate);
                    return HANDLED;
                case CMD_HANDLE_RECEIVED_REQUEST:
                    ReceivedRequest req = (ReceivedRequest) message.obj;
                    switch (req.exchangeSubtype) {
                        case IKE_EXCHANGE_SUBTYPE_DELETE_CHILD:
                            deferMessage(message);
                            transitionTo(mDeleteChildRemoteDelete);
                            return HANDLED;
                        case IKE_EXCHANGE_SUBTYPE_REKEY_CHILD:
                            deferMessage(message);
                            transitionTo(mRekeyChildRemoteCreate);
                            return HANDLED;
                        default:
                            return NOT_HANDLED;
                    }
                case CMD_FORCE_TRANSITION: // Testing command
                    transitionTo((State) message.obj);
                    return HANDLED;
                default:
                    return NOT_HANDLED;
            }
        }

        @Override
        protected @IkeMetrics.IkeState int getMetricsStateCode() {
            return IkeMetrics.IKE_STATE_CHILD_IDLE;
        }
    }

    /**
     * This class is for handling the case when the previous procedure was finished by a new request
     *
     * <p>This state is the destination state when Child Session receives a new procedure request in
     * Rekey Delete. When entering this state, Child Session will process the deferred request as
     * Idle state does but will not notify IKE Session that Child Session has finished all the
     * procedures. It prevents IKE Session from going back to Idle state when its Child Session is
     * still busy.
     */
    class IdleWithDeferredRequest extends Idle {
        @Override
        public void maybeNotifyIkeSessionStateMachine() {
            // Do not notify IkeSessionStateMachine because Child Session needs to process the
            // deferred request and start a new procedure
        }

        @Override
        protected @IkeMetrics.IkeState int getMetricsStateCode() {
            return IkeMetrics.IKE_STATE_CHILD_IDLE_WITH_DEFERRED_REQUEST;
        }
    }

    /**
     * This class represents the state that Child Session was closed by the remote while waiting for
     * a response.
     *
     * <p>This state is the destination state when Child Session receives a Delete request while
     * waitng for a Rekey Create response. When that happens, Child Session should close all IPsec
     * SAs and notify the user immediately to prevent security risk. Child Session also needs to
     * continue waiting for the response and keep its parent IKE Session retransmitting the request,
     * as required by the IKE spec.
     */
    private class ClosedAndAwaitResponse extends ExceptionHandler {
        @Override
        public boolean processStateMessage(Message message) {
            switch (message.what) {
                case CMD_HANDLE_RECEIVED_RESPONSE:
                    // Do not need to verify the response since the Child Session is already closed

                    // Metrics not recorded, since already closed. Metrics recorded at the same
                    // time that user callbacks are fired.
                    quitSessionNow();
                    return HANDLED;
                default:
                    return NOT_HANDLED;
            }
        }

        @Override
        protected @IkeMetrics.IkeState int getMetricsStateCode() {
            return IkeMetrics.IKE_STATE_CHILD_CLOSE_AND_AWAIT_RESPONSE;
        }
    }

    /**
     * DeleteResponderBase represents all states after Child Session is established
     *
     * <p>All post-init states share common functionality of being able to respond to Delete Child
     * requests.
     */
    private abstract class DeleteResponderBase extends ExceptionHandler {
        /**
         * Check if the payload list has a Delete Payload that includes the remote SPI of the input
         * ChildSaRecord.
         */
        protected boolean hasRemoteChildSpiForDelete(
                List<IkePayload> payloads, ChildSaRecord expectedRecord) {
            List<IkeDeletePayload> delPayloads =
                    IkePayload.getPayloadListForTypeInProvidedList(
                            PAYLOAD_TYPE_DELETE, IkeDeletePayload.class, payloads);

            for (IkeDeletePayload delPayload : delPayloads) {
                for (int spi : delPayload.spisToDelete) {
                    if (spi == expectedRecord.getRemoteSpi()) return true;
                }
            }
            return false;
        }

        /**
         * Build and send payload list that has a Delete Payload that includes the local SPI of the
         * input ChildSaRecord.
         */
        protected void sendDeleteChild(ChildSaRecord childSaRecord, boolean isResp) {
            List<IkePayload> outIkePayloads = new ArrayList<>(1);
            outIkePayloads.add(new IkeDeletePayload(new int[] {childSaRecord.getLocalSpi()}));

            mChildSmCallback.onOutboundPayloadsReady(
                    EXCHANGE_TYPE_INFORMATIONAL,
                    isResp,
                    outIkePayloads,
                    ChildSessionStateMachine.this);
        }

        /**
         * Helper method for responding to a session deletion request
         *
         * <p>Note that this method expects that the session is keyed on the mCurrentChildSaRecord
         * and closing this Child SA indicates that the remote wishes to end the session as a whole.
         * As such, this should not be used in rekey cases where there is any ambiguity as to which
         * Child SA the session is reliant upon.
         *
         * <p>Note that this method will also quit the state machine
         */
        protected void handleDeleteSessionRequest(List<IkePayload> payloads) {
            if (!hasRemoteChildSpiForDelete(payloads, mCurrentChildSaRecord)) {
                cleanUpAndQuit(
                        new IllegalStateException(
                                "Found no remote SPI for mCurrentChildSaRecord in a Delete Child"
                                        + " request."));
            } else {
                sendDeleteChild(mCurrentChildSaRecord, true /*isResp*/);
                closeSessionAndNotifyUser(true /* quitStateMachine */);
            }
        }

        protected void closeSessionAndNotifyUser(boolean quitStateMachine) {
            OnIpSecSaPairDeletedRunnable delRunnable =
                    new OnIpSecSaPairDeletedRunnable(mCurrentChildSaRecord);
            executeUserCallback(
                    () -> {
                        delRunnable.run();
                        mUserCallback.onClosed();
                    });
            recordMetricsEvent_sessionTerminated(null);

            mChildSmCallback.onChildSaDeleted(mCurrentChildSaRecord.getRemoteSpi());
            mCurrentChildSaRecord.close();
            mCurrentChildSaRecord = null;

            if (quitStateMachine) {
                quitSessionNow();
            }
        }
    }

    /**
     * DeleteBase abstracts deletion handling for all states initiating and responding to a Delete
     * Child exchange
     *
     * <p>All subclasses of this state share common functionality that a deletion request is sent,
     * and the response is received.
     */
    private abstract class DeleteBase extends DeleteResponderBase {
        /** Validate payload types in Delete Child response. */
        protected void validateDeleteRespPayloadAndExchangeType(
                List<IkePayload> respPayloads, @ExchangeType int exchangeType)
                throws IkeProtocolException {

            if (exchangeType != EXCHANGE_TYPE_INFORMATIONAL) {
                throw new InvalidSyntaxException(
                        "Unexpected exchange type in Delete Child response: " + exchangeType);
            }

            for (IkePayload payload : respPayloads) {
                handlePayload:
                switch (payload.payloadType) {
                    case PAYLOAD_TYPE_DELETE:
                        // A Delete Payload is only required when it is not simultaneous deletion.
                        // Included Child SPIs are verified in the subclass to make sure the remote
                        // side is deleting the right SAs.
                        break handlePayload;
                    case PAYLOAD_TYPE_NOTIFY:
                        IkeNotifyPayload notify = (IkeNotifyPayload) payload;
                        if (!notify.isErrorNotify()) {
                            logw(
                                    "Unexpected or unknown status notification in Delete Child"
                                            + " response: "
                                            + notify.notifyType);
                            break handlePayload;
                        }

                        throw notify.validateAndBuildIkeException();
                    default:
                        logw(
                                "Unexpected payload type in Delete Child response: "
                                        + payload.payloadType);
                }
            }
        }
    }

    /**
     * DeleteChildLocalDelete represents the state where Child Session initiates the Delete Child
     * exchange.
     */
    class DeleteChildLocalDelete extends DeleteBase {
        private boolean mSimulDeleteDetected = false;

        @Override
        public void enterState() {
            mSimulDeleteDetected = false;
            sendDeleteChild(mCurrentChildSaRecord, false /*isResp*/);
        }

        @Override
        public boolean processStateMessage(Message message) {
            switch (message.what) {
                case CMD_HANDLE_RECEIVED_RESPONSE:
                    try {
                        ReceivedResponse resp = (ReceivedResponse) message.obj;
                        validateDeleteRespPayloadAndExchangeType(
                                resp.responsePayloads, resp.exchangeType);

                        boolean currentSaSpiFound =
                                hasRemoteChildSpiForDelete(
                                        resp.responsePayloads, mCurrentChildSaRecord);
                        if (!currentSaSpiFound && !mSimulDeleteDetected) {
                            throw new InvalidSyntaxException(
                                    "Found no remote SPI in received Delete response.");
                        } else if (currentSaSpiFound && mSimulDeleteDetected) {
                            // As required by the RFC 7296, in simultaneous delete case, the remote
                            // side MUST NOT include SPI of mCurrentChildSaRecord. However, to
                            // provide better interoperatibility, IKE library will keep IKE Session
                            // alive and continue the deleting process.
                            logw(
                                    "Found remote SPI in the Delete response in a simultaneous"
                                            + " deletion case");
                        }

                        closeSessionAndNotifyUser(true /* quitStateMachine */);
                    } catch (IkeProtocolException e) {
                        // Shut down Child Session and notify users the error.
                        handleChildFatalError(e);
                    }
                    return HANDLED;
                case CMD_HANDLE_RECEIVED_REQUEST:
                    ReceivedRequest req = (ReceivedRequest) message.obj;
                    switch (req.exchangeSubtype) {
                        case IKE_EXCHANGE_SUBTYPE_DELETE_CHILD:
                            // It has been verified in IkeSessionStateMachine that the incoming
                            // request can be ONLY for mCurrentChildSaRecord at this point.
                            if (!hasRemoteChildSpiForDelete(
                                    req.requestPayloads, mCurrentChildSaRecord)) {
                                // Program error
                                cleanUpAndQuit(
                                        new IllegalStateException(
                                                "Found no remote SPI for mCurrentChildSaRecord in"
                                                        + " a Delete request"));

                            } else {
                                mChildSmCallback.onOutboundPayloadsReady(
                                        EXCHANGE_TYPE_INFORMATIONAL,
                                        true /*isResp*/,
                                        new LinkedList<>(),
                                        ChildSessionStateMachine.this);
                                mSimulDeleteDetected = true;
                            }
                            return HANDLED;
                        case IKE_EXCHANGE_SUBTYPE_REKEY_CHILD:
                            replyErrorNotification(ERROR_TYPE_TEMPORARY_FAILURE);
                            return HANDLED;
                        default:
                            cleanUpAndQuit(
                                    new IllegalStateException(
                                            "Invalid exchange subtype for Child Session: "
                                                    + req.exchangeSubtype));
                            return HANDLED;
                    }
                default:
                    return NOT_HANDLED;
            }
        }

        @Override
        protected @IkeMetrics.IkeState int getMetricsStateCode() {
            return IkeMetrics.IKE_STATE_CHILD_DELETE_LOCAL_DELETE;
        }
    }

    /**
     * DeleteChildRemoteDelete represents the state where Child Session receives the Delete Child
     * request.
     */
    class DeleteChildRemoteDelete extends DeleteResponderBase {
        @Override
        public boolean processStateMessage(Message message) {
            switch (message.what) {
                case CMD_HANDLE_RECEIVED_REQUEST:
                    ReceivedRequest req = (ReceivedRequest) message.obj;
                    if (req.exchangeSubtype == IKE_EXCHANGE_SUBTYPE_DELETE_CHILD) {
                        handleDeleteSessionRequest(req.requestPayloads);
                        return HANDLED;
                    }
                    return NOT_HANDLED;
                default:
                    return NOT_HANDLED;
            }
        }

        @Override
        protected @IkeMetrics.IkeState int getMetricsStateCode() {
            return IkeMetrics.IKE_STATE_CHILD_DELETE_REMOTE_DELETE;
        }
    }

    /**
     * RekeyChildLocalCreate represents the state where Child Session initiates the Rekey Child
     * exchange.
     *
     * <p>As indicated in RFC 7296 section 2.8, "when rekeying, the new Child SA SHOULD NOT have
     * different Traffic Selectors and algorithms than the old one."
     */
    class RekeyChildLocalCreate extends DeleteResponderBase {
        private List<IkePayload> mRequestPayloads;

        @Override
        public void enterState() {
            try {
                ChildSaProposal saProposal = mSaProposal;
                if (mIsFirstChild) {
                    saProposal = addDhGroupsFromChildSessionParamsIfAbsent();
                }

                // Build request with negotiated proposal and TS.
                mRequestPayloads =
                        CreateChildSaHelper.getRekeyChildCreateReqPayloads(
                                mIkeContext.getRandomnessFactory(),
                                mIpSecSpiGenerator,
                                mLocalAddress,
                                saProposal,
                                mLocalTs,
                                mRemoteTs,
                                mCurrentChildSaRecord.getLocalSpi(),
                                mChildSessionParams.isTransportMode());
                mChildSmCallback.onOutboundPayloadsReady(
                        EXCHANGE_TYPE_CREATE_CHILD_SA,
                        false /*isResp*/,
                        mRequestPayloads,
                        ChildSessionStateMachine.this);
            } catch (SpiUnavailableException | ResourceUnavailableException e) {
                loge("Fail to assign Child SPI. Schedule a retry for rekey Child");
                mCurrentChildSaRecord.rescheduleRekey(RETRY_INTERVAL_MS);
                transitionTo(mIdle);
            }
        }

        @Override
        public boolean processStateMessage(Message message) {
            switch (message.what) {
                case CMD_HANDLE_RECEIVED_REQUEST:
                    ReceivedRequest req = (ReceivedRequest) message.obj;

                    if (req.exchangeSubtype == IKE_EXCHANGE_SUBTYPE_DELETE_CHILD) {
                        // Handle Delete request, notify users and do state transition to continue
                        // waiting for the response
                        sendDeleteChild(mCurrentChildSaRecord, true /*isResp*/);
                        closeSessionAndNotifyUser(false /* quitStateMachine */);
                        transitionTo(mClosedAndAwaitResponse);
                    } else {
                        replyErrorNotification(ERROR_TYPE_TEMPORARY_FAILURE);
                    }
                    return HANDLED;
                case CMD_HANDLE_RECEIVED_RESPONSE:
                    ReceivedCreateResponse resp = (ReceivedCreateResponse) message.obj;
                    CreateChildResult createChildResult =
                            CreateChildSaHelper.validateAndNegotiateRekeyChildResp(
                                    mRequestPayloads,
                                    resp.responsePayloads,
                                    resp.exchangeType,
                                    EXCHANGE_TYPE_CREATE_CHILD_SA,
                                    mChildSessionParams.isTransportMode(),
                                    mCurrentChildSaRecord,
                                    mIpSecSpiGenerator,
                                    mRemoteAddress);

                    switch (createChildResult.status) {
                        case CREATE_STATUS_OK:
                            try {
                                // Do not need to update TS because they are not changed.
                                mSaProposal = createChildResult.negotiatedProposal;

                                mLocalInitNewChildSaRecord =
                                        ChildSaRecord.makeChildSaRecord(
                                                mIkeContext.getContext(),
                                                mRequestPayloads,
                                                resp.responsePayloads,
                                                createChildResult.initSpi,
                                                createChildResult.respSpi,
                                                mLocalAddress,
                                                mRemoteAddress,
                                                mUdpEncapSocket,
                                                mIkePrf,
                                                mChildIntegrity,
                                                mChildCipher,
                                                mSkD,
                                                mChildSessionParams.isTransportMode(),
                                                true /*isLocalInit*/,
                                                buildSaLifetimeAlarmSched(
                                                        createChildResult.respSpi.getSpi()));

                                List<Integer> integrityAlgorithms =
                                        mSaProposal.getIntegrityAlgorithms();
                                List<Integer> dhGroups = mSaProposal.getDhGroups();

                                recordMetricsEvent_SaNegotiation(
                                        dhGroups.isEmpty()
                                                ? IkeMetrics.DH_GROUP_UNSPECIFIED
                                                : dhGroups.get(0),
                                        mSaProposal.getEncryptionTransforms()[0].id,
                                        mSaProposal.getEncryptionTransforms()[0]
                                                .getSpecifiedKeyLength(),
                                        integrityAlgorithms.isEmpty()
                                                ? IkeMetrics.INTEGRITY_ALGORITHM_NONE
                                                : integrityAlgorithms.get(0),
                                        IkeMetrics.PSEUDORANDOM_FUNCTION_UNSPECIFIED,
                                        null);

                                notifyCallerForLocalChildSaRekey();

                                transitionTo(mRekeyChildLocalDelete);
                            } catch (GeneralSecurityException
                                    | ResourceUnavailableException
                                    | SpiUnavailableException
                                    | IOException e) {
                                // #makeChildSaRecord failed
                                handleProcessRespOrSaCreationFailAndQuit(resp.registeredSpi, e);
                            } finally {
                                // In the successful case the transform in ChildSaRecord has taken
                                // ownership of the SPI (in IpSecService), and will keep it alive.
                                createChildResult.initSpi.close();
                                createChildResult.respSpi.close();
                            }
                            break;
                        case CREATE_STATUS_CHILD_ERROR_INVALID_MSG:
                            handleProcessRespOrSaCreationFailAndQuit(
                                    resp.registeredSpi, createChildResult.exception);
                            break;
                        case CREATE_STATUS_CHILD_ERROR_RCV_NOTIFY:
                            handleErrorNotify(createChildResult.exception);
                            transitionTo(mIdle);
                            break;
                        default:
                            cleanUpAndQuit(
                                    new IllegalStateException(
                                            "Unrecognized status: " + createChildResult.status));
                    }
                    return HANDLED;
                default:
                    return NOT_HANDLED;
            }
        }

        protected void notifyCallerForLocalChildSaRekey() {
            OnIpSecSaPairCreatedRunnable createRunnable =
                    new OnIpSecSaPairCreatedRunnable(mLocalInitNewChildSaRecord);
            executeUserCallback(createRunnable);
        }

        protected void handleProcessRespOrSaCreationFailAndQuit(
                int registeredSpi, Exception exception) {
            // We don't retry rekey if failure was caused by invalid response or SA creation error.
            // Reason is there is no way to notify the remote side the old SA is still alive but the
            // new one has failed. Sending delete request for new SA indicates the rekey has
            // finished and the new SA has died.

            // TODO: Initiate deletion on newly created SA
            if (registeredSpi != SPI_NOT_REGISTERED) {
                mChildSmCallback.onChildSaDeleted(registeredSpi);
            }
            handleChildFatalError(exception);
        }

        protected void handleErrorNotify(Exception exception) {
            loge("Received error notification for rekey Child. Schedule a retry");
            mCurrentChildSaRecord.rescheduleRekey(RETRY_INTERVAL_MS);
        }

        @Override
        public void exitState() {
            CreateChildSaHelper.releaseSpiResources(mRequestPayloads);
        }

        @Override
        protected @IkeMetrics.IkeState int getMetricsStateCode() {
            return IkeMetrics.IKE_STATE_CHILD_REKEY_LOCAL_CREATE;
        }
    }

    /**
     * MobikeRekeyChildLocalCreate represents the state where Child Session initiates the Rekey
     * Child exchange for MOBIKE-enabled IKE Sessions.
     *
     * <p>MobikeRekeyChildLocalCreate behaves similarly to RekeyChildLocalCreate except:
     *
     * <ul>
     *   <li>It notifies the caller of Child SA creation via {@link
     *       ChildSessionCallback#onIpSecTransformsMigrated(android.net.IpSecTransform,
     *       android.net.IpSecTransform)}. .
     *   <li>It tears down IKE SA immediately if any error occurs. In this case the origin Child SA
     *       has lost connectivity due to the changed IP addresses, and renewing the Child SA by
     *       MOBIKE rekey has failed. IKE module will handle this failure by tearing down the IKE
     *       Session and notifying the caller. In this way, the caller can immediately re-establish
     *       the IKE session if needed.
     * </ul>
     *
     * <p>As indicated in RFC 7296 section 2.8, "when rekeying, the new Child SA SHOULD NOT have
     * different Traffic Selectors and algorithms than the old one."
     */
    class MobikeRekeyChildLocalCreate extends RekeyChildLocalCreate {
        @Override
        protected void notifyCallerForLocalChildSaRekey() {
            IpSecTransform inTransform = mLocalInitNewChildSaRecord.getInboundIpSecTransform();
            IpSecTransform outTransform = mLocalInitNewChildSaRecord.getOutboundIpSecTransform();
            executeUserCallback(
                    () -> mUserCallback.onIpSecTransformsMigrated(inTransform, outTransform));
        }

        @Override
        protected void handleProcessRespOrSaCreationFailAndQuit(
                int registeredSpi, Exception exception) {
            sendDeleteIkeRequest();

            recordMetricsEvent_sessionTerminated(wrapAsIkeException(exception));
            mChildSmCallback.onFatalIkeSessionError(exception);
        }

        @Override
        protected void handleErrorNotify(Exception exception) {
            loge("Received error notification for rekey Child. Tear down IKE SA");
            sendDeleteIkeRequest();

            recordMetricsEvent_sessionTerminated(wrapAsIkeException(exception));
            mChildSmCallback.onFatalIkeSessionError(exception);
        }

        @Override
        protected @IkeMetrics.IkeState int getMetricsStateCode() {
            return IkeMetrics.IKE_STATE_CHILD_MOBIKE_REKEY_LOCAL_CREATE;
        }
    }

    private ChildSaProposal addDhGroupsFromChildSessionParamsIfAbsent() {
        // DH groups are excluded for the first child. Add dh groups from child session params in
        // this case.
        if (mSaProposal.getDhGroups().size() != 0) {
            return mSaProposal;
        }

        Set<DhGroupTransform> dhGroupSet = new LinkedHashSet<>();
        for (SaProposal saProposal : mChildSessionParams.getSaProposals()) {
            if (!mSaProposal.isNegotiatedFromExceptDhGroup(saProposal)) continue;
            dhGroupSet.addAll(Arrays.asList(saProposal.getDhGroupTransforms()));
        }

        DhGroupTransform[] dhGroups = new DhGroupTransform[dhGroupSet.size()];
        dhGroupSet.toArray(dhGroups);

        return new ChildSaProposal(
                mSaProposal.getEncryptionTransforms(),
                mSaProposal.getIntegrityTransforms(),
                dhGroups,
                mSaProposal.getEsnTransforms());
    }

    /**
     * RekeyChildRemoteCreate represents the state where Child Session receives a Rekey Child
     * request.
     *
     * <p>As indicated in RFC 7296 section 2.8, "when rekeying, the new Child SA SHOULD NOT have
     * different Traffic Selectors and algorithms than the old one."
     *
     * <p>Errors in this exchange with no specific protocol error code will all be classified to use
     * NO_PROPOSAL_CHOSEN. The reason that we don't use NO_ADDITIONAL_SAS is because it indicates
     * "responder is unwilling to accept any more Child SAs on this IKE SA.", according to RFC 7296.
     * Sending this error may mislead the remote peer.
     */
    class RekeyChildRemoteCreate extends ExceptionHandler {
        @Override
        public boolean processStateMessage(Message message) {
            switch (message.what) {
                case CMD_HANDLE_RECEIVED_REQUEST:
                    ReceivedRequest req = (ReceivedRequest) message.obj;

                    if (req.exchangeSubtype == IKE_EXCHANGE_SUBTYPE_REKEY_CHILD) {
                        handleCreateChildRequest(req);
                        return HANDLED;
                    }

                    return NOT_HANDLED;
                default:
                    return NOT_HANDLED;
            }
        }

        private void handleCreateChildRequest(ReceivedRequest req) {
            List<IkePayload> reqPayloads = null;
            List<IkePayload> respPayloads = null;
            try {
                reqPayloads = req.requestPayloads;

                // Build a rekey response payload list with our previously selected proposal,
                // against which we will validate the received request. It is guaranteed in
                // IkeSessionStateMachine#getIkeExchangeSubType that a SA Payload is included in the
                // inbound request payload list.
                IkeSaPayload reqSaPayload =
                        IkePayload.getPayloadForTypeInProvidedList(
                                PAYLOAD_TYPE_SA, IkeSaPayload.class, reqPayloads);

                IkeKePayload reqKePayload =
                        IkePayload.getPayloadForTypeInProvidedList(
                                PAYLOAD_TYPE_KE, IkeKePayload.class, reqPayloads);

                ChildSaProposal saProposal = mSaProposal;
                if (reqKePayload != null) {
                    saProposal =
                            reqSaPayload.getNegotiatedChildProposalWithDh(
                                    mSaProposal,
                                    mChildSessionParams.getChildSaProposals(),
                                    reqKePayload.dhGroup,
                                    mIkeDhGroup);
                }


                byte respProposalNumber = reqSaPayload.getNegotiatedProposalNumber(saProposal);

                respPayloads =
                        CreateChildSaHelper.getRekeyChildCreateRespPayloads(
                                mIkeContext.getRandomnessFactory(),
                                mIpSecSpiGenerator,
                                mLocalAddress,
                                respProposalNumber,
                                saProposal,
                                mLocalTs,
                                mRemoteTs,
                                mCurrentChildSaRecord.getLocalSpi(),
                                mChildSessionParams.isTransportMode());
            } catch (NoValidProposalChosenException | InvalidKeException e) {
                handleCreationFailureAndBackToIdle(e);
                return;
            } catch (SpiUnavailableException | ResourceUnavailableException e) {
                handleCreationFailureAndBackToIdle(
                        new NoValidProposalChosenException("Fail to assign inbound SPI", e));
                return;
            }

            CreateChildResult createChildResult =
                    CreateChildSaHelper.validateAndNegotiateRekeyChildRequest(
                            reqPayloads,
                            respPayloads,
                            req.exchangeType /*exchangeType*/,
                            EXCHANGE_TYPE_CREATE_CHILD_SA /*expectedExchangeType*/,
                            mChildSessionParams.isTransportMode(),
                            mIpSecSpiGenerator,
                            mRemoteAddress);

            switch (createChildResult.status) {
                case CREATE_STATUS_OK:
                    try {
                        // Do not need to update TS because they are not changed.
                        mSaProposal = createChildResult.negotiatedProposal;

                        mRemoteInitNewChildSaRecord =
                                ChildSaRecord.makeChildSaRecord(
                                        mIkeContext.getContext(),
                                        reqPayloads,
                                        respPayloads,
                                        createChildResult.initSpi,
                                        createChildResult.respSpi,
                                        mLocalAddress,
                                        mRemoteAddress,
                                        mUdpEncapSocket,
                                        mIkePrf,
                                        mChildIntegrity,
                                        mChildCipher,
                                        mSkD,
                                        mChildSessionParams.isTransportMode(),
                                        false /*isLocalInit*/,
                                        buildSaLifetimeAlarmSched(
                                                createChildResult.initSpi.getSpi()));

                        mChildSmCallback.onChildSaCreated(
                                mRemoteInitNewChildSaRecord.getRemoteSpi(),
                                ChildSessionStateMachine.this);

                        // To avoid traffic loss, outbound transform should only be applied once
                        // the remote has (implicitly) acknowledged our response via the
                        // delete-old-SA request. This will be performed in the finishRekey()
                        // method.
                        IpSecTransform inTransform =
                                mRemoteInitNewChildSaRecord.getInboundIpSecTransform();
                        executeUserCallback(
                                () -> {
                                    mUserCallback.onIpSecTransformCreated(
                                            inTransform, IpSecManager.DIRECTION_IN);
                                });

                        mChildSmCallback.onOutboundPayloadsReady(
                                EXCHANGE_TYPE_CREATE_CHILD_SA,
                                true /*isResp*/,
                                respPayloads,
                                ChildSessionStateMachine.this);

                        List<Integer> integrityAlgorithms = mSaProposal.getIntegrityAlgorithms();
                        List<Integer> dhGroups = mSaProposal.getDhGroups();

                        recordMetricsEvent_SaNegotiation(
                                dhGroups.isEmpty()
                                        ? IkeMetrics.DH_GROUP_UNSPECIFIED
                                        : dhGroups.get(0),
                                mSaProposal.getEncryptionTransforms()[0].id,
                                mSaProposal.getEncryptionTransforms()[0].getSpecifiedKeyLength(),
                                integrityAlgorithms.isEmpty()
                                        ? IkeMetrics.INTEGRITY_ALGORITHM_NONE
                                        : integrityAlgorithms.get(0),
                                IkeMetrics.PSEUDORANDOM_FUNCTION_UNSPECIFIED,
                                null);

                        transitionTo(mRekeyChildRemoteDelete);
                    } catch (GeneralSecurityException
                            | ResourceUnavailableException
                            | SpiUnavailableException
                            | IOException e) {
                        // #makeChildSaRecord failed.
                        handleCreationFailureAndBackToIdle(
                                new NoValidProposalChosenException(
                                        "Error in Child SA creation", e));
                    } finally {
                        // In the successful case the transform in ChildSaRecord has taken ownership
                        // of the SPI (in IpSecService), and will keep it alive.
                        createChildResult.initSpi.close();
                        createChildResult.respSpi.close();
                    }
                    break;
                case CREATE_STATUS_CHILD_ERROR_INVALID_MSG:
                    IkeException error = createChildResult.exception;
                    if (error instanceof IkeProtocolException) {
                        handleCreationFailureAndBackToIdle((IkeProtocolException) error);
                    } else {
                        handleCreationFailureAndBackToIdle(
                                new NoValidProposalChosenException(
                                        "Error in validating Create Child request", error));
                    }
                    break;
                case CREATE_STATUS_CHILD_ERROR_RCV_NOTIFY:
                    cleanUpAndQuit(
                            new IllegalStateException(
                                    "Unexpected processing status in Create Child request: "
                                            + createChildResult.status));
                    break;
                default:
                    cleanUpAndQuit(
                            new IllegalStateException(
                                    "Unrecognized status: " + createChildResult.status));
            }
        }

        private void handleCreationFailureAndBackToIdle(IkeProtocolException e) {
            loge("Received invalid Rekey Child request. Reject with error notification", e);

            ArrayList<IkePayload> payloads = new ArrayList<>(1);
            payloads.add(e.buildNotifyPayload());
            mChildSmCallback.onOutboundPayloadsReady(
                    EXCHANGE_TYPE_CREATE_CHILD_SA,
                    true /*isResp*/,
                    payloads,
                    ChildSessionStateMachine.this);

            transitionTo(mIdle);
        }

        @Override
        protected @IkeMetrics.IkeState int getMetricsStateCode() {
            return IkeMetrics.IKE_STATE_CHILD_REKEY_REMOTE_CREATE;
        }
    }

    /**
     * RekeyChildDeleteBase represents common behaviours of deleting stage during rekeying Child SA.
     */
    abstract class RekeyChildDeleteBase extends DeleteBase {
        @Override
        public boolean processStateMessage(Message message) {
            switch (message.what) {
                case CMD_HANDLE_RECEIVED_REQUEST:
                    try {
                        if (isOnNewSa((ReceivedRequest) message.obj)) {
                            finishRekey();
                            deferMessage(message);
                            transitionTo(mIdleWithDeferredRequest);
                            return HANDLED;
                        }
                        return NOT_HANDLED;
                    } catch (IllegalStateException e) {
                        cleanUpAndQuit(e);
                        return HANDLED;
                    }
                default:
                    return NOT_HANDLED;
            }
        }

        private boolean isOnNewSa(ReceivedRequest req) {
            switch (req.exchangeSubtype) {
                case IKE_EXCHANGE_SUBTYPE_DELETE_CHILD:
                    return hasRemoteChildSpiForDelete(req.requestPayloads, mChildSaRecordSurviving);
                case IKE_EXCHANGE_SUBTYPE_REKEY_CHILD:
                    return CreateChildSaHelper.hasRemoteChildSpiForRekey(
                            req.requestPayloads, mChildSaRecordSurviving);
                default:
                    throw new IllegalStateException(
                            "Invalid exchange subtype for Child Session: " + req.exchangeSubtype);
            }
        }

        // Rekey timer for old SA will be cancelled as part of the closing of the SA.
        protected void finishRekey() {
            OnIpSecSaPairDeletedRunnable delRunnable =
                    new OnIpSecSaPairDeletedRunnable(mCurrentChildSaRecord);
            executeUserCallback(delRunnable);

            mChildSmCallback.onChildSaDeleted(mCurrentChildSaRecord.getRemoteSpi());
            mCurrentChildSaRecord.close();

            mCurrentChildSaRecord = mChildSaRecordSurviving;

            mLocalInitNewChildSaRecord = null;
            mRemoteInitNewChildSaRecord = null;
            mChildSaRecordSurviving = null;
        }
    }

    /**
     * RekeyChildLocalDelete represents the deleting stage of a locally-initiated Rekey Child
     * procedure.
     */
    class RekeyChildLocalDelete extends RekeyChildDeleteBase {
        private boolean mSimulDeleteDetected;

        @Override
        public void enterState() {
            mSimulDeleteDetected = false;
            mChildSaRecordSurviving = mLocalInitNewChildSaRecord;
            sendDeleteChild(mCurrentChildSaRecord, false /*isResp*/);
        }

        @Override
        public boolean processStateMessage(Message message) {
            if (super.processStateMessage(message) == HANDLED) {
                return HANDLED;
            }

            switch (message.what) {
                case CMD_HANDLE_RECEIVED_REQUEST:
                    ReceivedRequest req = (ReceivedRequest) message.obj;

                    if (req.exchangeSubtype == IKE_EXCHANGE_SUBTYPE_DELETE_CHILD) {
                        // Reply with empty message during simultaneous deleting and keep waiting
                        // for Delete response.
                        mChildSmCallback.onOutboundPayloadsReady(
                                EXCHANGE_TYPE_INFORMATIONAL,
                                true /*isResp*/,
                                new ArrayList<>(),
                                ChildSessionStateMachine.this);
                        mSimulDeleteDetected = true;
                    } else {
                        replyErrorNotification(ERROR_TYPE_TEMPORARY_FAILURE);
                    }
                    return HANDLED;
                case CMD_HANDLE_RECEIVED_RESPONSE:
                    try {
                        ReceivedResponse resp = (ReceivedResponse) message.obj;
                        validateDeleteRespPayloadAndExchangeType(
                                resp.responsePayloads, resp.exchangeType);

                        boolean currentSaSpiFound =
                                hasRemoteChildSpiForDelete(
                                        resp.responsePayloads, mCurrentChildSaRecord);
                        if (!mSimulDeleteDetected && !currentSaSpiFound) {
                            loge(
                                    "Found no remote SPI for current SA in received Delete"
                                        + " response. Shutting down old SA and finishing rekey.");
                        }
                    } catch (IkeProtocolException e) {
                        loge(
                                "Received Delete response with invalid syntax or error"
                                    + " notifications. Shutting down old SA and finishing rekey.",
                                e);
                    }
                    finishRekey();
                    transitionTo(mIdle);
                    return HANDLED;
                default:
                    return NOT_HANDLED;
            }
        }

        @Override
        protected @IkeMetrics.IkeState int getMetricsStateCode() {
            return IkeMetrics.IKE_STATE_CHILD_REKEY_LOCAL_DELETE;
        }
    }

    /**
     * RekeyChildRemoteDelete represents the deleting stage of a remotely-initiated Rekey Child
     * procedure.
     */
    class RekeyChildRemoteDelete extends RekeyChildDeleteBase {
        @Override
        public void enterState() {
            mChildSaRecordSurviving = mRemoteInitNewChildSaRecord;
            sendMessageDelayed(TIMEOUT_REKEY_REMOTE_DELETE, REKEY_DELETE_TIMEOUT_MS);
        }

        @Override
        public boolean processStateMessage(Message message) {
            if (super.processStateMessage(message) == HANDLED) {
                return HANDLED;
            }

            switch (message.what) {
                case CMD_HANDLE_RECEIVED_REQUEST:
                    ReceivedRequest req = (ReceivedRequest) message.obj;

                    if (req.exchangeSubtype == IKE_EXCHANGE_SUBTYPE_DELETE_CHILD) {
                        handleDeleteRequest(req.requestPayloads);

                    } else {
                        replyErrorNotification(ERROR_TYPE_TEMPORARY_FAILURE);
                    }
                    return HANDLED;
                case TIMEOUT_REKEY_REMOTE_DELETE:
                    // Receiving this signal means the remote side has received the outbound
                    // Rekey-Create response since no retransmissions were received during the
                    // waiting time. IKE library will assume the remote side has set up the new
                    // Child SA and finish the rekey procedure. Users should be warned there is
                    // a risk that the remote side failed to set up the new Child SA and all
                    // outbound IPsec traffic protected by new Child SA will be dropped.

                    // TODO:Consider finishing rekey procedure if the IKE Session receives a new
                    // request. Since window size is one, receiving a new request indicates the
                    // remote side has received the outbound Rekey-Create response

                    finishRekey();
                    transitionTo(mIdle);
                    return HANDLED;
                default:
                    return NOT_HANDLED;
            }
        }

        private void handleDeleteRequest(List<IkePayload> payloads) {
            if (!hasRemoteChildSpiForDelete(payloads, mCurrentChildSaRecord)) {
                // Request received on incorrect SA
                cleanUpAndQuit(
                        new IllegalStateException(
                                "Found no remote SPI for current SA in received Delete"
                                        + " response."));
            } else {
                sendDeleteChild(mCurrentChildSaRecord, true /*isResp*/);
                finishRekey();
                transitionTo(mIdle);
            }
        }

        @Override
        protected void finishRekey() {
            IpSecTransform outTransform = mRemoteInitNewChildSaRecord.getOutboundIpSecTransform();
            executeUserCallback(
                    () -> {
                        mUserCallback.onIpSecTransformCreated(
                                outTransform, IpSecManager.DIRECTION_OUT);
                    });

            super.finishRekey();
        }

        @Override
        public void exitState() {
            removeMessages(TIMEOUT_REKEY_REMOTE_DELETE);
        }

        @Override
        protected @IkeMetrics.IkeState int getMetricsStateCode() {
            return IkeMetrics.IKE_STATE_CHILD_REKEY_REMOTE_DELETE;
        }
    }

    @Override
    protected @IkeMetrics.IkeSessionType int getMetricsSessionType() {
        return IkeMetrics.IKE_SESSION_TYPE_CHILD;
    }

    /**
     * Package private helper class to generate IKE SA creation payloads, in both request and
     * response directions.
     */
    static class CreateChildSaHelper {
        /** Create payload list for creating the initial Child SA for this Child Session. */
        public static List<IkePayload> getInitChildCreateReqPayloads(
                RandomnessFactory randomFactory,
                IpSecSpiGenerator ipSecSpiGenerator,
                InetAddress localAddress,
                ChildSessionParams childSessionParams,
                boolean isFirstChildSa)
                throws SpiUnavailableException, ResourceUnavailableException {

            ChildSaProposal[] saProposals = childSessionParams.getSaProposalsInternal();

            if (isFirstChildSa) {
                for (int i = 0; i < saProposals.length; i++) {
                    saProposals[i] =
                            childSessionParams.getSaProposalsInternal()[i]
                                    .getCopyWithoutDhTransform();
                }
            }

            List<IkePayload> payloadList =
                    getChildCreatePayloads(
                            IkeSaPayload.createChildSaRequestPayload(
                                    saProposals, ipSecSpiGenerator, localAddress),
                            childSessionParams.getInboundTrafficSelectorsInternal(),
                            childSessionParams.getOutboundTrafficSelectorsInternal(),
                            childSessionParams.isTransportMode(),
                            isFirstChildSa,
                            randomFactory);

            return payloadList;
        }

        public static ConfigAttribute[] getConfigAttributes(ChildSessionParams params) {
            if (!params.isTransportMode()) {
                return ((TunnelModeChildSessionParams) params).getConfigurationAttributesInternal();
            }
            return new ConfigAttribute[0];
        }

        /** Create payload list as a rekey Child Session request. */
        public static List<IkePayload> getRekeyChildCreateReqPayloads(
                RandomnessFactory randomFactory,
                IpSecSpiGenerator ipSecSpiGenerator,
                InetAddress localAddress,
                ChildSaProposal currentProposal,
                IkeTrafficSelector[] currentLocalTs,
                IkeTrafficSelector[] currentRemoteTs,
                int localSpi,
                boolean isTransport)
                throws SpiUnavailableException, ResourceUnavailableException {
            List<IkePayload> payloads =
                    getChildCreatePayloads(
                            IkeSaPayload.createChildSaRequestPayload(
                                    new ChildSaProposal[] {currentProposal},
                                    ipSecSpiGenerator,
                                    localAddress),
                            currentLocalTs,
                            currentRemoteTs,
                            isTransport,
                            false /*isFirstChildSa*/,
                            randomFactory);

            payloads.add(
                    new IkeNotifyPayload(
                            PROTOCOL_ID_ESP, localSpi, NOTIFY_TYPE_REKEY_SA, new byte[0]));
            return payloads;
        }

        /** Create payload list as a rekey Child Session response. */
        public static List<IkePayload> getRekeyChildCreateRespPayloads(
                RandomnessFactory randomFactory,
                IpSecSpiGenerator ipSecSpiGenerator,
                InetAddress localAddress,
                byte proposalNumber,
                ChildSaProposal currentProposal,
                IkeTrafficSelector[] currentLocalTs,
                IkeTrafficSelector[] currentRemoteTs,
                int localSpi,
                boolean isTransport)
                throws SpiUnavailableException, ResourceUnavailableException {
            List<IkePayload> payloads =
                    getChildCreatePayloads(
                            IkeSaPayload.createChildSaResponsePayload(
                                    proposalNumber,
                                    currentProposal,
                                    ipSecSpiGenerator,
                                    localAddress),
                            currentRemoteTs /*initTs*/,
                            currentLocalTs /*respTs*/,
                            isTransport,
                            false /*isFirstChildSa*/,
                            randomFactory);

            payloads.add(
                    new IkeNotifyPayload(
                            PROTOCOL_ID_ESP, localSpi, NOTIFY_TYPE_REKEY_SA, new byte[0]));
            return payloads;
        }

        /** Create payload list for creating a new Child SA. */
        private static List<IkePayload> getChildCreatePayloads(
                IkeSaPayload saPayload,
                IkeTrafficSelector[] initTs,
                IkeTrafficSelector[] respTs,
                boolean isTransport,
                boolean isFirstChildSa,
                RandomnessFactory randomFactory)
                throws ResourceUnavailableException {
            List<IkePayload> payloadList = new ArrayList<>(5);

            payloadList.add(saPayload);
            payloadList.add(new IkeTsPayload(true /*isInitiator*/, initTs));
            payloadList.add(new IkeTsPayload(false /*isInitiator*/, respTs));

            if (!isFirstChildSa) {
                payloadList.add(new IkeNoncePayload(randomFactory));
            }

            DhGroupTransform[] dhGroups =
                    ((ChildProposal) saPayload.proposalList.get(0))
                            .saProposal.getDhGroupTransforms();
            if (dhGroups.length != 0 && dhGroups[0].id != DH_GROUP_NONE) {
                payloadList.add(
                        IkeKePayload.createOutboundKePayload(dhGroups[0].id, randomFactory));
            }

            if (isTransport) payloadList.add(new IkeNotifyPayload(NOTIFY_TYPE_USE_TRANSPORT_MODE));

            return payloadList;
        }

        /**
         * Validate the received response of initial Create Child SA exchange and return the
         * negotiation result.
         */
        public static CreateChildResult validateAndNegotiateInitChild(
                List<IkePayload> reqPayloads,
                List<IkePayload> respPayloads,
                @ExchangeType int exchangeType,
                @ExchangeType int expectedExchangeType,
                boolean expectTransport,
                IpSecSpiGenerator ipSecSpiGenerator,
                InetAddress remoteAddress) {

            return validateAndNegotiateChild(
                    reqPayloads,
                    respPayloads,
                    exchangeType,
                    expectedExchangeType,
                    true /*isLocalInit*/,
                    expectTransport,
                    ipSecSpiGenerator,
                    remoteAddress);
        }

        /**
         * Validate the received rekey-create request against locally built response (based on
         * previously negotiated Child SA) and return the negotiation result.
         */
        public static CreateChildResult validateAndNegotiateRekeyChildRequest(
                List<IkePayload> reqPayloads,
                List<IkePayload> respPayloads,
                @ExchangeType int exchangeType,
                @ExchangeType int expectedExchangeType,
                boolean expectTransport,
                IpSecSpiGenerator ipSecSpiGenerator,
                InetAddress remoteAddress) {

            // It is guaranteed that a Rekey-Notify Payload with remote SPI of current Child SA is
            // included in the reqPayloads. So we won't validate it again here.
            return validateAndNegotiateChild(
                    reqPayloads,
                    respPayloads,
                    exchangeType,
                    expectedExchangeType,
                    false /*isLocalInit*/,
                    expectTransport,
                    ipSecSpiGenerator,
                    remoteAddress);
        }

        /**
         * Validate the received rekey-create response against locally built request and previously
         * negotiated Child SA, and return the negotiation result.
         */
        public static CreateChildResult validateAndNegotiateRekeyChildResp(
                List<IkePayload> reqPayloads,
                List<IkePayload> respPayloads,
                @ExchangeType int exchangeType,
                @ExchangeType int expectedExchangeType,
                boolean expectTransport,
                ChildSaRecord expectedChildRecord,
                IpSecSpiGenerator ipSecSpiGenerator,
                InetAddress remoteAddress) {
            // Validate rest of payloads and negotiate Child SA.
            CreateChildResult childResult =
                    validateAndNegotiateChild(
                            reqPayloads,
                            respPayloads,
                            exchangeType,
                            expectedExchangeType,
                            true /*isLocalInit*/,
                            expectTransport,
                            ipSecSpiGenerator,
                            remoteAddress);

            // TODO: Validate new Child SA does not have different Traffic Selectors

            return childResult;
        }

        /**
         * Check if SPI of Child SA that is expected to be rekeyed is included in the provided
         * payload list.
         */
        public static boolean hasRemoteChildSpiForRekey(
                List<IkePayload> payloads, ChildSaRecord expectedRecord) {
            List<IkeNotifyPayload> notifyPayloads =
                    IkePayload.getPayloadListForTypeInProvidedList(
                            IkePayload.PAYLOAD_TYPE_NOTIFY, IkeNotifyPayload.class, payloads);

            boolean hasExpectedRekeyNotify = false;
            for (IkeNotifyPayload notifyPayload : notifyPayloads) {
                if (notifyPayload.notifyType == NOTIFY_TYPE_REKEY_SA
                        && notifyPayload.spi == expectedRecord.getRemoteSpi()) {
                    hasExpectedRekeyNotify = true;
                    break;
                }
            }

            return hasExpectedRekeyNotify;
        }

        public static void releaseSpiResources(List<IkePayload> reqPayloads) {
            if (reqPayloads == null) {
                return;
            }

            IkeSaPayload saPayload =
                    IkePayload.getPayloadForTypeInProvidedList(
                            IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class, reqPayloads);
            if (saPayload != null) {
                saPayload.releaseSpiResources();
            }
        }

        /** Validate the received payload list and negotiate Child SA. */
        private static CreateChildResult validateAndNegotiateChild(
                List<IkePayload> reqPayloads,
                List<IkePayload> respPayloads,
                @ExchangeType int exchangeType,
                @ExchangeType int expectedExchangeType,
                boolean isLocalInit,
                boolean expectTransport,
                IpSecSpiGenerator ipSecSpiGenerator,
                InetAddress remoteAddress) {
            List<IkePayload> inboundPayloads = isLocalInit ? respPayloads : reqPayloads;

            try {
                validatePayloadAndExchangeType(
                        inboundPayloads,
                        isLocalInit /*isResp*/,
                        exchangeType,
                        expectedExchangeType);
            } catch (InvalidSyntaxException e) {
                return new CreateChildResult(CREATE_STATUS_CHILD_ERROR_INVALID_MSG, e);
            }

            List<IkeNotifyPayload> notifyPayloads =
                    IkePayload.getPayloadListForTypeInProvidedList(
                            IkePayload.PAYLOAD_TYPE_NOTIFY,
                            IkeNotifyPayload.class,
                            inboundPayloads);

            boolean hasTransportNotify = false;
            for (IkeNotifyPayload notify : notifyPayloads) {
                if (notify.isErrorNotify()) {
                    try {
                        IkeProtocolException exception = notify.validateAndBuildIkeException();
                        if (isLocalInit) {
                            return new CreateChildResult(
                                    CREATE_STATUS_CHILD_ERROR_RCV_NOTIFY, exception);
                        } else {
                            logw("Received unexpected error notification: " + notify.notifyType);
                        }
                    } catch (InvalidSyntaxException e) {
                        return new CreateChildResult(CREATE_STATUS_CHILD_ERROR_INVALID_MSG, e);
                    }
                }

                switch (notify.notifyType) {
                    case IkeNotifyPayload.NOTIFY_TYPE_ADDITIONAL_TS_POSSIBLE:
                        // TODO: Store it as part of negotiation results that can be retrieved
                        // by users.
                        break;
                    case IkeNotifyPayload.NOTIFY_TYPE_IPCOMP_SUPPORTED:
                        // Ignore
                        break;
                    case IkeNotifyPayload.NOTIFY_TYPE_USE_TRANSPORT_MODE:
                        hasTransportNotify = true;
                        break;
                    case IkeNotifyPayload.NOTIFY_TYPE_ESP_TFC_PADDING_NOT_SUPPORTED:
                        // Ignore
                        break;
                    case IkeNotifyPayload.NOTIFY_TYPE_REKEY_SA:
                        // Handled in Rekey State. Ignore here.
                        break;
                    default:
                        // Unknown and unexpected status notifications are ignored as per RFC7296.
                        logw(
                                "Received unknown or unexpected status notifications with notify"
                                        + " type: "
                                        + notify.notifyType);
                }
            }

            Pair<ChildProposal, ChildProposal> childProposalPair = null;
            try {
                IkeSaPayload reqSaPayload =
                        IkePayload.getPayloadForTypeInProvidedList(
                                IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class, reqPayloads);
                IkeSaPayload respSaPayload =
                        IkePayload.getPayloadForTypeInProvidedList(
                                IkePayload.PAYLOAD_TYPE_SA, IkeSaPayload.class, respPayloads);

                // This method either throws exception or returns non-null pair that contains two
                // valid {@link ChildProposal} both with a {@link SecurityParameterIndex} allocated
                // inside.
                childProposalPair =
                        IkeSaPayload.getVerifiedNegotiatedChildProposalPair(
                                reqSaPayload, respSaPayload, ipSecSpiGenerator, remoteAddress);
                ChildSaProposal saProposal = childProposalPair.second.saProposal;

                validateKePayloads(inboundPayloads, isLocalInit /*isResp*/, saProposal);

                if (expectTransport != hasTransportNotify) {
                    throw new NoValidProposalChosenException(
                            "Failed the negotiation on Child SA mode (conflicting modes chosen).");
                }

                Pair<IkeTrafficSelector[], IkeTrafficSelector[]> tsPair =
                        validateAndGetNegotiatedTsPair(reqPayloads, respPayloads);

                return new CreateChildResult(
                        childProposalPair.first.getChildSpiResource(),
                        childProposalPair.second.getChildSpiResource(),
                        saProposal,
                        tsPair.first,
                        tsPair.second);
            } catch (IkeProtocolException
                    | ResourceUnavailableException
                    | SpiUnavailableException e) {
                if (childProposalPair != null) {
                    childProposalPair.first.getChildSpiResource().close();
                    childProposalPair.second.getChildSpiResource().close();
                }

                if (e instanceof InvalidSyntaxException) {
                    return new CreateChildResult(
                            CREATE_STATUS_CHILD_ERROR_INVALID_MSG, (InvalidSyntaxException) e);
                } else if (e instanceof IkeProtocolException) {
                    return new CreateChildResult(
                            CREATE_STATUS_CHILD_ERROR_INVALID_MSG,
                            new InvalidSyntaxException(
                                    "Processing error in received Create Child response", e));
                } else {
                    return new CreateChildResult(
                            CREATE_STATUS_CHILD_ERROR_INVALID_MSG, wrapAsIkeException(e));
                }
            }
        }

        // Validate syntax to make sure all necessary payloads exist and exchange type is correct.
        private static void validatePayloadAndExchangeType(
                List<IkePayload> inboundPayloads,
                boolean isResp,
                @ExchangeType int exchangeType,
                @ExchangeType int expectedExchangeType)
                throws InvalidSyntaxException {
            boolean hasSaPayload = false;
            boolean hasKePayload = false;
            boolean hasNoncePayload = false;
            boolean hasTsInitPayload = false;
            boolean hasTsRespPayload = false;
            boolean hasErrorNotify = false;

            for (IkePayload payload : inboundPayloads) {
                switch (payload.payloadType) {
                    case PAYLOAD_TYPE_SA:
                        hasSaPayload = true;
                        break;
                    case PAYLOAD_TYPE_KE:
                        // Could not decide if KE Payload MUST or MUST NOT be included until SA
                        // negotiation is done.
                        hasKePayload = true;
                        break;
                    case PAYLOAD_TYPE_NONCE:
                        hasNoncePayload = true;
                        break;
                    case PAYLOAD_TYPE_TS_INITIATOR:
                        hasTsInitPayload = true;
                        break;
                    case PAYLOAD_TYPE_TS_RESPONDER:
                        hasTsRespPayload = true;
                        break;
                    case PAYLOAD_TYPE_NOTIFY:
                        if (((IkeNotifyPayload) payload).isErrorNotify()) hasErrorNotify = true;
                        // Do not have enough context to handle all notifications. Handle them
                        // together in higher layer.
                        break;
                    case PAYLOAD_TYPE_CP:
                        // Handled in child creation state. Note Child Session can only handle
                        // Config Payload in initial creation and can only handle a Config Reply.
                        // For interoperability, Config Payloads received in rekey creation
                        // or with other config types will be ignored.
                        break;
                    default:
                        logw(
                                "Received unexpected payload in Create Child SA message. Payload"
                                        + " type: "
                                        + payload.payloadType);
                }
            }

            // Do not need to check exchange type of a request because it has been already verified
            // in IkeSessionStateMachine
            if (isResp
                    && exchangeType != expectedExchangeType
                    && exchangeType != EXCHANGE_TYPE_INFORMATIONAL) {
                throw new InvalidSyntaxException("Received invalid exchange type: " + exchangeType);
            }

            if (exchangeType == EXCHANGE_TYPE_INFORMATIONAL
                    && (hasSaPayload
                            || hasKePayload
                            || hasNoncePayload
                            || hasTsInitPayload
                            || hasTsRespPayload)) {
                logw(
                        "Unexpected payload found in an INFORMATIONAL message: SA, KE, Nonce,"
                                + " TS-Initiator or TS-Responder");
            }

            if (isResp
                    && !hasErrorNotify
                    && (!hasSaPayload
                            || !hasNoncePayload
                            || !hasTsInitPayload
                            || !hasTsRespPayload)) {
                throw new InvalidSyntaxException(
                        "SA, Nonce, TS-Initiator or TS-Responder missing.");
            }
        }

        private static Pair<IkeTrafficSelector[], IkeTrafficSelector[]>
                validateAndGetNegotiatedTsPair(
                        List<IkePayload> reqPayloads, List<IkePayload> respPayloads)
                        throws TsUnacceptableException {
            IkeTrafficSelector[] initTs =
                    validateAndGetNegotiatedTs(reqPayloads, respPayloads, true /*isInitTs*/);
            IkeTrafficSelector[] respTs =
                    validateAndGetNegotiatedTs(reqPayloads, respPayloads, false /*isInitTs*/);

            return new Pair<IkeTrafficSelector[], IkeTrafficSelector[]>(initTs, respTs);
        }

        private static IkeTrafficSelector[] validateAndGetNegotiatedTs(
                List<IkePayload> reqPayloads, List<IkePayload> respPayloads, boolean isInitTs)
                throws TsUnacceptableException {
            int tsType = isInitTs ? PAYLOAD_TYPE_TS_INITIATOR : PAYLOAD_TYPE_TS_RESPONDER;
            IkeTsPayload reqPayload =
                    IkePayload.getPayloadForTypeInProvidedList(
                            tsType, IkeTsPayload.class, reqPayloads);
            IkeTsPayload respPayload =
                    IkePayload.getPayloadForTypeInProvidedList(
                            tsType, IkeTsPayload.class, respPayloads);

            if (!reqPayload.contains(respPayload)) {
                throw new TsUnacceptableException();
            }

            // It is guaranteed by decoding inbound TS Payload and constructing outbound TS Payload
            // that each TS Payload has at least one IkeTrafficSelector.
            return respPayload.trafficSelectors;
        }

        @VisibleForTesting
        static void validateKePayloads(
                List<IkePayload> inboundPayloads,
                boolean isResp,
                ChildSaProposal negotiatedProposal)
                throws IkeProtocolException {
            DhGroupTransform[] dhTransforms = negotiatedProposal.getDhGroupTransforms();

            if (dhTransforms.length > 1) {
                throw new IllegalArgumentException(
                        "Found multiple DH Group Transforms in the negotiated SA proposal");
            }
            boolean expectKePayload =
                    dhTransforms.length == 1 && dhTransforms[0].id != DH_GROUP_NONE;

            IkeKePayload kePayload =
                    IkePayload.getPayloadForTypeInProvidedList(
                            PAYLOAD_TYPE_KE, IkeKePayload.class, inboundPayloads);

            if (expectKePayload && (kePayload == null || dhTransforms[0].id != kePayload.dhGroup)) {
                if (isResp) {
                    throw new InvalidSyntaxException(
                            "KE Payload missing or has mismatched DH Group with the negotiated"
                                    + " proposal.");
                } else {
                    throw new InvalidKeException(dhTransforms[0].id);
                }

            } else if (!expectKePayload && kePayload != null && isResp) {
                // It is valid when the remote request proposed multiple DH Groups with a KE
                // payload, and the responder chose DH_GROUP_NONE.
                throw new InvalidSyntaxException("Received unexpected KE Payload.");
            }
        }

        private static void logw(String s) {
            getIkeLog().w(TAG, s);
        }
    }

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
        CREATE_STATUS_OK,
        CREATE_STATUS_CHILD_ERROR_INVALID_MSG,
        CREATE_STATUS_CHILD_ERROR_RCV_NOTIFY
    })
    @interface CreateStatus {}

    /** The Child SA negotiation succeeds. */
    private static final int CREATE_STATUS_OK = 0;
    /** The inbound message is invalid in Child negotiation but is non-fatal for IKE Session. */
    private static final int CREATE_STATUS_CHILD_ERROR_INVALID_MSG = 1;
    /** The inbound message includes error notification that failed the Child negotiation. */
    private static final int CREATE_STATUS_CHILD_ERROR_RCV_NOTIFY = 2;

    private static class CreateChildResult {
        @CreateStatus public final int status;
        public final SecurityParameterIndex initSpi;
        public final SecurityParameterIndex respSpi;
        public final ChildSaProposal negotiatedProposal;
        public final IkeTrafficSelector[] initTs;
        public final IkeTrafficSelector[] respTs;
        public final IkeException exception;

        private CreateChildResult(
                @CreateStatus int status,
                SecurityParameterIndex initSpi,
                SecurityParameterIndex respSpi,
                ChildSaProposal negotiatedProposal,
                IkeTrafficSelector[] initTs,
                IkeTrafficSelector[] respTs,
                IkeException exception) {
            this.status = status;
            this.initSpi = initSpi;
            this.respSpi = respSpi;
            this.negotiatedProposal = negotiatedProposal;
            this.initTs = initTs;
            this.respTs = respTs;
            this.exception = exception;
        }

        /* Construct a CreateChildResult instance for a successful case. */
        CreateChildResult(
                SecurityParameterIndex initSpi,
                SecurityParameterIndex respSpi,
                ChildSaProposal negotiatedProposal,
                IkeTrafficSelector[] initTs,
                IkeTrafficSelector[] respTs) {
            this(
                    CREATE_STATUS_OK,
                    initSpi,
                    respSpi,
                    negotiatedProposal,
                    initTs,
                    respTs,
                    null /*exception*/);
        }

        /** Construct a CreateChildResult instance for an error case. */
        CreateChildResult(@CreateStatus int status, IkeException exception) {
            this(
                    status,
                    null /*initSpi*/,
                    null /*respSpi*/,
                    null /*negotiatedProposal*/,
                    null /*initTs*/,
                    null /*respTs*/,
                    exception);
        }
    }
}
