Merge "Add end-to-end testing for EAP-AKA."
diff --git a/src/java/com/android/ike/eap/message/simaka/EapAkaTypeData.java b/src/java/com/android/ike/eap/message/simaka/EapAkaTypeData.java
index 381c538..c486485 100644
--- a/src/java/com/android/ike/eap/message/simaka/EapAkaTypeData.java
+++ b/src/java/com/android/ike/eap/message/simaka/EapAkaTypeData.java
@@ -91,7 +91,7 @@
     }
 
     /**
-     * EapSimTypeDataDecoder will be used for decoding {@link EapAkaTypeData} objects.
+     * EapAkaTypeDataDecoder will be used for decoding {@link EapAkaTypeData} objects.
      */
     public static class EapAkaTypeDataDecoder extends EapSimAkaTypeDataDecoder<EapAkaTypeData> {
         private static final String TAG = EapAkaTypeDataDecoder.class.getSimpleName();
@@ -109,7 +109,7 @@
         /**
          * Decodes the given byte-array into a DecodeResult object.
          *
-         * @param typeData the byte-encoding of the EapSimTypeData to be parsed
+         * @param typeData the byte-encoding of the EapAkaTypeData to be parsed
          * @return a DecodeResult object. If the decoding is successful, this will encapsulate an
          *         EapAkaTypeData instance representing the data stored in typeData. Otherwise, it
          *         will contain the relevant AtClientErrorCode for the decoding error.
diff --git a/src/java/com/android/ike/ikev2/IkeManager.java b/src/java/com/android/ike/ikev2/IkeManager.java
index ab37429..61a67de 100644
--- a/src/java/com/android/ike/ikev2/IkeManager.java
+++ b/src/java/com/android/ike/ikev2/IkeManager.java
@@ -66,8 +66,8 @@
                 firstChildSessionCallback);
     }
 
-    // Package private
-    static Log getIkeLog() {
+    /** Returns IKE logger. */
+    public static Log getIkeLog() {
         return sIkeLog;
     }
 
diff --git a/src/java/com/android/ike/ikev2/IkeSessionStateMachine.java b/src/java/com/android/ike/ikev2/IkeSessionStateMachine.java
index 10180f1..c191037 100644
--- a/src/java/com/android/ike/ikev2/IkeSessionStateMachine.java
+++ b/src/java/com/android/ike/ikev2/IkeSessionStateMachine.java
@@ -22,8 +22,10 @@
 import static com.android.ike.ikev2.exceptions.IkeProtocolException.ErrorType;
 import static com.android.ike.ikev2.message.IkeHeader.EXCHANGE_TYPE_INFORMATIONAL;
 import static com.android.ike.ikev2.message.IkeMessage.DECODE_STATUS_OK;
-import static com.android.ike.ikev2.message.IkeMessage.DECODE_STATUS_PROTECTED_ERROR_MESSAGE;
-import static com.android.ike.ikev2.message.IkeMessage.DECODE_STATUS_UNPROTECTED_ERROR_MESSAGE;
+import static com.android.ike.ikev2.message.IkeMessage.DECODE_STATUS_PARTIAL;
+import static com.android.ike.ikev2.message.IkeMessage.DECODE_STATUS_PROTECTED_ERROR;
+import static com.android.ike.ikev2.message.IkeMessage.DECODE_STATUS_UNPROTECTED_ERROR;
+import static com.android.ike.ikev2.message.IkeNotifyPayload.NOTIFY_TYPE_IKEV2_FRAGMENTATION_SUPPORTED;
 import static com.android.ike.ikev2.message.IkeNotifyPayload.NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP;
 import static com.android.ike.ikev2.message.IkeNotifyPayload.NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP;
 import static com.android.ike.ikev2.message.IkeNotifyPayload.NOTIFY_TYPE_REKEY_SA;
@@ -76,6 +78,9 @@
 import com.android.ike.ikev2.message.IkeKePayload;
 import com.android.ike.ikev2.message.IkeMessage;
 import com.android.ike.ikev2.message.IkeMessage.DecodeResult;
+import com.android.ike.ikev2.message.IkeMessage.DecodeResultError;
+import com.android.ike.ikev2.message.IkeMessage.DecodeResultOk;
+import com.android.ike.ikev2.message.IkeMessage.DecodeResultPartial;
 import com.android.ike.ikev2.message.IkeNoncePayload;
 import com.android.ike.ikev2.message.IkeNotifyPayload;
 import com.android.ike.ikev2.message.IkePayload;
@@ -133,6 +138,11 @@
 
     private static final String TAG = "IkeSessionStateMachine";
 
+    // TODO: b/140579254 Allow users to configure fragment size.
+
+    // Default fragment size in bytes.
+    @VisibleForTesting static final int DEFAULT_FRAGMENT_SIZE = 1280;
+
     // TODO: Add SA_HARD_LIFETIME_MS
 
     // Time after which IKE SA needs to be rekeyed
@@ -265,8 +275,11 @@
     final HashMap<IChildSessionCallback, ChildSessionStateMachine> mChildCbToSessions =
             new HashMap<>();
 
-    @VisibleForTesting byte[] mLastReceivedIkeReq;
-    @VisibleForTesting byte[] mLastSentIkeResp;
+    // TODO: Store them in IkeSaRecord
+    // If received request is identical in byte with last received request or the first fragment of
+    // last received request, retransmit the cached response.
+    @VisibleForTesting byte[] mLastReceivedIkeReqFirstPacket;
+    @VisibleForTesting byte[][] mLastSentIkeResp;
 
     /**
      * Package private socket that sends and receives encoded IKE message. Initialized in Initial
@@ -286,6 +299,9 @@
     /** Indicates if remote node is behind a NAT. */
     @VisibleForTesting boolean mIsRemoteBehindNat;
 
+    /** Indicates if both sides support fragmentation. Set in IKE INIT */
+    @VisibleForTesting boolean mSupportFragment;
+
     /** Package private SaProposal that represents the negotiated IKE SA proposal. */
     @VisibleForTesting SaProposal mSaProposal;
 
@@ -1148,10 +1164,17 @@
     // Sends the provided IkeMessage using the provided IKE SA record
     @VisibleForTesting
     void sendEncryptedIkeMessage(IkeSaRecord ikeSaRecord, IkeMessage msg) {
-        byte[] bytes = msg.encryptAndEncode(mIkeIntegrity, mIkeCipher, ikeSaRecord);
-        mIkeSocket.sendIkePacket(bytes, mRemoteAddress);
-
-        if (msg.ikeHeader.isResponseMsg) mLastSentIkeResp = bytes;
+        byte[][] packetList =
+                msg.encryptAndEncode(
+                        mIkeIntegrity,
+                        mIkeCipher,
+                        ikeSaRecord,
+                        mSupportFragment,
+                        DEFAULT_FRAGMENT_SIZE);
+        for (byte[] packet : packetList) {
+            mIkeSocket.sendIkePacket(packet, mRemoteAddress);
+        }
+        if (msg.ikeHeader.isResponseMsg) mLastSentIkeResp = packetList;
     }
 
     // Builds and sends IKE-level error notification response on the provided IKE SA record
@@ -1362,36 +1385,45 @@
                                 mIkeCipher,
                                 ikeSaRecord,
                                 ikeHeader,
-                                ikePacketBytes);
+                                ikePacketBytes,
+                                ikeSaRecord.getCollectedFragments(true /*isResp*/));
                 switch (decodeResult.status) {
                     case DECODE_STATUS_OK:
                         ikeSaRecord.incrementLocalRequestMessageId();
+                        ikeSaRecord.resetCollectedFragments(true /*isResp*/);
 
-                        if (isTempFailure(decodeResult.ikeMessage)) {
+                        DecodeResultOk resultOk = (DecodeResultOk) decodeResult;
+                        if (isTempFailure(resultOk.ikeMessage)) {
                             handleTempFailure();
                         } else {
                             mTempFailHandler.reset();
                         }
 
-                        handleResponseIkeMessage(decodeResult.ikeMessage);
+                        handleResponseIkeMessage(resultOk.ikeMessage);
                         break;
-                    case DECODE_STATUS_PROTECTED_ERROR_MESSAGE:
-                        loge(methodTag + "Protected error", decodeResult.ikeException);
+                    case DECODE_STATUS_PARTIAL:
+                        ikeSaRecord.updateCollectedFragments(
+                                (DecodeResultPartial) decodeResult, true /*isResp*/);
+                        break;
+                    case DECODE_STATUS_PROTECTED_ERROR:
+                        IkeException ikeException = ((DecodeResultError) decodeResult).ikeException;
+                        logi(methodTag + "Protected error", ikeException);
 
                         ikeSaRecord.incrementLocalRequestMessageId();
+                        ikeSaRecord.resetCollectedFragments(true /*isResp*/);
 
                         handleResponseGenericProcessError(
                                 ikeSaRecord,
                                 new InvalidSyntaxException(
                                         "Generic processing error in the received response",
-                                        decodeResult.ikeException));
+                                        ikeException));
                         break;
-                    case DECODE_STATUS_UNPROTECTED_ERROR_MESSAGE:
+                    case DECODE_STATUS_UNPROTECTED_ERROR:
                         logi(
                                 methodTag
                                         + "Message authentication or decryption failed on received"
                                         + " response. Discard it",
-                                decodeResult.ikeException);
+                                ((DecodeResultError) decodeResult).ikeException);
                         break;
                     default:
                         cleanUpAndQuit(
@@ -1402,10 +1434,12 @@
             } else {
                 int expectedMsgId = ikeSaRecord.getRemoteRequestMessageId();
                 if (expectedMsgId - 1 == ikeHeader.messageId) {
-                    if (Arrays.equals(mLastReceivedIkeReq, ikePacketBytes)) {
+                    if (Arrays.equals(mLastReceivedIkeReqFirstPacket, ikePacketBytes)) {
                         logd("Received re-transmitted request. Retransmitting response");
 
-                        mIkeSocket.sendIkePacket(mLastSentIkeResp, mRemoteAddress);
+                        for (byte[] packet : mLastSentIkeResp) {
+                            mIkeSocket.sendIkePacket(packet, mRemoteAddress);
+                        }
 
                         // Notify state if it is listening for retransmitted request.
                         handleRetransmittedReq();
@@ -1421,12 +1455,16 @@
                                     mIkeCipher,
                                     ikeSaRecord,
                                     ikeHeader,
-                                    ikePacketBytes);
+                                    ikePacketBytes,
+                                    ikeSaRecord.getCollectedFragments(false /*isResp*/));
                     switch (decodeResult.status) {
                         case DECODE_STATUS_OK:
                             ikeSaRecord.incrementRemoteRequestMessageId();
-                            mLastReceivedIkeReq = ikePacketBytes;
-                            IkeMessage ikeMessage = decodeResult.ikeMessage;
+                            ikeSaRecord.resetCollectedFragments(false /*isResp*/);
+
+                            // TODO:b/140429499 Only store the first fragment
+                            mLastReceivedIkeReqFirstPacket = ikePacketBytes;
+                            IkeMessage ikeMessage = ((DecodeResultOk) decodeResult).ikeMessage;
 
                             // Handle DPD here.
                             if (ikeMessage.isDpdRequest()) {
@@ -1468,24 +1506,32 @@
                             }
                             handleRequestIkeMessage(ikeMessage, ikeExchangeSubType, message);
                             break;
-                        case DECODE_STATUS_PROTECTED_ERROR_MESSAGE:
-                            loge(methodTag + "Protected error", decodeResult.ikeException);
+                        case DECODE_STATUS_PARTIAL:
+                            ikeSaRecord.updateCollectedFragments(
+                                    (DecodeResultPartial) decodeResult, false /*isResp*/);
+                            break;
+                        case DECODE_STATUS_PROTECTED_ERROR:
+                            IkeException ikeException =
+                                    ((DecodeResultError) decodeResult).ikeException;
+                            logi(methodTag + "Protected error", ikeException);
 
                             ikeSaRecord.incrementRemoteRequestMessageId();
-                            mLastReceivedIkeReq = ikePacketBytes;
+                            ikeSaRecord.resetCollectedFragments(false /*isResp*/);
+
+                            mLastReceivedIkeReqFirstPacket = ikePacketBytes;
 
                             // IkeException MUST be already wrapped into an IkeProtocolException
                             handleRequestGenericProcessError(
                                     ikeSaRecord,
                                     ikeHeader.messageId,
-                                    (IkeProtocolException) decodeResult.ikeException);
+                                    (IkeProtocolException) ikeException);
                             break;
-                        case DECODE_STATUS_UNPROTECTED_ERROR_MESSAGE:
+                        case DECODE_STATUS_UNPROTECTED_ERROR:
                             logi(
                                     methodTag
                                             + "Message authentication or decryption failed on"
                                             + " received request. Discard it",
-                                    decodeResult.ikeException);
+                                    ((DecodeResultError) decodeResult).ikeException);
                             break;
                         default:
                             cleanUpAndQuit(
@@ -2279,6 +2325,8 @@
         private IkeSecurityParameterIndex mRemoteIkeSpiResource;
         private Retransmitter mRetransmitter;
 
+        // TODO: Support negotiating IKE fragmentation
+
         @Override
         public void enterState() {
             try {
@@ -2336,7 +2384,7 @@
 
                 switch (decodeResult.status) {
                     case DECODE_STATUS_OK:
-                        handleResponseIkeMessage(decodeResult.ikeMessage);
+                        handleResponseIkeMessage(((DecodeResultOk) decodeResult).ikeMessage);
                         mIkeInitResponseBytes = ikePacketBytes;
 
                         // SA negotiation failed
@@ -2344,15 +2392,20 @@
 
                         mCurrentIkeSaRecord.incrementLocalRequestMessageId();
                         break;
-                    case DECODE_STATUS_PROTECTED_ERROR_MESSAGE:
+                    case DECODE_STATUS_PARTIAL:
+                        // Fall through. We don't support IKE fragmentation here. We should never
+                        // get this status.
+                    case DECODE_STATUS_PROTECTED_ERROR:
                         // IKE INIT response is not protected. So we should never get this status
                         cleanUpAndQuit(
                                 new IllegalStateException(
                                         "Unexpected decoding status: " + decodeResult.status));
-                    case DECODE_STATUS_UNPROTECTED_ERROR_MESSAGE:
+                        break;
+                    case DECODE_STATUS_UNPROTECTED_ERROR:
                         logi(
                                 "Discard unencrypted response with syntax error",
-                                decodeResult.ikeException);
+                                ((DecodeResultError) decodeResult).ikeException);
+                        break;
                     default:
                         cleanUpAndQuit(
                                 new IllegalStateException(
@@ -2422,6 +2475,9 @@
                             mRemoteAddress,
                             mLocalPort,
                             IkeSocket.IKE_SERVER_PORT);
+            payloadList.add(
+                    new IkeNotifyPayload(
+                            IkeNotifyPayload.NOTIFY_TYPE_IKEV2_FRAGMENTATION_SUPPORTED));
 
             // TODO: Add Notification Payloads according to user configurations.
 
@@ -2505,6 +2561,9 @@
                                 }
                                 natDestPayload = notifyPayload;
                                 break;
+                            case NOTIFY_TYPE_IKEV2_FRAGMENTATION_SUPPORTED:
+                                mSupportFragment = true;
+                                break;
                             default:
                                 // Unknown and unexpected status notifications are ignored as per
                                 // RFC7296.
@@ -3628,10 +3687,12 @@
                                         mIkeCipher,
                                         ikeSaRecord,
                                         ikeHeader,
-                                        receivedIkePacket.ikePacketBytes);
+                                        receivedIkePacket.ikePacketBytes,
+                                        ikeSaRecord.getCollectedFragments(ikeHeader.isResponseMsg));
                         isMessageOnNewSa =
-                                (decodeResult.status == DECODE_STATUS_PROTECTED_ERROR_MESSAGE)
-                                        || (decodeResult.status == DECODE_STATUS_OK);
+                                (decodeResult.status == DECODE_STATUS_PROTECTED_ERROR)
+                                        || (decodeResult.status == DECODE_STATUS_OK)
+                                        || (decodeResult.status == DECODE_STATUS_PARTIAL);
                     }
 
                     // Authenticated request received on the new/surviving SA; treat it as
diff --git a/src/java/com/android/ike/ikev2/SaRecord.java b/src/java/com/android/ike/ikev2/SaRecord.java
index 5a02b2b..9edd77d 100644
--- a/src/java/com/android/ike/ikev2/SaRecord.java
+++ b/src/java/com/android/ike/ikev2/SaRecord.java
@@ -33,6 +33,7 @@
 import com.android.ike.ikev2.crypto.IkeMacPrf;
 import com.android.ike.ikev2.message.IkeKePayload;
 import com.android.ike.ikev2.message.IkeMessage;
+import com.android.ike.ikev2.message.IkeMessage.DecodeResultPartial;
 import com.android.ike.ikev2.message.IkeNoncePayload;
 import com.android.ike.ikev2.message.IkePayload;
 import com.android.internal.annotations.VisibleForTesting;
@@ -563,6 +564,9 @@
         private int mLocalRequestMessageId;
         private int mRemoteRequestMessageId;
 
+        private DecodeResultPartial mCollectedReqFragments;
+        private DecodeResultPartial mCollectedRespFragments;
+
         /** Package private */
         IkeSaRecord(
                 IkeSecurityParameterIndex initSpi,
@@ -589,6 +593,9 @@
 
             mLocalRequestMessageId = 0;
             mRemoteRequestMessageId = 0;
+
+            mCollectedReqFragments = null;
+            mCollectedRespFragments = null;
         }
 
         /**
@@ -748,6 +755,33 @@
             mRemoteRequestMessageId++;
         }
 
+        /** Return all collected IKE fragments that have been collected. */
+        public DecodeResultPartial getCollectedFragments(boolean isResp) {
+            return isResp ? mCollectedRespFragments : mCollectedReqFragments;
+        }
+
+        /**
+         * Update collected IKE fragments when receiving new IKE fragment.
+         *
+         * <p>TODO: b/140264067 Investigate if we need to support reassembling timeout. It is safe
+         * to do not support it because as an initiator, we will re-transmit the request anyway. As
+         * a responder, caching these fragments until getting a complete message won't affect
+         * anything.
+         */
+        public void updateCollectedFragments(
+                DecodeResultPartial updatedFragments, boolean isResp) {
+            if (isResp) {
+                mCollectedRespFragments = updatedFragments;
+            } else {
+                mCollectedReqFragments = updatedFragments;
+            }
+        }
+
+        /** Reset collected IKE fragemnts */
+        public void resetCollectedFragments(boolean isResp) {
+            updateCollectedFragments(null, isResp);
+        }
+
         /** Release IKE SPI resource. */
         @Override
         public void close() {
diff --git a/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBody.java b/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBody.java
index 6967efd..2286ac3 100644
--- a/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBody.java
+++ b/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBody.java
@@ -60,7 +60,7 @@
             IkeMacIntegrity integrityMac,
             IkeCipher decryptCipher,
             byte[] integrityKey,
-            byte[] decryptKey)
+            byte[] decryptionKey)
             throws IkeProtocolException, GeneralSecurityException {
         ByteBuffer inputBuffer = ByteBuffer.wrap(message);
 
@@ -82,7 +82,7 @@
         // Authenticate and decrypt.
         byte[] dataToAuthenticate = Arrays.copyOfRange(message, 0, message.length - checksumLen);
         validateChecksumOrThrow(dataToAuthenticate, integrityMac, integrityKey, mIntegrityChecksum);
-        mUnencryptedData = decrypt(mEncryptedAndPaddedData, decryptCipher, decryptKey, mIv);
+        mUnencryptedData = decrypt(mEncryptedAndPaddedData, decryptCipher, decryptionKey, mIv);
     }
 
     /**
@@ -96,7 +96,7 @@
             IkeMacIntegrity integrityMac,
             IkeCipher encryptCipher,
             byte[] integrityKey,
-            byte[] encryptKey) {
+            byte[] encryptionKey) {
         this(
                 ikeHeader,
                 firstPayloadType,
@@ -104,7 +104,7 @@
                 integrityMac,
                 encryptCipher,
                 integrityKey,
-                encryptKey,
+                encryptionKey,
                 encryptCipher.generateIv(),
                 calculatePadding(unencryptedPayloads.length, encryptCipher.getBlockSize()));
     }
@@ -118,7 +118,7 @@
             IkeMacIntegrity integrityMac,
             IkeCipher encryptCipher,
             byte[] integrityKey,
-            byte[] encryptKey,
+            byte[] encryptionKey,
             byte[] iv,
             byte[] padding) {
         mUnencryptedData = unencryptedPayloads;
@@ -126,7 +126,7 @@
         // Encrypt data
         mIv = iv;
         mEncryptedAndPaddedData =
-                encrypt(unencryptedPayloads, encryptCipher, encryptKey, iv, padding);
+                encrypt(unencryptedPayloads, encryptCipher, encryptionKey, iv, padding);
 
         // Build authenticated section using ByteBuffer. Authenticated section includes bytes from
         // beginning of IKE header to the pad length, which are concatenation of IKE header, current
@@ -189,7 +189,7 @@
     static byte[] encrypt(
             byte[] dataToEncrypt,
             IkeCipher encryptCipher,
-            byte[] encryptKey,
+            byte[] encryptionKey,
             byte[] iv,
             byte[] padding) {
         int padLength = padding.length;
@@ -198,15 +198,15 @@
         inputBuffer.put(dataToEncrypt).put(padding).put((byte) padLength);
 
         // Encrypt data.
-        return encryptCipher.encrypt(inputBuffer.array(), encryptKey, iv);
+        return encryptCipher.encrypt(inputBuffer.array(), encryptionKey, iv);
     }
 
     /** Package private for testing */
     @VisibleForTesting
     static byte[] decrypt(
-            byte[] encryptedData, IkeCipher decryptCipher, byte[] decryptKey, byte[] iv)
+            byte[] encryptedData, IkeCipher decryptCipher, byte[] decryptionKey, byte[] iv)
             throws IllegalBlockSizeException {
-        byte[] decryptedPaddedData = decryptCipher.decrypt(encryptedData, decryptKey, iv);
+        byte[] decryptedPaddedData = decryptCipher.decrypt(encryptedData, decryptionKey, iv);
 
         // Remove padding. Pad length value is the last byte of the padded unencrypted data.
         int padLength = Byte.toUnsignedInt(decryptedPaddedData[encryptedData.length - 1]);
diff --git a/src/java/com/android/ike/ikev2/message/IkeHeader.java b/src/java/com/android/ike/ikev2/message/IkeHeader.java
index 188a8b0..a4d739c 100644
--- a/src/java/com/android/ike/ikev2/message/IkeHeader.java
+++ b/src/java/com/android/ike/ikev2/message/IkeHeader.java
@@ -145,6 +145,21 @@
         mEncodedMessageLength = buffer.getInt();
     }
 
+    /** Packet private method to build header of an IKE fragemnt from current IKE header. */
+    IkeHeader makeSkfHeaderFromSkHeader() {
+        if (nextPayloadType != IkePayload.PAYLOAD_TYPE_SK) {
+            throw new IllegalArgumentException("Next payload type is not SK.");
+        }
+        return new IkeHeader(
+                ikeInitiatorSpi,
+                ikeResponderSpi,
+                IkePayload.PAYLOAD_TYPE_SKF,
+                exchangeType,
+                isResponseMsg,
+                fromIkeInitiator,
+                messageId);
+    }
+
     /*Package private*/
     @VisibleForTesting
     int getInboundMessageLength() {
@@ -155,8 +170,8 @@
         return mEncodedMessageLength;
     }
 
-    /** Validate syntax and major version of inbound IKE header. */
-    public void checkInboundValidOrThrow(int packetLength) throws IkeProtocolException {
+    /** Validate major version of inbound IKE header. */
+    public void validateMajorVersion() throws IkeProtocolException {
         if (majorVersion > 2) {
             // Receive higher version of protocol. Stop parsing.
             throw new InvalidMajorVersionException(majorVersion);
@@ -168,6 +183,15 @@
             // error.
             throw new InvalidSyntaxException("Major version is smaller than 2.");
         }
+    }
+
+    /**
+     * Validate syntax of inbound IKE header.
+     *
+     * <p>It MUST only be used for an inbound IKE header because we don't know the outbound message
+     * length before we encode it.
+     */
+    public void validateInboundHeader(int packetLength) throws IkeProtocolException {
         if (exchangeType < EXCHANGE_TYPE_IKE_SA_INIT
                 || exchangeType > EXCHANGE_TYPE_INFORMATIONAL) {
             throw new InvalidSyntaxException("Invalid IKE Exchange Type.");
diff --git a/src/java/com/android/ike/ikev2/message/IkeMessage.java b/src/java/com/android/ike/ikev2/message/IkeMessage.java
index 38366d8..88d760e 100644
--- a/src/java/com/android/ike/ikev2/message/IkeMessage.java
+++ b/src/java/com/android/ike/ikev2/message/IkeMessage.java
@@ -16,6 +16,7 @@
 
 package com.android.ike.ikev2.message;
 
+import static com.android.ike.ikev2.IkeManager.getIkeLog;
 import static com.android.ike.ikev2.message.IkePayload.PayloadType;
 
 import android.annotation.IntDef;
@@ -56,6 +57,8 @@
  *     Protocol Version 2 (IKEv2)</a>
  */
 public final class IkeMessage {
+    private static final String TAG = "IkeMessage";
+
     private static IIkeMessageHelper sIkeMessageHelper = new IkeMessageHelper();
     // Currently use Bouncy Castle as crypto security provider
     static final Provider SECURITY_PROVIDER = new BouncyCastleProvider();
@@ -120,6 +123,7 @@
      * @param ikeSaRecord ikeSaRecord where this packet is sent on.
      * @param ikeHeader header of IKE packet.
      * @param packet IKE packet as a byte array.
+     * @param collectedFragments previously received IKE fragments.
      * @return the decoding result.
      */
     public static DecodeResult decode(
@@ -128,9 +132,16 @@
             IkeCipher decryptCipher,
             IkeSaRecord ikeSaRecord,
             IkeHeader ikeHeader,
-            byte[] packet) {
+            byte[] packet,
+            DecodeResultPartial collectedFragments) {
         return sIkeMessageHelper.decode(
-                expectedMsgId, integrityMac, decryptCipher, ikeSaRecord, ikeHeader, packet);
+                expectedMsgId,
+                integrityMac,
+                decryptCipher,
+                ikeSaRecord,
+                ikeHeader,
+                packet,
+                collectedFragments);
     }
 
     private static List<IkePayload> decodePayloadList(
@@ -200,13 +211,18 @@
      * @param integrityMac the negotiated integrity algorithm.
      * @param encryptCipher the negotiated encryption algortihm.
      * @param ikeSaRecord the ikeSaRecord where this packet is sent on.
+     * @param supportFragment if IKE fragmentation is supported
+     * @param fragSize the maximum size of IKE fragment
      * @return encoded IKE message in byte array.
      */
-    public byte[] encryptAndEncode(
+    public byte[][] encryptAndEncode(
             @Nullable IkeMacIntegrity integrityMac,
             IkeCipher encryptCipher,
-            IkeSaRecord ikeSaRecord) {
-        return sIkeMessageHelper.encryptAndEncode(integrityMac, encryptCipher, ikeSaRecord, this);
+            IkeSaRecord ikeSaRecord,
+            boolean supportFragment,
+            int fragSize) {
+        return sIkeMessageHelper.encryptAndEncode(
+                integrityMac, encryptCipher, ikeSaRecord, this, supportFragment, fragSize);
     }
 
     /**
@@ -330,14 +346,18 @@
          * @param integrityMac the negotiated integrity algorithm.
          * @param encryptCipher the negotiated encryption algortihm.
          * @param ikeSaRecord the ikeSaRecord where this packet is sent on.
-         * @param ikeMessage message need to be encoded.
+         * @param ikeMessage message need to be encoded. * @param supportFragment if IKE
+         *     fragmentation is supported.
+         * @param fragSize the maximum size of IKE fragment.
          * @return encoded IKE message in byte array.
          */
-        byte[] encryptAndEncode(
+        byte[][] encryptAndEncode(
                 @Nullable IkeMacIntegrity integrityMac,
                 IkeCipher encryptCipher,
                 IkeSaRecord ikeSaRecord,
-                IkeMessage ikeMessage);
+                IkeMessage ikeMessage,
+                boolean supportFragment,
+                int fragSize);
 
         // TODO: Return DecodeResult when decoding unencrypted message
         /**
@@ -359,6 +379,7 @@
          * @param ikeSaRecord ikeSaRecord where this packet is sent on.
          * @param ikeHeader header of IKE packet.
          * @param packet IKE packet as a byte array.
+         * @param collectedFragments previously received IKE fragments.
          * @return the decoding result.
          */
         DecodeResult decode(
@@ -367,7 +388,8 @@
                 IkeCipher decryptCipher,
                 IkeSaRecord ikeSaRecord,
                 IkeHeader ikeHeader,
-                byte[] packet);
+                byte[] packet,
+                DecodeResultPartial collectedFragments);
     }
 
     /** IkeMessageHelper provides methods for decoding, encoding and processing IKE packet. */
@@ -379,11 +401,13 @@
         }
 
         @Override
-        public byte[] encryptAndEncode(
+        public byte[][] encryptAndEncode(
                 @Nullable IkeMacIntegrity integrityMac,
                 IkeCipher encryptCipher,
                 IkeSaRecord ikeSaRecord,
-                IkeMessage ikeMessage) {
+                IkeMessage ikeMessage,
+                boolean supportFragment,
+                int fragSize) {
             return encryptAndEncode(
                     ikeMessage.ikeHeader,
                     ikeMessage.ikePayloadList.isEmpty()
@@ -393,32 +417,112 @@
                     integrityMac,
                     encryptCipher,
                     ikeSaRecord.getOutboundIntegrityKey(),
-                    ikeSaRecord.getOutboundEncryptionKey());
+                    ikeSaRecord.getOutboundEncryptionKey(),
+                    supportFragment,
+                    fragSize);
         }
 
-        private byte[] encryptAndEncode(
+        @VisibleForTesting
+        byte[][] encryptAndEncode(
                 IkeHeader ikeHeader,
-                @PayloadType int firstPayload,
+                @PayloadType int firstInnerPayload,
                 byte[] unencryptedPayloads,
                 @Nullable IkeMacIntegrity integrityMac,
                 IkeCipher encryptCipher,
                 byte[] integrityKey,
-                byte[] encryptKey) {
+                byte[] encryptionKey,
+                boolean supportFragment,
+                int fragSize) {
+
             IkeSkPayload skPayload =
                     new IkeSkPayload(
                             ikeHeader,
-                            firstPayload,
+                            firstInnerPayload,
                             unencryptedPayloads,
                             integrityMac,
                             encryptCipher,
                             integrityKey,
-                            encryptKey);
+                            encryptionKey);
+            int msgLen = IkeHeader.IKE_HEADER_LENGTH + skPayload.getPayloadLength();
 
+            // Build complete IKE message
+            if (!supportFragment || msgLen <= fragSize) {
+                byte[][] packetList = new byte[1][];
+                packetList[0] = encodeHeaderAndBody(ikeHeader, skPayload, firstInnerPayload);
+
+                getIkeLog()
+                        .d(
+                                "IkeMessage",
+                                "Build a complete IKE message: " + getIkeLog().pii(packetList[0]));
+                return packetList;
+            }
+
+            // Build IKE fragments
+            int dataLenPerPacket =
+                    fragSize
+                            - IkeHeader.IKE_HEADER_LENGTH
+                            - IkePayload.GENERIC_HEADER_LENGTH
+                            - IkeSkfPayload.SKF_HEADER_LEN
+                            - encryptCipher.getIvLen()
+                            - integrityMac.getChecksumLen()
+                            - encryptCipher.getBlockSize();
+
+            // Caller of this method MUST validate fragSize is valid.
+            if (dataLenPerPacket <= 0) {
+                throw new IllegalArgumentException(
+                        "Max fragment size is too small for an IKE fragment.");
+            }
+
+            int totalFragments =
+                    (unencryptedPayloads.length + dataLenPerPacket - 1) / dataLenPerPacket;
+            IkeHeader skfHeader = ikeHeader.makeSkfHeaderFromSkHeader();
+            byte[][] packetList = new byte[totalFragments][];
+
+            ByteBuffer unencryptedDataBuffer = ByteBuffer.wrap(unencryptedPayloads);
+            for (int i = 0; i < totalFragments; i++) {
+                byte[] unencryptedData =
+                        new byte[Math.min(dataLenPerPacket, unencryptedDataBuffer.remaining())];
+                unencryptedDataBuffer.get(unencryptedData);
+
+                int fragNum = i + 1; // 1-based
+
+                IkeSkfPayload skfPayload =
+                        new IkeSkfPayload(
+                                ikeHeader,
+                                firstInnerPayload,
+                                unencryptedData,
+                                integrityMac,
+                                encryptCipher,
+                                integrityKey,
+                                encryptionKey,
+                                fragNum,
+                                totalFragments);
+
+                packetList[i] =
+                        encodeHeaderAndBody(
+                                skfHeader,
+                                skfPayload,
+                                i == 0 ? firstInnerPayload : IkePayload.PAYLOAD_TYPE_NO_NEXT);
+                getIkeLog()
+                        .d(
+                                "IkeMessage",
+                                "Build an IKE fragment ("
+                                        + (i + 1)
+                                        + "/"
+                                        + totalFragments
+                                        + "): "
+                                        + getIkeLog().pii(packetList[0]));
+            }
+
+            return packetList;
+        }
+
+        private byte[] encodeHeaderAndBody(
+                IkeHeader ikeHeader, IkeSkPayload skPayload, @PayloadType int firstInnerPayload) {
             ByteBuffer outputBuffer =
                     ByteBuffer.allocate(IkeHeader.IKE_HEADER_LENGTH + skPayload.getPayloadLength());
             ikeHeader.encodeToByteBuffer(outputBuffer, skPayload.getPayloadLength());
-            skPayload.encodeToByteBuffer(firstPayload, outputBuffer);
-
+            skPayload.encodeToByteBuffer(firstInnerPayload, outputBuffer);
             return outputBuffer.array();
         }
 
@@ -429,7 +533,8 @@
                     throw new InvalidMessageIdException(header.messageId);
                 }
 
-                header.checkInboundValidOrThrow(inputPacket.length);
+                header.validateMajorVersion();
+                header.validateInboundHeader(inputPacket.length);
 
                 byte[] unencryptedPayloads =
                         Arrays.copyOfRange(
@@ -437,19 +542,14 @@
                 List<IkePayload> supportedPayloadList =
                         decodePayloadList(
                                 header.nextPayloadType, header.isResponseMsg, unencryptedPayloads);
-                return new DecodeResult(
-                        DECODE_STATUS_OK,
-                        new IkeMessage(header, supportedPayloadList),
-                        null /*ikeException*/);
+                return new DecodeResultOk(new IkeMessage(header, supportedPayloadList));
             } catch (NegativeArraySizeException | BufferUnderflowException e) {
                 // Invalid length error when parsing payload bodies.
-                return new DecodeResult(
-                        DECODE_STATUS_UNPROTECTED_ERROR_MESSAGE,
-                        null /*ikeMessage*/,
+                return new DecodeResultError(
+                        DECODE_STATUS_UNPROTECTED_ERROR,
                         new InvalidSyntaxException("Malformed IKE Payload"));
             } catch (IkeProtocolException e) {
-                return new DecodeResult(
-                        DECODE_STATUS_UNPROTECTED_ERROR_MESSAGE, null /*ikeMessage*/, e);
+                return new DecodeResultError(DECODE_STATUS_UNPROTECTED_ERROR, e);
             }
         }
 
@@ -460,7 +560,8 @@
                 IkeCipher decryptCipher,
                 IkeSaRecord ikeSaRecord,
                 IkeHeader ikeHeader,
-                byte[] packet) {
+                byte[] packet,
+                DecodeResultPartial collectedFragments) {
             return decode(
                     expectedMsgId,
                     ikeHeader,
@@ -468,7 +569,8 @@
                     integrityMac,
                     decryptCipher,
                     ikeSaRecord.getInboundIntegrityKey(),
-                    ikeSaRecord.getInboundDecryptionKey());
+                    ikeSaRecord.getInboundDecryptionKey(),
+                    collectedFragments);
         }
 
         private DecodeResult decode(
@@ -478,104 +580,324 @@
                 @Nullable IkeMacIntegrity integrityMac,
                 IkeCipher decryptCipher,
                 byte[] integrityKey,
-                byte[] decryptKey) {
-
-            if (header.nextPayloadType != IkePayload.PAYLOAD_TYPE_SK) {
+                byte[] decryptionKey,
+                DecodeResultPartial collectedFragments) {
+            if (header.nextPayloadType != IkePayload.PAYLOAD_TYPE_SK
+                    && header.nextPayloadType != IkePayload.PAYLOAD_TYPE_SKF) {
                 // TODO: b/123372339 Handle message containing unprotected payloads.
                 throw new UnsupportedOperationException("Message contains unprotected payloads");
             }
 
-            // Validate security parameters.
+            // Decrypt message and do authentication
             Pair<IkeSkPayload, Integer> pair;
             try {
-                if (header.messageId != expectedMsgId) {
-                    throw new InvalidMessageIdException(header.messageId);
-                }
                 pair =
-                        IkePayloadFactory.getIkeSkPayload(
-                                false /*isSkf*/,
+                        decryptAndAuthenticate(
+                                expectedMsgId,
+                                header,
                                 inputPacket,
                                 integrityMac,
                                 decryptCipher,
                                 integrityKey,
-                                decryptKey);
-
-                // TODO: Support decoding IkeSkfPayload
-            } catch (NegativeArraySizeException | BufferUnderflowException e) {
-                return new DecodeResult(
-                        DECODE_STATUS_UNPROTECTED_ERROR_MESSAGE,
-                        null /*ikeMessage*/,
-                        new InvalidSyntaxException("Malformed IKE Payload"));
-            } catch (GeneralSecurityException e) {
-                return new DecodeResult(
-                        DECODE_STATUS_UNPROTECTED_ERROR_MESSAGE,
-                        null /*ikeMessage*/,
-                        new IkeInternalException(e));
+                                decryptionKey);
             } catch (IkeException e) {
-                return new DecodeResult(
-                        DECODE_STATUS_UNPROTECTED_ERROR_MESSAGE, null /*ikeMessage*/, e);
+                if (collectedFragments == null) {
+                    return new DecodeResultError(DECODE_STATUS_UNPROTECTED_ERROR, e);
+                } else {
+                    getIkeLog()
+                            .i(
+                                    TAG,
+                                    "Message authentication or decryption failed on received"
+                                            + " message. Discard it ",
+                                    e);
+                    return collectedFragments;
+                }
             }
 
-            // Check is there is protocol error in this IKE message.
+            // Handle IKE fragment
+            boolean isFragment = (header.nextPayloadType == IkePayload.PAYLOAD_TYPE_SKF);
+            boolean fragReassemblyStarted = (collectedFragments != null);
+
+            if (isFragment) {
+                getIkeLog()
+                        .d(
+                                TAG,
+                                "Received an IKE fragment ("
+                                        + ((IkeSkfPayload) pair.first).fragmentNum
+                                        + "/"
+                                        + ((IkeSkfPayload) pair.first).totalFragments
+                                        + ")");
+            }
+
+            // IKE fragment reassembly has started but a complete message was received.
+            if (!isFragment && fragReassemblyStarted) {
+                getIkeLog()
+                        .w(
+                                TAG,
+                                "Received a complete IKE message while doing IKE fragment"
+                                        + " reassembly. Discard the newly received message.");
+                return collectedFragments;
+            }
+
+            byte[] decryptedBytes = pair.first.getUnencryptedData();
+            int firstPayloadType = pair.second;
+
+            // Received an IKE fragment
+            if (isFragment) {
+                validateFragmentHeader(header, inputPacket.length, collectedFragments);
+
+                // Add the recently received fragment to the reassembly queue.
+                DecodeResultPartial DecodeResultPartial =
+                        processIkeFragment(
+                                header,
+                                (IkeSkfPayload) (pair.first),
+                                pair.second,
+                                collectedFragments);
+
+                if (!DecodeResultPartial.isAllFragmentsReceived()) return DecodeResultPartial;
+
+                firstPayloadType = DecodeResultPartial.firstPayloadType;
+                decryptedBytes = DecodeResultPartial.reassembleAllFrags();
+            }
+
+            // Received or has reassembled a complete IKE message. Check if there is protocol error.
             try {
-                IkeSkPayload skPayload = pair.first;
-                int firstPayloadType = pair.second;
+                // TODO: Log IKE header information and payload types
 
                 List<IkePayload> supportedPayloadList =
-                        decodePayloadList(
-                                firstPayloadType,
-                                header.isResponseMsg,
-                                skPayload.getUnencryptedData());
+                        decodePayloadList(firstPayloadType, header.isResponseMsg, decryptedBytes);
 
-                header.checkInboundValidOrThrow(inputPacket.length);
-                return new DecodeResult(
-                        DECODE_STATUS_OK,
-                        new IkeMessage(header, supportedPayloadList),
-                        null /*ikeException*/);
+                header.validateInboundHeader(inputPacket.length);
+                return new DecodeResultOk(new IkeMessage(header, supportedPayloadList));
             } catch (NegativeArraySizeException | BufferUnderflowException e) {
                 // Invalid length error when parsing payload bodies.
-                return new DecodeResult(
-                        DECODE_STATUS_PROTECTED_ERROR_MESSAGE,
-                        null /*ikeMessage*/,
-                        new InvalidSyntaxException("Malformed IKE Payload"));
+                return new DecodeResultError(
+                        DECODE_STATUS_PROTECTED_ERROR,
+                        new InvalidSyntaxException("Malformed IKE Payload", e));
             } catch (IkeProtocolException e) {
-                return new DecodeResult(
-                        DECODE_STATUS_PROTECTED_ERROR_MESSAGE, null /*ikeMessage*/, e);
+                return new DecodeResultError(DECODE_STATUS_PROTECTED_ERROR, e);
             }
         }
+
+        private Pair<IkeSkPayload, Integer> decryptAndAuthenticate(
+                int expectedMsgId,
+                IkeHeader header,
+                byte[] inputPacket,
+                @Nullable IkeMacIntegrity integrityMac,
+                IkeCipher decryptCipher,
+                byte[] integrityKey,
+                byte[] decryptionKey)
+                throws IkeException {
+
+            try {
+                if (header.messageId != expectedMsgId) {
+                    throw new InvalidMessageIdException(header.messageId);
+                }
+
+                header.validateMajorVersion();
+
+                boolean isSkf = header.nextPayloadType == IkePayload.PAYLOAD_TYPE_SKF;
+                return IkePayloadFactory.getIkeSkPayload(
+                        isSkf,
+                        inputPacket,
+                        integrityMac,
+                        decryptCipher,
+                        integrityKey,
+                        decryptionKey);
+            } catch (NegativeArraySizeException | BufferUnderflowException e) {
+                throw new InvalidSyntaxException("Malformed IKE Payload", e);
+            } catch (GeneralSecurityException e) {
+                throw new IkeInternalException(e);
+            }
+        }
+
+        private void validateFragmentHeader(
+                IkeHeader fragIkeHeader, int packetLen, DecodeResultPartial collectedFragments) {
+            try {
+                fragIkeHeader.validateInboundHeader(packetLen);
+            } catch (IkeProtocolException e) {
+                getIkeLog()
+                        .e(
+                                TAG,
+                                "Received an IKE fragment with invalid header. Will be handled when"
+                                        + " reassembly is done.",
+                                e);
+            }
+
+            if (collectedFragments == null) return;
+            if (fragIkeHeader.exchangeType != collectedFragments.ikeHeader.exchangeType) {
+                getIkeLog()
+                        .e(
+                                TAG,
+                                "Received an IKE fragment with different exchange type from"
+                                        + " previously collected fragments. Ignore it.");
+            }
+        }
+
+        private DecodeResultPartial processIkeFragment(
+                IkeHeader header,
+                IkeSkfPayload skf,
+                int nextPayloadType,
+                @Nullable DecodeResultPartial collectedFragments) {
+            if (collectedFragments == null) {
+                return new DecodeResultPartial(header, skf, nextPayloadType, collectedFragments);
+            }
+
+            if (skf.totalFragments > collectedFragments.collectedFragsList.length) {
+                getIkeLog()
+                        .i(
+                                TAG,
+                                "Received IKE fragment has larger total fragments number. Discard"
+                                        + " all previously collected fragments");
+                return new DecodeResultPartial(
+                        header, skf, nextPayloadType, null /*collectedFragments*/);
+            }
+
+            if (skf.totalFragments < collectedFragments.collectedFragsList.length) {
+                getIkeLog()
+                        .i(
+                                TAG,
+                                "Received IKE fragment has smaller total fragments number. Discard"
+                                        + " it.");
+                return collectedFragments;
+            }
+
+            if (collectedFragments.collectedFragsList[skf.fragmentNum - 1] != null) {
+                getIkeLog().i(TAG, "Received IKE fragment is a replay.");
+                return collectedFragments;
+            }
+
+            return new DecodeResultPartial(header, skf, nextPayloadType, collectedFragments);
+        }
     }
 
     /** Status to describe the result of decoding an inbound IKE message. */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
         DECODE_STATUS_OK,
-        DECODE_STATUS_PROTECTED_ERROR_MESSAGE,
-        DECODE_STATUS_UNPROTECTED_ERROR_MESSAGE
+        DECODE_STATUS_PARTIAL,
+        DECODE_STATUS_PROTECTED_ERROR,
+        DECODE_STATUS_UNPROTECTED_ERROR,
     })
     public @interface DecodeStatus {}
 
-    /** Represents a message that has been successfuly (decrypted and) decoded. */
+    /**
+     * Represents a message that has been successfully (decrypted and) decoded or reassembled from
+     * IKE fragments
+     */
     public static final int DECODE_STATUS_OK = 0;
+    /** Represents that reassembly process of IKE fragments has started but has not finished */
+    public static final int DECODE_STATUS_PARTIAL = 1;
     /** Represents a crypto protected message with correct message ID but has parsing error. */
-    public static final int DECODE_STATUS_PROTECTED_ERROR_MESSAGE = 1;
+    public static final int DECODE_STATUS_PROTECTED_ERROR = 2;
     /**
      * Represents an unencrypted message with parsing error, an encrypted message with
      * authentication or decryption error, or any message with wrong message ID.
      */
-    public static final int DECODE_STATUS_UNPROTECTED_ERROR_MESSAGE = 2;
+    public static final int DECODE_STATUS_UNPROTECTED_ERROR = 3;
 
-    /** This class represents a result of decoding an IKE message. */
-    public static class DecodeResult {
-        // TODO: Extend this class to support IKE fragmentation.
-
+    /** This class represents common decoding result of an IKE message. */
+    public abstract static class DecodeResult {
         public final int status;
-        public final IkeMessage ikeMessage;
-        public final IkeException ikeException;
 
         /** Construct an instance of DecodeResult. */
-        public DecodeResult(int status, IkeMessage ikeMessage, IkeException ikeException) {
+        protected DecodeResult(int status) {
             this.status = status;
+        }
+    }
+
+    /** This class represents an IKE message has been successfully (decrypted and) decoded. */
+    public static class DecodeResultOk extends DecodeResult {
+        public final IkeMessage ikeMessage;
+
+        public DecodeResultOk(IkeMessage ikeMessage) {
+            super(DECODE_STATUS_OK);
             this.ikeMessage = ikeMessage;
+        }
+    }
+
+    /**
+     * This class represents IKE fragments are being reassembled to build a complete IKE message.
+     *
+     * <p>All IKE fragments should have the same IKE headers, except for the message length. This
+     * class only stores the IKE header of the first arrived IKE fragment to represent the IKE
+     * header of the complete IKE message. In this way we can verify all subsequent fragments'
+     * headers against it.
+     *
+     * <p>The first payload type is only stored in the first fragment, as indicated in RFC 7383. So
+     * this class only stores the next payload type field taken from the first fragment.
+     */
+    public static class DecodeResultPartial extends DecodeResult {
+        public final int firstPayloadType;
+        public final IkeHeader ikeHeader;
+        public final byte[][] collectedFragsList;
+
+        /**
+         * Construct an instance of DecodeResultPartial with collected fragments and the newly
+         * received fragment.
+         *
+         * <p>The newly received fragment has been validated against collected fragments during
+         * decoding that all fragments have the same total fragments number and the newly received
+         * fragment is not a replay.
+         */
+        public DecodeResultPartial(
+                IkeHeader ikeHeader,
+                IkeSkfPayload skfPayload,
+                int nextPayloadType,
+                @Nullable DecodeResultPartial collectedFragments) {
+            super(DECODE_STATUS_PARTIAL);
+
+            boolean isFirstFragment = 1 == skfPayload.fragmentNum;
+            if (collectedFragments == null) {
+                // First arrived IKE fragment
+                this.ikeHeader = ikeHeader;
+                this.firstPayloadType =
+                        isFirstFragment ? nextPayloadType : IkePayload.PAYLOAD_TYPE_NO_NEXT;
+                this.collectedFragsList = new byte[skfPayload.totalFragments][];
+            } else {
+                this.ikeHeader = collectedFragments.ikeHeader;
+                this.firstPayloadType =
+                        isFirstFragment ? nextPayloadType : collectedFragments.firstPayloadType;
+                this.collectedFragsList = collectedFragments.collectedFragsList;
+            }
+
+            this.collectedFragsList[skfPayload.fragmentNum - 1] = skfPayload.getUnencryptedData();
+        }
+
+        /** Return if all IKE fragments have been collected */
+        public boolean isAllFragmentsReceived() {
+            for (byte[] frag : collectedFragsList) {
+                if (frag == null) return false;
+            }
+            return true;
+        }
+
+        /** Reassemble all IKE fragments and return the unencrypted message body in byte array. */
+        public byte[] reassembleAllFrags() {
+            if (!isAllFragmentsReceived()) {
+                throw new IllegalStateException("Not all fragments have been received");
+            }
+
+            int len = 0;
+            for (byte[] frag : collectedFragsList) {
+                len += frag.length;
+            }
+
+            ByteBuffer buffer = ByteBuffer.allocate(len);
+            for (byte[] frag : collectedFragsList) {
+                buffer.put(frag);
+            }
+
+            return buffer.array();
+        }
+    }
+
+    /** This class represents that errors have been found during decrypting or decoding. */
+    public static class DecodeResultError extends DecodeResult {
+        public final IkeException ikeException;
+
+        public DecodeResultError(int status, IkeException ikeException) {
+            super(status);
             this.ikeException = ikeException;
         }
     }
diff --git a/src/java/com/android/ike/ikev2/message/IkeNotifyPayload.java b/src/java/com/android/ike/ikev2/message/IkeNotifyPayload.java
index 352932c..58dbfb7 100644
--- a/src/java/com/android/ike/ikev2/message/IkeNotifyPayload.java
+++ b/src/java/com/android/ike/ikev2/message/IkeNotifyPayload.java
@@ -116,6 +116,10 @@
      * being negotiated. Only allowed in the request/response for negotiating a Child SA.
      */
     public static final int NOTIFY_TYPE_ESP_TFC_PADDING_NOT_SUPPORTED = 16394;
+    /**
+     * Indicates that the sender supports IKE fragmentation.
+     */
+    public static final int NOTIFY_TYPE_IKEV2_FRAGMENTATION_SUPPORTED = 16430;
     // TODO: List all supported notify types.
 
     private static final int NOTIFY_HEADER_LEN = 4;
diff --git a/src/java/com/android/ike/ikev2/message/IkePayloadFactory.java b/src/java/com/android/ike/ikev2/message/IkePayloadFactory.java
index 74dd4c9..0e4290c 100644
--- a/src/java/com/android/ike/ikev2/message/IkePayloadFactory.java
+++ b/src/java/com/android/ike/ikev2/message/IkePayloadFactory.java
@@ -16,6 +16,7 @@
 
 package com.android.ike.ikev2.message;
 
+import android.annotation.Nullable;
 import android.util.Pair;
 
 import com.android.ike.ikev2.crypto.IkeCipher;
@@ -90,6 +91,35 @@
                     return new IkeUnsupportedPayload(payloadType, isCritical);
             }
         }
+
+        @Override
+        public IkeSkPayload decodeIkeSkPayload(
+                boolean isSkf,
+                boolean critical,
+                byte[] message,
+                @Nullable IkeMacIntegrity integrityMac,
+                IkeCipher decryptCipher,
+                byte[] integrityKey,
+                byte[] decryptionKey)
+                throws IkeProtocolException, GeneralSecurityException {
+            if (isSkf) {
+                return new IkeSkfPayload(
+                        critical,
+                        message,
+                        integrityMac,
+                        decryptCipher,
+                        integrityKey,
+                        decryptionKey);
+            } else {
+                return new IkeSkPayload(
+                        critical,
+                        message,
+                        integrityMac,
+                        decryptCipher,
+                        integrityKey,
+                        decryptionKey);
+            }
+        }
     }
 
     /**
@@ -134,7 +164,7 @@
      * @param integrityMac the negotiated integrity algorithm.
      * @param decryptCipher the negotiated encryption algorithm.
      * @param integrityKey the negotiated integrity algorithm key.
-     * @param decryptKey the negotiated decryption key.
+     * @param decryptionKey the negotiated decryption key.
      * @return a pair including IkePayload and next payload type.
      * @throws IkeProtocolException for decoding errors.
      * @throws GeneralSecurityException if there is any error during integrity check or decryption.
@@ -145,7 +175,7 @@
             IkeMacIntegrity integrityMac,
             IkeCipher decryptCipher,
             byte[] integrityKey,
-            byte[] decryptKey)
+            byte[] decryptionKey)
             throws IkeProtocolException, GeneralSecurityException {
         ByteBuffer input =
                 ByteBuffer.wrap(
@@ -174,26 +204,15 @@
                             + " or SK Payload is not the only payload.");
         }
 
-        IkeSkPayload payload = null;
-        if (isSkf) {
-            payload =
-                    new IkeSkfPayload(
-                            isCritical,
-                            message,
-                            integrityMac,
-                            decryptCipher,
-                            integrityKey,
-                            decryptKey);
-        } else {
-            payload =
-                    new IkeSkPayload(
-                            isCritical,
-                            message,
-                            integrityMac,
-                            decryptCipher,
-                            integrityKey,
-                            decryptKey);
-        }
+        IkeSkPayload payload =
+                sDecoderInstance.decodeIkeSkPayload(
+                        isSkf,
+                        isCritical,
+                        message,
+                        integrityMac,
+                        decryptCipher,
+                        integrityKey,
+                        decryptionKey);
 
         return new Pair(payload, nextPayloadType);
     }
@@ -209,5 +228,15 @@
         IkePayload decodeIkePayload(
                 int payloadType, boolean isCritical, boolean isResp, byte[] payloadBody)
                 throws IkeProtocolException;
+
+        IkeSkPayload decodeIkeSkPayload(
+                boolean isSkf,
+                boolean critical,
+                byte[] message,
+                @Nullable IkeMacIntegrity integrityMac,
+                IkeCipher decryptCipher,
+                byte[] integrityKey,
+                byte[] decryptionKey)
+                throws IkeProtocolException, GeneralSecurityException;
     }
 }
diff --git a/src/java/com/android/ike/ikev2/message/IkeSkPayload.java b/src/java/com/android/ike/ikev2/message/IkeSkPayload.java
index d50dede..40c5ec7 100644
--- a/src/java/com/android/ike/ikev2/message/IkeSkPayload.java
+++ b/src/java/com/android/ike/ikev2/message/IkeSkPayload.java
@@ -40,7 +40,7 @@
  */
 public class IkeSkPayload extends IkePayload {
 
-    private final IkeEncryptedPayloadBody mIkeEncryptedPayloadBody;
+    protected final IkeEncryptedPayloadBody mIkeEncryptedPayloadBody;
 
     /**
      * Construct an instance of IkeSkPayload from decrypting an incoming packet.
@@ -50,7 +50,7 @@
      * @param integrityMac the negotiated integrity algorithm.
      * @param decryptCipher the negotiated encryption algorithm.
      * @param integrityKey the negotiated integrity algorithm key.
-     * @param decryptKey the negotiated decryption key.
+     * @param decryptionKey the negotiated decryption key.
      */
     @VisibleForTesting
     IkeSkPayload(
@@ -59,7 +59,7 @@
             @Nullable IkeMacIntegrity integrityMac,
             IkeCipher decryptCipher,
             byte[] integrityKey,
-            byte[] decryptKey)
+            byte[] decryptionKey)
             throws IkeProtocolException, GeneralSecurityException {
 
         this(
@@ -70,7 +70,14 @@
                 integrityMac,
                 decryptCipher,
                 integrityKey,
-                decryptKey);
+                decryptionKey);
+    }
+
+    /** Construct an instance of IkeSkPayload for testing.*/
+    @VisibleForTesting
+    IkeSkPayload(boolean isSkf, IkeEncryptedPayloadBody encryptedPayloadBody) {
+        super(isSkf ? PAYLOAD_TYPE_SKF : PAYLOAD_TYPE_SK, false/*critical*/);
+        mIkeEncryptedPayloadBody = encryptedPayloadBody;
     }
 
     /** Construct an instance of IkeSkPayload from decrypting an incoming packet. */
@@ -82,7 +89,7 @@
             @Nullable IkeMacIntegrity integrityMac,
             IkeCipher decryptCipher,
             byte[] integrityKey,
-            byte[] decryptKey)
+            byte[] decryptionKey)
             throws IkeProtocolException, GeneralSecurityException {
         super(isSkf ? PAYLOAD_TYPE_SKF : PAYLOAD_TYPE_SK, critical);
 
@@ -95,7 +102,7 @@
                         integrityMac,
                         decryptCipher,
                         integrityKey,
-                        decryptKey);
+                        decryptionKey);
     }
 
     /**
@@ -107,7 +114,7 @@
      * @param integrityMac the negotiated integrity algorithm.
      * @param encryptCipher the negotiated encryption algorithm.
      * @param integrityKey the negotiated integrity algorithm key.
-     * @param encryptKey the negotiated encryption key.
+     * @param encryptionKey the negotiated encryption key.
      */
     @VisibleForTesting
     IkeSkPayload(
@@ -117,8 +124,31 @@
             @Nullable IkeMacIntegrity integrityMac,
             IkeCipher encryptCipher,
             byte[] integrityKey,
-            byte[] encryptKey) {
-        super(PAYLOAD_TYPE_SK, false);
+            byte[] encryptionKey) {
+
+        this(
+                false /*isSkf*/,
+                ikeHeader,
+                firstPayloadType,
+                unencryptedPayloads,
+                integrityMac,
+                encryptCipher,
+                integrityKey,
+                encryptionKey);
+    }
+
+    /** Construct an instance of IkeSkPayload for building outbound packet. */
+    @VisibleForTesting
+    protected IkeSkPayload(
+            boolean isSkf,
+            IkeHeader ikeHeader,
+            @PayloadType int firstPayloadType,
+            byte[] unencryptedPayloads,
+            @Nullable IkeMacIntegrity integrityMac,
+            IkeCipher encryptCipher,
+            byte[] integrityKey,
+            byte[] encryptionKey) {
+        super(isSkf ? PAYLOAD_TYPE_SKF : PAYLOAD_TYPE_SK, false);
 
         // TODO: Support constructing IkeEncryptedPayloadBody using AEAD.
 
@@ -130,7 +160,7 @@
                         integrityMac,
                         encryptCipher,
                         integrityKey,
-                        encryptKey);
+                        encryptionKey);
     }
 
     /**
diff --git a/src/java/com/android/ike/ikev2/message/IkeSkfPayload.java b/src/java/com/android/ike/ikev2/message/IkeSkfPayload.java
index cd7802f..709d2e1 100644
--- a/src/java/com/android/ike/ikev2/message/IkeSkfPayload.java
+++ b/src/java/com/android/ike/ikev2/message/IkeSkfPayload.java
@@ -22,6 +22,7 @@
 import com.android.ike.ikev2.crypto.IkeMacIntegrity;
 import com.android.ike.ikev2.exceptions.IkeProtocolException;
 import com.android.ike.ikev2.exceptions.InvalidSyntaxException;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.nio.ByteBuffer;
 import java.security.GeneralSecurityException;
@@ -33,7 +34,7 @@
  *     Version 2 (IKEv2) Message Fragmentation</a>
  */
 public final class IkeSkfPayload extends IkeSkPayload {
-    private static final int SKF_HEADER_LEN = 4;
+    public static final int SKF_HEADER_LEN = 4;
 
     /** Current Fragment message number, starting from 1 */
     public final int fragmentNum;
@@ -51,7 +52,7 @@
      * @param integrityMac the negotiated integrity algorithm.
      * @param decryptCipher the negotiated encryption algorithm.
      * @param integrityKey the negotiated integrity algorithm key.
-     * @param decryptKey the negotiated decryption key.
+     * @param decryptionKey the negotiated decryption key.
      */
     IkeSkfPayload(
             boolean critical,
@@ -59,7 +60,7 @@
             @Nullable IkeMacIntegrity integrityMac,
             IkeCipher decryptCipher,
             byte[] integrityKey,
-            byte[] decryptKey)
+            byte[] decryptionKey)
             throws IkeProtocolException, GeneralSecurityException {
         super(
                 true /*isSkf*/,
@@ -69,7 +70,7 @@
                 integrityMac,
                 decryptCipher,
                 integrityKey,
-                decryptKey);
+                decryptionKey);
 
         // TODO: Support constructing IkeEncryptedPayloadBody using AEAD.
 
@@ -88,9 +89,72 @@
         }
     }
 
-    // TODO: Support constructing outbound SKF Payload.
+    /**
+     * Construct an instance of IkeSkfPayload for building outbound packet.
+     *
+     * @param ikeHeader the IKE header.
+     * @param firstPayloadType the type of first payload nested in SkPayload.
+     * @param unencryptedPayloads the encoded payload list to protect.
+     * @param integrityMac the negotiated integrity algorithm.
+     * @param encryptCipher the negotiated encryption algorithm.
+     * @param integrityKey the negotiated integrity algorithm key.
+     * @param encryptionKey the negotiated encryption key.
+     */
+    IkeSkfPayload(
+            IkeHeader ikeHeader,
+            @PayloadType int firstPayloadType,
+            byte[] unencryptedPayloads,
+            @Nullable IkeMacIntegrity integrityMac,
+            IkeCipher encryptCipher,
+            byte[] integrityKey,
+            byte[] encryptionKey,
+            int fragNum,
+            int totalFrags) {
+        super(
+                true /*isSkf*/,
+                ikeHeader,
+                firstPayloadType,
+                unencryptedPayloads,
+                integrityMac,
+                encryptCipher,
+                integrityKey,
+                encryptionKey);
+        fragmentNum = fragNum;
+        totalFragments = totalFrags;
+    }
 
-    // TODO: Override encodeToByteBuffer and getPayloadLength
+    /** Construct an instance of IkeSkfPayload for testing. */
+    @VisibleForTesting
+    IkeSkfPayload(IkeEncryptedPayloadBody encryptedPayloadBody, int fragNum, int totalFrags) {
+        super(true /*isSkf*/, encryptedPayloadBody);
+        fragmentNum = fragNum;
+        totalFragments = totalFrags;
+    }
+
+    /**
+     * Encode this payload to a ByteBuffer.
+     *
+     * @param nextPayload type of payload that follows this payload.
+     * @param byteBuffer destination ByteBuffer that stores encoded payload.
+     */
+    @Override
+    protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) {
+        encodePayloadHeaderToByteBuffer(nextPayload, getPayloadLength(), byteBuffer);
+        byteBuffer
+                .putShort((short) fragmentNum)
+                .putShort((short) totalFragments)
+                .put(mIkeEncryptedPayloadBody.encode());
+    }
+
+    /**
+     * Get entire payload length.
+     *
+     * @return entire payload length.
+     */
+    @Override
+    protected int getPayloadLength() {
+        return GENERIC_HEADER_LENGTH + SKF_HEADER_LEN + mIkeEncryptedPayloadBody.getLength();
+    }
 
     /**
      * Return the payload type as a String.
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionStateMachineTest.java b/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionStateMachineTest.java
index b08def6..435c976 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionStateMachineTest.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionStateMachineTest.java
@@ -31,11 +31,13 @@
 import static com.android.ike.ikev2.exceptions.IkeProtocolException.ERROR_TYPE_UNSUPPORTED_CRITICAL_PAYLOAD;
 import static com.android.ike.ikev2.message.IkeHeader.EXCHANGE_TYPE_CREATE_CHILD_SA;
 import static com.android.ike.ikev2.message.IkeHeader.EXCHANGE_TYPE_INFORMATIONAL;
-import static com.android.ike.ikev2.message.IkeMessage.DECODE_STATUS_OK;
-import static com.android.ike.ikev2.message.IkeMessage.DECODE_STATUS_PROTECTED_ERROR_MESSAGE;
+import static com.android.ike.ikev2.message.IkeMessage.DECODE_STATUS_PROTECTED_ERROR;
+import static com.android.ike.ikev2.message.IkeNotifyPayload.NOTIFY_TYPE_IKEV2_FRAGMENTATION_SUPPORTED;
 import static com.android.ike.ikev2.message.IkeNotifyPayload.NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP;
 import static com.android.ike.ikev2.message.IkeNotifyPayload.NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP;
+import static com.android.ike.ikev2.message.IkePayload.PAYLOAD_TYPE_AUTH;
 import static com.android.ike.ikev2.message.IkePayload.PAYLOAD_TYPE_NOTIFY;
+import static com.android.ike.ikev2.message.IkePayload.PAYLOAD_TYPE_SA;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -46,6 +48,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.anyObject;
 import static org.mockito.Matchers.anyString;
@@ -88,6 +91,7 @@
 import com.android.ike.ikev2.crypto.IkeMacIntegrity;
 import com.android.ike.ikev2.crypto.IkeMacPrf;
 import com.android.ike.ikev2.exceptions.AuthenticationFailedException;
+import com.android.ike.ikev2.exceptions.IkeException;
 import com.android.ike.ikev2.exceptions.IkeInternalException;
 import com.android.ike.ikev2.exceptions.IkeProtocolException;
 import com.android.ike.ikev2.exceptions.InvalidSyntaxException;
@@ -104,6 +108,9 @@
 import com.android.ike.ikev2.message.IkeKePayload;
 import com.android.ike.ikev2.message.IkeMessage;
 import com.android.ike.ikev2.message.IkeMessage.DecodeResult;
+import com.android.ike.ikev2.message.IkeMessage.DecodeResultError;
+import com.android.ike.ikev2.message.IkeMessage.DecodeResultOk;
+import com.android.ike.ikev2.message.IkeMessage.DecodeResultPartial;
 import com.android.ike.ikev2.message.IkeMessage.IIkeMessageHelper;
 import com.android.ike.ikev2.message.IkeMessage.IkeMessageHelper;
 import com.android.ike.ikev2.message.IkeNoncePayload;
@@ -115,6 +122,7 @@
 import com.android.ike.ikev2.message.IkeSaPayload.EsnTransform;
 import com.android.ike.ikev2.message.IkeSaPayload.IntegrityTransform;
 import com.android.ike.ikev2.message.IkeSaPayload.PrfTransform;
+import com.android.ike.ikev2.message.IkeSkfPayload;
 import com.android.ike.ikev2.message.IkeTestUtils;
 import com.android.ike.ikev2.message.IkeTsPayload;
 import com.android.ike.utils.Log;
@@ -185,6 +193,7 @@
             "2900001c00004004e54f73b7d83f6beb881eab2051d8663f421d10b0";
     private static final String NAT_DETECTION_DESTINATION_PAYLOAD_HEX_STRING =
             "2b00001c00004005d915368ca036004cb578ae3e3fb268509aeab190";
+    private static final String FRAGMENTATION_SUPPORTED_PAYLOAD_HEX_STRING = "290000080000402e";
     private static final String DELETE_IKE_PAYLOAD_HEX_STRING = "0000000801000000";
     private static final String NOTIFY_REKEY_IKE_PAYLOAD_HEX_STRING = "2100000800004009";
     private static final String ID_PAYLOAD_INITIATOR_HEX_STRING =
@@ -328,7 +337,7 @@
 
         byte[] dummyIkePacketBytes = new byte[0];
         when(mMockIkeMessageHelper.decode(0, dummyIkeMessage.ikeHeader, dummyIkePacketBytes))
-                .thenReturn(new DecodeResult(DECODE_STATUS_OK, dummyIkeMessage, null));
+                .thenReturn(new DecodeResultOk(dummyIkeMessage));
 
         return new ReceivedIkePacket(dummyIkeMessage.ikeHeader, dummyIkePacketBytes);
     }
@@ -383,14 +392,11 @@
                         true /*isEncyprted*/,
                         payloadList);
 
-        when(mMockIkeMessageHelper.decode(
-                        anyInt(),
-                        any(),
-                        any(),
-                        eq(ikeSaRecord),
-                        eq(dummyIkeMessage.ikeHeader),
-                        eq(dummyIkePacketBytes)))
-                .thenReturn(new DecodeResult(DECODE_STATUS_OK, dummyIkeMessage, null));
+        setDecodeEncryptedPacketResult(
+                ikeSaRecord,
+                dummyIkeMessage.ikeHeader,
+                null /*collectedFrags*/,
+                new DecodeResultOk(dummyIkeMessage));
 
         return new ReceivedIkePacket(dummyIkeMessage.ikeHeader, dummyIkePacketBytes);
     }
@@ -404,24 +410,95 @@
     private ReceivedIkePacket makeDummyReceivedIkePacketWithDecodingError(
             IkeSaRecord ikeSaRecord, boolean isResp, int eType, IkeProtocolException exception) {
         IkeHeader header =
-                new IkeHeader(
-                        ikeSaRecord.getInitiatorSpi(),
-                        ikeSaRecord.getResponderSpi(),
-                        IkePayload.PAYLOAD_TYPE_SK,
-                        eType,
-                        isResp,
-                        !ikeSaRecord.isLocalInit,
-                        isResp
-                                ? ikeSaRecord.getLocalRequestMessageId()
-                                : ikeSaRecord.getRemoteRequestMessageId());
+                makeDummyIkeHeader(ikeSaRecord, isResp, eType, IkePayload.PAYLOAD_TYPE_SK);
         when(mMockIkeMessageHelper.decode(
-                        anyInt(), any(), any(), eq(ikeSaRecord), eq(header), any()))
-                .thenReturn(
-                        new DecodeResult(DECODE_STATUS_PROTECTED_ERROR_MESSAGE, null, exception));
+                        anyInt(), any(), any(), eq(ikeSaRecord), eq(header), any(), any()))
+                .thenReturn(new DecodeResultError(DECODE_STATUS_PROTECTED_ERROR, exception));
 
         return new ReceivedIkePacket(header, new byte[0]);
     }
 
+    private ReceivedIkePacket makeDummyReceivedIkeFragmentPacket(
+            IkeSaRecord ikeSaRecord,
+            boolean isResp,
+            int eType,
+            IkeSkfPayload skfPayload,
+            int nextPayloadType,
+            DecodeResultPartial collectedFrags) {
+        IkeHeader header =
+                makeDummyIkeHeader(ikeSaRecord, isResp, eType, IkePayload.PAYLOAD_TYPE_SKF);
+        DecodeResultPartial resultFrags =
+                new DecodeResultPartial(header, skfPayload, nextPayloadType, collectedFrags);
+        setDecodeEncryptedPacketResult(ikeSaRecord, header, collectedFrags, resultFrags);
+
+        return new ReceivedIkePacket(header, new byte[0]);
+    }
+
+    private ReceivedIkePacket makeDummyReceivedLastIkeFragmentPacketOk(
+            IkeSaRecord ikeSaRecord,
+            boolean isResp,
+            int eType,
+            DecodeResultPartial collectedFrags,
+            List<IkePayload> payloadList) {
+        IkeHeader header =
+                makeDummyIkeHeader(ikeSaRecord, isResp, eType, IkePayload.PAYLOAD_TYPE_SKF);
+
+        IkeMessage completeMessage = new IkeMessage(header, payloadList);
+
+        setDecodeEncryptedPacketResult(
+                ikeSaRecord, header, collectedFrags, new DecodeResultOk(completeMessage));
+
+        return new ReceivedIkePacket(header, new byte[0] /*dummyIkePacketBytes*/);
+    }
+
+    private ReceivedIkePacket makeDummyReceivedLastIkeFragmentPacketError(
+            IkeSaRecord ikeSaRecord,
+            boolean isResp,
+            int eType,
+            DecodeResultPartial collectedFrags,
+            IkeException exception) {
+        IkeHeader header =
+                makeDummyIkeHeader(ikeSaRecord, isResp, eType, IkePayload.PAYLOAD_TYPE_SKF);
+
+        setDecodeEncryptedPacketResult(
+                ikeSaRecord,
+                header,
+                collectedFrags,
+                new DecodeResultError(DECODE_STATUS_PROTECTED_ERROR, exception));
+
+        return new ReceivedIkePacket(header, new byte[0] /*dummyIkePacketBytes*/);
+    }
+
+    private IkeHeader makeDummyIkeHeader(
+            IkeSaRecord ikeSaRecord, boolean isResp, int eType, int firstPayloadType) {
+        return new IkeHeader(
+                ikeSaRecord.getInitiatorSpi(),
+                ikeSaRecord.getResponderSpi(),
+                firstPayloadType,
+                eType,
+                isResp,
+                !ikeSaRecord.isLocalInit,
+                isResp
+                        ? ikeSaRecord.getLocalRequestMessageId()
+                        : ikeSaRecord.getRemoteRequestMessageId());
+    }
+
+    private void setDecodeEncryptedPacketResult(
+            IkeSaRecord ikeSaRecord,
+            IkeHeader header,
+            DecodeResultPartial collectedFrags,
+            DecodeResult result) {
+        when(mMockIkeMessageHelper.decode(
+                        anyInt(),
+                        any(),
+                        any(),
+                        eq(ikeSaRecord),
+                        eq(header),
+                        any(),
+                        eq(collectedFrags)))
+                .thenReturn(result);
+    }
+
     private IkeMessage makeDummyIkeMessageForTest(
             long initSpi,
             long respSpi,
@@ -463,7 +540,8 @@
                         any(),
                         eq(record),
                         eq(rcvPacket.ikeHeader),
-                        eq(rcvPacket.ikePacketBytes));
+                        eq(rcvPacket.ikePacketBytes),
+                        eq(null));
     }
 
     private static IkeSaRecord makeDummyIkeSaRecord(long initSpi, long respSpi, boolean isLocalInit)
@@ -539,10 +617,8 @@
         mIkeSessionStateMachine = makeAndStartIkeSession(buildIkeSessionOptionsPsk(mPsk));
 
         mMockIkeMessageHelper = mock(IkeMessage.IIkeMessageHelper.class);
-        when(mMockIkeMessageHelper.encode(any())).thenReturn(new byte[0]);
-        when(mMockIkeMessageHelper.encryptAndEncode(any(), any(), any(), any()))
-                .thenReturn(new byte[0]);
         IkeMessage.setIkeMessageHelper(mMockIkeMessageHelper);
+        resetMockIkeMessageHelper();
 
         mMockSaRecordHelper = mock(SaRecord.ISaRecordHelper.class);
         SaRecord.setSaRecordHelper(mMockSaRecordHelper);
@@ -646,12 +722,14 @@
         payloadTypeList.add(IkePayload.PAYLOAD_TYPE_NONCE);
         payloadTypeList.add(IkePayload.PAYLOAD_TYPE_NOTIFY);
         payloadTypeList.add(IkePayload.PAYLOAD_TYPE_NOTIFY);
+        payloadTypeList.add(IkePayload.PAYLOAD_TYPE_NOTIFY);
 
         payloadHexStringList.add(IKE_SA_PAYLOAD_HEX_STRING);
         payloadHexStringList.add(KE_PAYLOAD_HEX_STRING);
         payloadHexStringList.add(NONCE_RESP_PAYLOAD_HEX_STRING);
         payloadHexStringList.add(NAT_DETECTION_SOURCE_PAYLOAD_HEX_STRING);
         payloadHexStringList.add(NAT_DETECTION_DESTINATION_PAYLOAD_HEX_STRING);
+        payloadHexStringList.add(FRAGMENTATION_SUPPORTED_PAYLOAD_HEX_STRING);
 
         // In each test assign different IKE responder SPI in IKE INIT response to avoid remote SPI
         // collision during response validation.
@@ -667,8 +745,8 @@
                 payloadHexStringList);
     }
 
-    private ReceivedIkePacket makeIkeAuthRespWithChildPayloads(List<IkePayload> authRelatedPayloads)
-            throws Exception {
+    private List<IkePayload> getIkeAuthPayloadListWithChildPayloads(
+            List<IkePayload> authRelatedPayloads) throws Exception {
         List<Integer> payloadTypeList = new LinkedList<>();
         List<String> payloadHexStringList = new LinkedList<>();
 
@@ -684,6 +762,13 @@
                 hexStrListToIkePayloadList(payloadTypeList, payloadHexStringList, true /*isResp*/);
         payloadList.addAll(authRelatedPayloads);
 
+        return payloadList;
+    }
+
+    private ReceivedIkePacket makeIkeAuthRespWithChildPayloads(List<IkePayload> authRelatedPayloads)
+            throws Exception {
+        List<IkePayload> payloadList = getIkeAuthPayloadListWithChildPayloads(authRelatedPayloads);
+
         return makeDummyEncryptedReceivedIkePacketWithPayloadList(
                 mSpyCurrentIkeSaRecord,
                 IkeHeader.EXCHANGE_TYPE_IKE_AUTH,
@@ -911,6 +996,48 @@
                         .hasMessages(IkeSessionStateMachine.CMD_RETRANSMIT));
     }
 
+    private IkeMessage verifyEncryptAndEncodeAndGetMessage(IkeSaRecord ikeSaRecord) {
+        verify(mMockIkeMessageHelper)
+                .encryptAndEncode(
+                        anyObject(),
+                        anyObject(),
+                        eq(ikeSaRecord),
+                        mIkeMessageCaptor.capture(),
+                        anyBoolean(),
+                        anyInt());
+        return mIkeMessageCaptor.getValue();
+    }
+
+    private void verifyEncryptAndEncodeNeverCalled(IkeSaRecord ikeSaRecord) {
+        verify(mMockIkeMessageHelper, never())
+                .encryptAndEncode(
+                        anyObject(),
+                        anyObject(),
+                        eq(ikeSaRecord),
+                        any(IkeMessage.class),
+                        anyBoolean(),
+                        anyInt());
+    }
+
+    private void verifyEncryptAndEncodeNeverCalled() {
+        verify(mMockIkeMessageHelper, never())
+                .encryptAndEncode(
+                        anyObject(),
+                        anyObject(),
+                        any(IkeSaRecord.class),
+                        any(IkeMessage.class),
+                        anyBoolean(),
+                        anyInt());
+    }
+
+    private void resetMockIkeMessageHelper() {
+        reset(mMockIkeMessageHelper);
+        when(mMockIkeMessageHelper.encode(any())).thenReturn(new byte[0]);
+        when(mMockIkeMessageHelper.encryptAndEncode(
+                        any(), any(), any(), any(), anyBoolean(), anyInt()))
+                .thenReturn(new byte[1][0]);
+    }
+
     @Test
     public void testQuit() {
         mIkeSessionStateMachine.quit();
@@ -1018,6 +1145,7 @@
         assertTrue(isIkePayloadExist(payloadList, IkePayload.PAYLOAD_TYPE_NONCE));
         assertTrue(isNotifyExist(payloadList, NOTIFY_TYPE_NAT_DETECTION_SOURCE_IP));
         assertTrue(isNotifyExist(payloadList, NOTIFY_TYPE_NAT_DETECTION_DESTINATION_IP));
+        assertTrue(isNotifyExist(payloadList, NOTIFY_TYPE_IKEV2_FRAGMENTATION_SUPPORTED));
 
         verify(mSpyIkeSocket)
                 .registerIke(eq(mSpyCurrentIkeSaRecord.getLocalSpi()), eq(mIkeSessionStateMachine));
@@ -1058,6 +1186,9 @@
         // Validate NAT detection
         assertTrue(mIkeSessionStateMachine.mIsLocalBehindNat);
         assertFalse(mIkeSessionStateMachine.mIsRemoteBehindNat);
+
+        // Validate fragmentation support negotiation
+        assertTrue(mIkeSessionStateMachine.mSupportFragment);
     }
 
     private void setIkeInitResults() throws Exception {
@@ -1069,6 +1200,7 @@
         mIkeSessionStateMachine.mLocalAddress = LOCAL_ADDRESS;
         mIkeSessionStateMachine.mIsLocalBehindNat = true;
         mIkeSessionStateMachine.mIsRemoteBehindNat = false;
+        mIkeSessionStateMachine.mSupportFragment = true;
         mIkeSessionStateMachine.addIkeSaRecord(mSpyCurrentIkeSaRecord);
     }
 
@@ -1183,13 +1315,7 @@
         mLooper.dispatchAll();
         verifyRetransmissionStarted();
 
-        verify(mMockIkeMessageHelper)
-                .encryptAndEncode(
-                        anyObject(),
-                        anyObject(),
-                        eq(mSpyCurrentIkeSaRecord),
-                        mIkeMessageCaptor.capture());
-        IkeMessage createChildRequest = mIkeMessageCaptor.getValue();
+        IkeMessage createChildRequest = verifyEncryptAndEncodeAndGetMessage(mSpyCurrentIkeSaRecord);
 
         IkeHeader ikeHeader = createChildRequest.ikeHeader;
         assertEquals(IkeHeader.EXCHANGE_TYPE_CREATE_CHILD_SA, ikeHeader.exchangeType);
@@ -1360,13 +1486,7 @@
     }
 
     private List<IkePayload> verifyOutInfoMsgHeaderAndGetPayloads(boolean isResp) {
-        verify(mMockIkeMessageHelper)
-                .encryptAndEncode(
-                        anyObject(),
-                        anyObject(),
-                        eq(mSpyCurrentIkeSaRecord),
-                        mIkeMessageCaptor.capture());
-        IkeMessage deleteChildMessage = mIkeMessageCaptor.getValue();
+        IkeMessage deleteChildMessage = verifyEncryptAndEncodeAndGetMessage(mSpyCurrentIkeSaRecord);
 
         IkeHeader ikeHeader = deleteChildMessage.ikeHeader;
         assertEquals(mSpyCurrentIkeSaRecord.getInitiatorSpi(), ikeHeader.ikeInitiatorSpi);
@@ -1464,12 +1584,7 @@
         mLooper.dispatchAll();
 
         // Verify that no response is sent
-        verify(mMockIkeMessageHelper, never())
-                .encryptAndEncode(
-                        anyObject(),
-                        anyObject(),
-                        eq(mSpyCurrentIkeSaRecord),
-                        any(IkeMessage.class));
+        verifyEncryptAndEncodeNeverCalled(mSpyCurrentIkeSaRecord);
 
         // childTwo outbound payload list ready
         IkeDeletePayload outDelPayloadTwo = new IkeDeletePayload(new int[] {childTwoLocalSpi});
@@ -1703,13 +1818,7 @@
     }
 
     private IkeMessage verifyAuthReqAndGetMsg() {
-        verify(mMockIkeMessageHelper)
-                .encryptAndEncode(
-                        anyObject(),
-                        anyObject(),
-                        eq(mSpyCurrentIkeSaRecord),
-                        mIkeMessageCaptor.capture());
-        IkeMessage ikeAuthReqMessage = mIkeMessageCaptor.getValue();
+        IkeMessage ikeAuthReqMessage = verifyEncryptAndEncodeAndGetMessage(mSpyCurrentIkeSaRecord);
 
         IkeHeader ikeHeader = ikeAuthReqMessage.ikeHeader;
         assertEquals(IkeHeader.EXCHANGE_TYPE_IKE_AUTH, ikeHeader.exchangeType);
@@ -1748,9 +1857,9 @@
             boolean hasChildPayloads)
             throws Exception {
         // Send IKE AUTH response to IKE state machine
+        ReceivedIkePacket authResp = makeIkeAuthRespWithChildPayloads(authRelatedPayloads);
         mIkeSessionStateMachine.sendMessage(
-                IkeSessionStateMachine.CMD_RECEIVE_IKE_PACKET,
-                makeIkeAuthRespWithChildPayloads(authRelatedPayloads));
+                IkeSessionStateMachine.CMD_RECEIVE_IKE_PACKET, authResp);
         mLooper.dispatchAll();
 
         // Validate outbound IKE AUTH request
@@ -1766,7 +1875,7 @@
 
         // Validate inbound IKE AUTH response
         verifyIncrementLocaReqMsgId();
-        verify(mMockIkeMessageHelper).decode(anyInt(), any(), any(), any(), any(), any());
+        verifyDecodeEncryptedMessage(mSpyCurrentIkeSaRecord, authResp);
 
         // Validate authentication is done. Cannot use matchers because IkeAuthPskPayload is final.
         verify(spyAuthPayload)
@@ -1838,13 +1947,7 @@
                 mIkeSessionStateMachine.getCurrentState() instanceof IkeSessionStateMachine.Idle);
     }
 
-    @Test
-    public void testCreateIkeLocalIkeAuthPsk() throws Exception {
-        mockIkeInitAndTransitionToIkeAuth(mIkeSessionStateMachine.mCreateIkeLocalIkeAuth);
-        verifyRetransmissionStarted();
-
-        // Build IKE AUTH response with Auth-PSK Payload and ID-Responder Payload.
-        List<IkePayload> authRelatedPayloads = new LinkedList<>();
+    private IkeAuthPskPayload makeSpyRespPskPayload() throws Exception {
         IkeAuthPskPayload spyAuthPayload =
                 spy(
                         (IkeAuthPskPayload)
@@ -1856,14 +1959,28 @@
         doNothing()
                 .when(spyAuthPayload)
                 .verifyInboundSignature(any(), any(), any(), any(), any(), any());
+        return spyAuthPayload;
+    }
+
+    private IkeIdPayload makeRespIdPayload() throws Exception {
+        return (IkeIdPayload)
+                IkeTestUtils.hexStringToIkePayload(
+                        IkePayload.PAYLOAD_TYPE_ID_RESPONDER,
+                        true /*isResp*/,
+                        ID_PAYLOAD_RESPONDER_HEX_STRING);
+    }
+
+    @Test
+    public void testCreateIkeLocalIkeAuthPsk() throws Exception {
+        mockIkeInitAndTransitionToIkeAuth(mIkeSessionStateMachine.mCreateIkeLocalIkeAuth);
+        verifyRetransmissionStarted();
+
+        // Build IKE AUTH response with Auth-PSK Payload and ID-Responder Payload.
+        List<IkePayload> authRelatedPayloads = new LinkedList<>();
+        IkeAuthPskPayload spyAuthPayload = makeSpyRespPskPayload();
         authRelatedPayloads.add(spyAuthPayload);
 
-        IkeIdPayload respIdPayload =
-                (IkeIdPayload)
-                        IkeTestUtils.hexStringToIkePayload(
-                                IkePayload.PAYLOAD_TYPE_ID_RESPONDER,
-                                true /*isResp*/,
-                                ID_PAYLOAD_RESPONDER_HEX_STRING);
+        IkeIdPayload respIdPayload = makeRespIdPayload();
         authRelatedPayloads.add(respIdPayload);
 
         verifySharedKeyAuthentication(spyAuthPayload, respIdPayload, authRelatedPayloads, true);
@@ -1874,29 +1991,17 @@
     public void testCreateIkeLocalIkeAuthPskVerifyFail() throws Exception {
         mockIkeInitAndTransitionToIkeAuth(mIkeSessionStateMachine.mCreateIkeLocalIkeAuth);
         verifyRetransmissionStarted();
-        reset(mMockIkeMessageHelper);
+        resetMockIkeMessageHelper();
 
         // Build IKE AUTH response with invalid Auth-PSK Payload and ID-Responder Payload.
         List<IkePayload> authRelatedPayloads = new LinkedList<>();
-        IkeAuthPskPayload spyAuthPayload =
-                spy(
-                        (IkeAuthPskPayload)
-                                IkeTestUtils.hexStringToIkePayload(
-                                        IkePayload.PAYLOAD_TYPE_AUTH,
-                                        true /*isResp*/,
-                                        PSK_AUTH_RESP_PAYLOAD_HEX_STRING));
-
+        IkeAuthPskPayload spyAuthPayload = makeSpyRespPskPayload();
         doThrow(new AuthenticationFailedException("DummyAuthFailException"))
                 .when(spyAuthPayload)
                 .verifyInboundSignature(any(), any(), any(), any(), any(), any());
         authRelatedPayloads.add(spyAuthPayload);
 
-        IkeIdPayload respIdPayload =
-                (IkeIdPayload)
-                        IkeTestUtils.hexStringToIkePayload(
-                                IkePayload.PAYLOAD_TYPE_ID_RESPONDER,
-                                true /*isResp*/,
-                                ID_PAYLOAD_RESPONDER_HEX_STRING);
+        IkeIdPayload respIdPayload = makeRespIdPayload();
         authRelatedPayloads.add(respIdPayload);
 
         // Send response to IKE state machine
@@ -1919,7 +2024,7 @@
     public void testAuthPskHandleRespWithParsingError() throws Exception {
         mockIkeInitAndTransitionToIkeAuth(mIkeSessionStateMachine.mCreateIkeLocalIkeAuth);
         verifyRetransmissionStarted();
-        reset(mMockIkeMessageHelper);
+        resetMockIkeMessageHelper();
 
         // Mock receiving packet with syntax error
         ReceivedIkePacket mockInvalidPacket =
@@ -2038,15 +2143,8 @@
         mLooper.dispatchAll();
         verifyRetransmissionStarted();
 
-        verify(mMockIkeMessageHelper)
-                .encryptAndEncode(
-                        anyObject(),
-                        anyObject(),
-                        eq(mSpyCurrentIkeSaRecord),
-                        mIkeMessageCaptor.capture());
-
         // Verify EAP response
-        IkeMessage resp = mIkeMessageCaptor.getValue();
+        IkeMessage resp = verifyEncryptAndEncodeAndGetMessage(mSpyCurrentIkeSaRecord);
         IkeHeader ikeHeader = resp.ikeHeader;
         assertEquals(IkePayload.PAYLOAD_TYPE_SK, ikeHeader.nextPayloadType);
         assertEquals(IkeHeader.EXCHANGE_TYPE_IKE_AUTH, ikeHeader.exchangeType);
@@ -2191,31 +2289,131 @@
 
         // Build IKE AUTH response with Auth-PSK Payload and ID-Responder Payload.
         List<IkePayload> authRelatedPayloads = new LinkedList<>();
-        IkeAuthPskPayload spyAuthPayload =
-                spy(
-                        (IkeAuthPskPayload)
-                                IkeTestUtils.hexStringToIkePayload(
-                                        IkePayload.PAYLOAD_TYPE_AUTH,
-                                        true /*isResp*/,
-                                        PSK_AUTH_RESP_PAYLOAD_HEX_STRING));
-
-        doNothing()
-                .when(spyAuthPayload)
-                .verifyInboundSignature(any(), any(), any(), any(), any(), any());
+        IkeAuthPskPayload spyAuthPayload = makeSpyRespPskPayload();
         authRelatedPayloads.add(spyAuthPayload);
 
-        IkeIdPayload respIdPayload =
-                (IkeIdPayload)
-                        IkeTestUtils.hexStringToIkePayload(
-                                IkePayload.PAYLOAD_TYPE_ID_RESPONDER,
-                                true /*isResp*/,
-                                ID_PAYLOAD_RESPONDER_HEX_STRING);
+        IkeIdPayload respIdPayload = makeRespIdPayload();
 
         verifySharedKeyAuthentication(spyAuthPayload, respIdPayload, authRelatedPayloads, false);
         verifyRetransmissionStopped();
     }
 
     @Test
+    public void testCreateIkeLocalIkeAuthHandlesFirstFrag() throws Exception {
+        mockIkeInitAndTransitionToIkeAuth(mIkeSessionStateMachine.mCreateIkeLocalIkeAuth);
+        verifyRetransmissionStarted();
+
+        // Received IKE fragment
+        byte[] unencryptedData = "testCreateIkeLocalIkeAuthHandlesFrag".getBytes();
+        int fragNum = 1;
+        int totalFragments = 2;
+        IkeSkfPayload skfPayload =
+                IkeTestUtils.makeDummySkfPayload(unencryptedData, fragNum, totalFragments);
+
+        ReceivedIkePacket packet =
+                makeDummyReceivedIkeFragmentPacket(
+                        mSpyCurrentIkeSaRecord,
+                        true /*isResp*/,
+                        IkeHeader.EXCHANGE_TYPE_IKE_AUTH,
+                        skfPayload,
+                        PAYLOAD_TYPE_AUTH,
+                        null /* collectedFrags*/);
+        mIkeSessionStateMachine.sendMessage(IkeSessionStateMachine.CMD_RECEIVE_IKE_PACKET, packet);
+        mLooper.dispatchAll();
+
+        // Verify state doesn't change
+        assertTrue(
+                mIkeSessionStateMachine.getCurrentState()
+                        instanceof IkeSessionStateMachine.CreateIkeLocalIkeAuth);
+
+        // Verify the IkeSaRecord has stored the fragment.
+        DecodeResultPartial resultPartial =
+                mSpyCurrentIkeSaRecord.getCollectedFragments(true /*isResp*/);
+        assertEquals(PAYLOAD_TYPE_AUTH, resultPartial.firstPayloadType);
+        assertEquals(totalFragments, resultPartial.collectedFragsList.length);
+        assertArrayEquals(unencryptedData, resultPartial.collectedFragsList[fragNum - 1]);
+        assertFalse(resultPartial.isAllFragmentsReceived());
+
+        assertNull(mSpyCurrentIkeSaRecord.getCollectedFragments(false /*isResp*/));
+    }
+
+    @Test
+    public void testCreateIkeLocalIkeAuthHandlesLastFragOk() throws Exception {
+        mockIkeInitAndTransitionToIkeAuth(mIkeSessionStateMachine.mCreateIkeLocalIkeAuth);
+        verifyRetransmissionStarted();
+
+        // Set previously collected IKE fragments
+        DecodeResultPartial mockCollectedFrags = mock(DecodeResultPartial.class);
+        mSpyCurrentIkeSaRecord.updateCollectedFragments(mockCollectedFrags, true /*isResp*/);
+
+        // Build reassembled IKE AUTH response with Auth-PSK Payload and ID-Responder Payload.
+        List<IkePayload> authRelatedPayloads = new LinkedList<>();
+        IkeAuthPskPayload spyAuthPayload = makeSpyRespPskPayload();
+        authRelatedPayloads.add(spyAuthPayload);
+
+        IkeIdPayload respIdPayload = makeRespIdPayload();
+        authRelatedPayloads.add(respIdPayload);
+
+        List<IkePayload> authPayloadList =
+                getIkeAuthPayloadListWithChildPayloads(authRelatedPayloads);
+
+        // Receive last auth response and do IKE_AUTH
+        ReceivedIkePacket packet =
+                makeDummyReceivedLastIkeFragmentPacketOk(
+                        mSpyCurrentIkeSaRecord,
+                        true /*isResp*/,
+                        IkeHeader.EXCHANGE_TYPE_IKE_AUTH,
+                        mockCollectedFrags,
+                        authPayloadList);
+        mIkeSessionStateMachine.sendMessage(IkeSessionStateMachine.CMD_RECEIVE_IKE_PACKET, packet);
+        mLooper.dispatchAll();
+
+        // Verify IKE AUTH is done
+        assertTrue(
+                mIkeSessionStateMachine.getCurrentState()
+                        instanceof IkeSessionStateMachine.ChildProcedureOngoing);
+
+        // Verify collected response fragments are cleared.
+        assertNull(mSpyCurrentIkeSaRecord.getCollectedFragments(true /*isResp*/));
+        verify(mSpyCurrentIkeSaRecord).resetCollectedFragments(true /*isResp*/);
+    }
+
+    @Test
+    public void testCreateIkeLocalIkeAuthHandlesLastFragError() throws Exception {
+        mockIkeInitAndTransitionToIkeAuth(mIkeSessionStateMachine.mCreateIkeLocalIkeAuth);
+        verifyRetransmissionStarted();
+        resetMockIkeMessageHelper();
+
+        // Set previously collected IKE fragments
+        DecodeResultPartial mockCollectedFrags = mock(DecodeResultPartial.class);
+        mSpyCurrentIkeSaRecord.updateCollectedFragments(mockCollectedFrags, true /*isResp*/);
+
+        // Receive last auth response with syntax error
+        ReceivedIkePacket packet =
+                makeDummyReceivedLastIkeFragmentPacketError(
+                        mSpyCurrentIkeSaRecord,
+                        true /*isResp*/,
+                        IkeHeader.EXCHANGE_TYPE_IKE_AUTH,
+                        mockCollectedFrags,
+                        new InvalidSyntaxException("IkeStateMachineTest"));
+        mIkeSessionStateMachine.sendMessage(IkeSessionStateMachine.CMD_RECEIVE_IKE_PACKET, packet);
+        mLooper.dispatchAll();
+
+        // Verify Delete request is sent
+        List<IkePayload> payloads = verifyOutInfoMsgHeaderAndGetPayloads(false /*isResp*/);
+        assertEquals(1, payloads.size());
+        assertEquals(IkePayload.PAYLOAD_TYPE_DELETE, payloads.get(0).payloadType);
+
+        // Verify IKE Session is closed properly
+        assertNull(mIkeSessionStateMachine.getCurrentState());
+        verify(mMockIkeSessionCallback).onError(any(InvalidSyntaxException.class));
+
+        // Collected response fragments are cleared
+        assertNull(mSpyCurrentIkeSaRecord.getCollectedFragments(true /*isResp*/));
+        verify(mSpyCurrentIkeSaRecord).resetCollectedFragments(true /*isResp*/);
+    }
+
+    @Test
     public void testRekeyIkeLocalCreateSendsRequest() throws Exception {
         setupIdleStateMachine();
 
@@ -2227,16 +2425,10 @@
         assertTrue(
                 mIkeSessionStateMachine.getCurrentState()
                         instanceof IkeSessionStateMachine.RekeyIkeLocalCreate);
-        verify(mMockIkeMessageHelper)
-                .encryptAndEncode(
-                        anyObject(),
-                        anyObject(),
-                        eq(mSpyCurrentIkeSaRecord),
-                        mIkeMessageCaptor.capture());
         verifyRetransmissionStarted();
 
         // Verify outbound message
-        IkeMessage rekeyMsg = mIkeMessageCaptor.getValue();
+        IkeMessage rekeyMsg = verifyEncryptAndEncodeAndGetMessage(mSpyCurrentIkeSaRecord);
 
         IkeHeader ikeHeader = rekeyMsg.ikeHeader;
         assertEquals(IkePayload.PAYLOAD_TYPE_SK, ikeHeader.nextPayloadType);
@@ -2311,7 +2503,7 @@
                 new LocalRequest(IkeSessionStateMachine.CMD_LOCAL_REQUEST_REKEY_IKE));
         mLooper.dispatchAll();
         verifyRetransmissionStarted();
-        reset(mMockIkeMessageHelper);
+        resetMockIkeMessageHelper();
 
         // Mock receiving packet with syntax error
         ReceivedIkePacket mockInvalidPacket =
@@ -2370,7 +2562,7 @@
                 IkeSessionStateMachine.CMD_EXECUTE_LOCAL_REQ,
                 new LocalRequest(IkeSessionStateMachine.CMD_LOCAL_REQUEST_REKEY_IKE));
         mLooper.dispatchAll();
-        reset(mMockIkeMessageHelper);
+        resetMockIkeMessageHelper();
 
         // Mock receiving packet with NO_PROPOSAL_CHOSEN
         ReceivedIkePacket resp =
@@ -2379,9 +2571,7 @@
         mLooper.dispatchAll();
 
         // Verify no message was sent because a fatal error notification was received
-        verify(mMockIkeMessageHelper, never())
-                .encryptAndEncode(
-                        anyObject(), anyObject(), eq(mSpyCurrentIkeSaRecord), anyObject());
+        verifyEncryptAndEncodeNeverCalled(mSpyCurrentIkeSaRecord);
 
         // Verify IKE Session is closed properly
         assertNull(mIkeSessionStateMachine.getCurrentState());
@@ -2401,7 +2591,7 @@
                 IkeSessionStateMachine.CMD_EXECUTE_LOCAL_REQ,
                 new LocalRequest(IkeSessionStateMachine.CMD_LOCAL_REQUEST_REKEY_IKE));
         mLooper.dispatchAll();
-        reset(mMockIkeMessageHelper);
+        resetMockIkeMessageHelper();
 
         // Receive Rekey response
         ReceivedIkePacket dummyRekeyIkeRespReceivedPacket = makeRekeyIkeResponse();
@@ -2429,7 +2619,7 @@
                 new LocalRequest(IkeSessionStateMachine.CMD_LOCAL_REQUEST_REKEY_IKE));
         mLooper.dispatchAll();
         verifyRetransmissionStarted();
-        reset(mMockIkeMessageHelper);
+        resetMockIkeMessageHelper();
 
         // Build protocol exception
         List<Integer> unsupportedPayloads = new LinkedList<>();
@@ -2462,16 +2652,21 @@
                         instanceof IkeSessionStateMachine.RekeyIkeLocalCreate);
     }
 
-    @Test
-    public void testRekeyIkeLocalDeleteSendsRequest() throws Exception {
-        setupIdleStateMachine();
-
+    private void mockCreateAndTransitionToRekeyDeleteLocal() {
         // Seed fake rekey data and force transition to RekeyIkeLocalDelete
         mIkeSessionStateMachine.mLocalInitNewIkeSaRecord = mSpyLocalInitIkeSaRecord;
+        mIkeSessionStateMachine.addIkeSaRecord(mSpyLocalInitIkeSaRecord);
         mIkeSessionStateMachine.sendMessage(
                 IkeSessionStateMachine.CMD_FORCE_TRANSITION,
                 mIkeSessionStateMachine.mRekeyIkeLocalDelete);
         mLooper.dispatchAll();
+        verifyRetransmissionStarted();
+    }
+
+    @Test
+    public void testRekeyIkeLocalDeleteSendsRequest() throws Exception {
+        setupIdleStateMachine();
+        mockCreateAndTransitionToRekeyDeleteLocal();
 
         // Verify Rekey-Delete request
         assertTrue(
@@ -2479,15 +2674,8 @@
                         instanceof IkeSessionStateMachine.RekeyIkeLocalDelete);
         verifyRetransmissionStarted();
 
-        verify(mMockIkeMessageHelper)
-                .encryptAndEncode(
-                        anyObject(),
-                        anyObject(),
-                        eq(mSpyCurrentIkeSaRecord),
-                        mIkeMessageCaptor.capture());
-
         // Verify outbound message
-        IkeMessage delMsg = mIkeMessageCaptor.getValue();
+        IkeMessage delMsg = verifyEncryptAndEncodeAndGetMessage(mSpyCurrentIkeSaRecord);
 
         IkeHeader ikeHeader = delMsg.ikeHeader;
         assertEquals(mSpyCurrentIkeSaRecord.getInitiatorSpi(), ikeHeader.ikeInitiatorSpi);
@@ -2522,14 +2710,7 @@
     @Test
     public void testRekeyIkeLocalDeleteHandlesResponse() throws Exception {
         setupIdleStateMachine();
-
-        // Seed fake rekey data and force transition to RekeyIkeLocalDelete
-        mIkeSessionStateMachine.mLocalInitNewIkeSaRecord = mSpyLocalInitIkeSaRecord;
-        mIkeSessionStateMachine.sendMessage(
-                IkeSessionStateMachine.CMD_FORCE_TRANSITION,
-                mIkeSessionStateMachine.mRekeyIkeLocalDelete);
-        mLooper.dispatchAll();
-        verifyRetransmissionStarted();
+        mockCreateAndTransitionToRekeyDeleteLocal();
 
         // Receive Delete response
         ReceivedIkePacket dummyDeleteIkeRespReceivedPacket =
@@ -2551,15 +2732,8 @@
     @Test
     public void testRekeyIkeLocalDeleteHandlesRespWithParsingError() throws Exception {
         setupIdleStateMachine();
-
-        // Seed fake rekey data and force transition to RekeyIkeLocalDelete
-        mIkeSessionStateMachine.mLocalInitNewIkeSaRecord = mSpyLocalInitIkeSaRecord;
-        mIkeSessionStateMachine.sendMessage(
-                IkeSessionStateMachine.CMD_FORCE_TRANSITION,
-                mIkeSessionStateMachine.mRekeyIkeLocalDelete);
-        mLooper.dispatchAll();
-        verifyRetransmissionStarted();
-        reset(mMockIkeMessageHelper);
+        mockCreateAndTransitionToRekeyDeleteLocal();
+        resetMockIkeMessageHelper();
 
         // Mock receiving packet with syntax error
         ReceivedIkePacket mockInvalidPacket =
@@ -2571,12 +2745,7 @@
         mLooper.dispatchAll();
 
         // Verify no more request out
-        verify(mMockIkeMessageHelper, never())
-                .encryptAndEncode(
-                        anyObject(),
-                        anyObject(),
-                        eq(mSpyCurrentIkeSaRecord),
-                        any(IkeMessage.class));
+        verifyEncryptAndEncodeNeverCalled(mSpyCurrentIkeSaRecord);
 
         // Verify final state - Idle, with new SA, and old SA closed.
         verifyRekeyReplaceSa(mSpyLocalInitIkeSaRecord);
@@ -2588,15 +2757,7 @@
     @Test
     public void testRekeyIkeLocalDeleteWithRequestOnNewSa() throws Exception {
         setupIdleStateMachine();
-
-        // Seed fake rekey data and force transition to RekeyIkeLocalDelete
-        mIkeSessionStateMachine.mLocalInitNewIkeSaRecord = mSpyLocalInitIkeSaRecord;
-        mIkeSessionStateMachine.addIkeSaRecord(mSpyLocalInitIkeSaRecord);
-        mIkeSessionStateMachine.sendMessage(
-                IkeSessionStateMachine.CMD_FORCE_TRANSITION,
-                mIkeSessionStateMachine.mRekeyIkeLocalDelete);
-        mLooper.dispatchAll();
-        verifyRetransmissionStarted();
+        mockCreateAndTransitionToRekeyDeleteLocal();
 
         // Receive an empty (DPD) request on the new IKE SA
         mIkeSessionStateMachine.sendMessage(
@@ -2612,6 +2773,42 @@
     }
 
     @Test
+    public void testRekeyIkeLocalDeleteWithRequestFragOnNewSa() throws Exception {
+        setupIdleStateMachine();
+        mockCreateAndTransitionToRekeyDeleteLocal();
+
+        // Received IKE fragment
+        byte[] unencryptedData = "testRekeyIkeLocalDeleteWithRequestFragOnNewSa".getBytes();
+        int fragNum = 1;
+        int totalFragments = 2;
+        IkeSkfPayload skfPayload =
+                IkeTestUtils.makeDummySkfPayload(unencryptedData, fragNum, totalFragments);
+
+        ReceivedIkePacket packet =
+                makeDummyReceivedIkeFragmentPacket(
+                        mSpyLocalInitIkeSaRecord,
+                        false /*isResp*/,
+                        IkeHeader.EXCHANGE_TYPE_CREATE_CHILD_SA,
+                        skfPayload,
+                        PAYLOAD_TYPE_SA,
+                        null /* collectedFrags*/);
+        mIkeSessionStateMachine.sendMessage(IkeSessionStateMachine.CMD_RECEIVE_IKE_PACKET, packet);
+        mLooper.dispatchAll();
+
+        // Verify rekey is done.
+        verifyRekeyReplaceSa(mSpyLocalInitIkeSaRecord);
+        verifyRetransmissionStopped();
+
+        // Verify the IkeSaRecord has stored the new fragment.
+        DecodeResultPartial resultPartial =
+                mSpyLocalInitIkeSaRecord.getCollectedFragments(false /*isResp*/);
+        assertEquals(PAYLOAD_TYPE_SA, resultPartial.firstPayloadType);
+        assertEquals(totalFragments, resultPartial.collectedFragsList.length);
+        assertArrayEquals(unencryptedData, resultPartial.collectedFragsList[fragNum - 1]);
+        assertFalse(resultPartial.isAllFragmentsReceived());
+    }
+
+    @Test
     public void testRekeyIkeRemoteDeleteWithRequestOnNewSa() throws Exception {
         setupIdleStateMachine();
 
@@ -2657,13 +2854,7 @@
         assertEquals(IKE_REKEY_SA_INITIATOR_SPI, recordConfigCaptor.getValue().initSpi.getSpi());
 
         // Verify outbound CREATE_CHILD_SA message
-        verify(mMockIkeMessageHelper)
-                .encryptAndEncode(
-                        anyObject(),
-                        anyObject(),
-                        eq(mSpyCurrentIkeSaRecord),
-                        mIkeMessageCaptor.capture());
-        IkeMessage rekeyCreateResp = mIkeMessageCaptor.getValue();
+        IkeMessage rekeyCreateResp = verifyEncryptAndEncodeAndGetMessage(mSpyCurrentIkeSaRecord);
         IkeHeader rekeyCreateRespHeader = rekeyCreateResp.ikeHeader;
         assertEquals(IkePayload.PAYLOAD_TYPE_SK, rekeyCreateRespHeader.nextPayloadType);
         assertEquals(IkeHeader.EXCHANGE_TYPE_CREATE_CHILD_SA, rekeyCreateRespHeader.exchangeType);
@@ -2748,13 +2939,7 @@
         verifyDecodeEncryptedMessage(mSpyCurrentIkeSaRecord, dummyDeleteIkeRequestReceivedPacket);
 
         // Verify outbound DELETE_IKE_SA message
-        verify(mMockIkeMessageHelper)
-                .encryptAndEncode(
-                        anyObject(),
-                        anyObject(),
-                        eq(mSpyCurrentIkeSaRecord),
-                        mIkeMessageCaptor.capture());
-        IkeMessage rekeyDeleteResp = mIkeMessageCaptor.getValue();
+        IkeMessage rekeyDeleteResp = verifyEncryptAndEncodeAndGetMessage(mSpyCurrentIkeSaRecord);
         IkeHeader rekeyDeleteRespHeader = rekeyDeleteResp.ikeHeader;
         assertEquals(IkePayload.PAYLOAD_TYPE_SK, rekeyDeleteRespHeader.nextPayloadType);
         assertEquals(IkeHeader.EXCHANGE_TYPE_INFORMATIONAL, rekeyDeleteRespHeader.exchangeType);
@@ -2797,9 +2982,7 @@
 
         // Verify no request received, or response sent.
         verify(mMockIkeMessageHelper, never()).decode(anyInt(), anyObject(), anyObject());
-        verify(mMockIkeMessageHelper, never())
-                .encryptAndEncode(
-                        anyObject(), anyObject(), eq(mSpyCurrentIkeSaRecord), anyObject());
+        verifyEncryptAndEncodeNeverCalled(mSpyCurrentIkeSaRecord);
 
         // Verify final state has not changed - signal was not sent.
         assertTrue(
@@ -2823,9 +3006,7 @@
 
         // Verify no request received, or response sent.
         verify(mMockIkeMessageHelper, never()).decode(anyInt(), anyObject(), anyObject());
-        verify(mMockIkeMessageHelper, never())
-                .encryptAndEncode(
-                        anyObject(), anyObject(), eq(mSpyCurrentIkeSaRecord), anyObject());
+        verifyEncryptAndEncodeNeverCalled(mSpyCurrentIkeSaRecord);
 
         // Verify final state - Idle, with new SA, and old SA closed.
         verifyRekeyReplaceSa(mSpyRemoteInitIkeSaRecord);
@@ -2987,7 +3168,8 @@
     @Test
     public void testEncryptedRetransmitterImmediatelySendsRequest() throws Exception {
         setupIdleStateMachine();
-        byte[] dummyLastRespBytes = "testRetransmitterSendsRequestLastResp".getBytes();
+        byte[][] dummyLastRespBytes =
+                new byte[][] {"testRetransmitterSendsRequestLastResp".getBytes()};
         mIkeSessionStateMachine.mLastSentIkeResp = dummyLastRespBytes;
 
         IkeMessage spyIkeReqMessage =
@@ -3004,43 +3186,60 @@
                                 new LinkedList<>()));
 
         // Use something unique as a sentinel value
-        byte[] dummyReqBytes = "testRetransmitterSendsRequest".getBytes();
-        doReturn(dummyReqBytes)
+        byte[][] dummyReqBytesList =
+                new byte[][] {
+                    "testRetransmitterSendsReqFrag1".getBytes(),
+                    "testRetransmitterSendsReqFrag2".getBytes()
+                };
+
+        doReturn(dummyReqBytesList)
                 .when(spyIkeReqMessage)
-                .encryptAndEncode(any(), any(), eq(mSpyCurrentIkeSaRecord));
+                .encryptAndEncode(any(), any(), eq(mSpyCurrentIkeSaRecord), anyBoolean(), anyInt());
 
         IkeSessionStateMachine.EncryptedRetransmitter retransmitter =
                 mIkeSessionStateMachine.new EncryptedRetransmitter(spyIkeReqMessage);
 
         // Verify message is sent out, and that request does not change cached retransmit-response
         // mLastSentIkeResp.
-        verify(mSpyIkeSocket).sendIkePacket(eq(dummyReqBytes), eq(REMOTE_ADDRESS));
+        verify(mSpyIkeSocket).sendIkePacket(eq(dummyReqBytesList[0]), eq(REMOTE_ADDRESS));
+        verify(mSpyIkeSocket).sendIkePacket(eq(dummyReqBytesList[1]), eq(REMOTE_ADDRESS));
         assertArrayEquals(dummyLastRespBytes, mIkeSessionStateMachine.mLastSentIkeResp);
     }
 
     @Test
     public void testCacheLastRequestAndResponse() throws Exception {
         setupIdleStateMachine();
-        mIkeSessionStateMachine.mLastReceivedIkeReq = null;
+        mIkeSessionStateMachine.mLastReceivedIkeReqFirstPacket = null;
         mIkeSessionStateMachine.mLastSentIkeResp = null;
 
-        byte[] dummyIkeReq = "testLastSentRequest".getBytes();
-        byte[] dummyIkeResp = "testLastSentResponse".getBytes();
+        byte[] dummyIkeReqFirstPacket = "testLastSentRequest".getBytes();
+        byte[][] dummyIkeResp =
+                new byte[][] {
+                    "testLastSentRespFrag1".getBytes(), "testLastSentRespFrag2".getBytes()
+                };
 
         when(mMockIkeMessageHelper.encryptAndEncode(
-                        any(), any(), eq(mSpyCurrentIkeSaRecord), any(IkeMessage.class)))
+                        any(),
+                        any(),
+                        eq(mSpyCurrentIkeSaRecord),
+                        any(IkeMessage.class),
+                        anyBoolean(),
+                        anyInt()))
                 .thenReturn(dummyIkeResp);
 
         // Receive a DPD request, expect to send dummyIkeResp
         ReceivedIkePacket dummyDpdRequest =
-                makeDpdIkeRequest(mSpyCurrentIkeSaRecord.getRemoteRequestMessageId(), dummyIkeReq);
+                makeDpdIkeRequest(
+                        mSpyCurrentIkeSaRecord.getRemoteRequestMessageId(), dummyIkeReqFirstPacket);
         mIkeSessionStateMachine.sendMessage(
                 IkeSessionStateMachine.CMD_RECEIVE_IKE_PACKET, dummyDpdRequest);
         mLooper.dispatchAll();
 
-        verify(mSpyIkeSocket).sendIkePacket(eq(dummyIkeResp), eq(REMOTE_ADDRESS));
+        verify(mSpyIkeSocket).sendIkePacket(eq(dummyIkeResp[0]), eq(REMOTE_ADDRESS));
+        verify(mSpyIkeSocket).sendIkePacket(eq(dummyIkeResp[1]), eq(REMOTE_ADDRESS));
         assertArrayEquals(dummyIkeResp, mIkeSessionStateMachine.mLastSentIkeResp);
-        assertArrayEquals(dummyIkeReq, mIkeSessionStateMachine.mLastReceivedIkeReq);
+        assertArrayEquals(
+                dummyIkeReqFirstPacket, mIkeSessionStateMachine.mLastReceivedIkeReqFirstPacket);
     }
 
     @Test
@@ -3048,23 +3247,25 @@
         setupIdleStateMachine();
 
         // Mock last sent request and response
-        byte[] dummyIkeReq = "testRcvRetransmittedRequestReq".getBytes();
-        byte[] dummyIkeResp = "testRcvRetransmittedRequestResp".getBytes();
-        mIkeSessionStateMachine.mLastReceivedIkeReq = dummyIkeReq;
+        byte[] dummyIkeReqFisrtPacket = "testRcvRetransmittedRequestReq".getBytes();
+        byte[][] dummyIkeResp = new byte[][] {"testRcvRetransmittedRequestResp".getBytes()};
+
+        mIkeSessionStateMachine.mLastReceivedIkeReqFirstPacket = dummyIkeReqFisrtPacket;
         mIkeSessionStateMachine.mLastSentIkeResp = dummyIkeResp;
         mSpyCurrentIkeSaRecord.incrementRemoteRequestMessageId();
 
         // Build request with last validated message ID
         ReceivedIkePacket request =
                 makeDpdIkeRequest(
-                        mSpyCurrentIkeSaRecord.getRemoteRequestMessageId() - 1, dummyIkeReq);
+                        mSpyCurrentIkeSaRecord.getRemoteRequestMessageId() - 1,
+                        dummyIkeReqFisrtPacket);
 
         mIkeSessionStateMachine.sendMessage(IkeSessionStateMachine.CMD_RECEIVE_IKE_PACKET, request);
 
         mLooper.dispatchAll();
 
         assertArrayEquals(dummyIkeResp, mIkeSessionStateMachine.mLastSentIkeResp);
-        verify(mSpyIkeSocket).sendIkePacket(eq(dummyIkeResp), eq(REMOTE_ADDRESS));
+        verify(mSpyIkeSocket).sendIkePacket(eq(dummyIkeResp[0]), eq(REMOTE_ADDRESS));
 
         assertTrue(
                 mIkeSessionStateMachine.getCurrentState() instanceof IkeSessionStateMachine.Idle);
@@ -3075,9 +3276,9 @@
         setupIdleStateMachine();
 
         // Mock last sent request and response
-        byte[] dummyIkeReq = "testDiscardFakeRetransmittedRequestReq".getBytes();
-        byte[] dummyIkeResp = "testDiscardFakeRetransmittedRequestResp".getBytes();
-        mIkeSessionStateMachine.mLastReceivedIkeReq = dummyIkeReq;
+        byte[] dummyIkeReqFirstPacket = "testDiscardFakeRetransmittedRequestReq".getBytes();
+        byte[][] dummyIkeResp = new byte[][] {"testDiscardFakeRetransmittedRequestResp".getBytes()};
+        mIkeSessionStateMachine.mLastReceivedIkeReqFirstPacket = dummyIkeReqFirstPacket;
         mIkeSessionStateMachine.mLastSentIkeResp = dummyIkeResp;
         mSpyCurrentIkeSaRecord.incrementRemoteRequestMessageId();
 
@@ -3129,15 +3330,8 @@
         mLooper.dispatchAll();
         verifyRetransmissionStarted();
 
-        verify(mMockIkeMessageHelper)
-                .encryptAndEncode(
-                        anyObject(),
-                        anyObject(),
-                        eq(mSpyCurrentIkeSaRecord),
-                        mIkeMessageCaptor.capture());
-
         // Verify outbound message
-        IkeMessage delMsg = mIkeMessageCaptor.getValue();
+        IkeMessage delMsg = verifyEncryptAndEncodeAndGetMessage(mSpyCurrentIkeSaRecord);
 
         IkeHeader ikeHeader = delMsg.ikeHeader;
         assertEquals(IkePayload.PAYLOAD_TYPE_SK, ikeHeader.nextPayloadType);
@@ -3188,7 +3382,7 @@
                 new LocalRequest(IkeSessionStateMachine.CMD_LOCAL_REQUEST_DELETE_IKE));
         mLooper.dispatchAll();
         verifyRetransmissionStarted();
-        reset(mMockIkeMessageHelper);
+        resetMockIkeMessageHelper();
 
         // Mock receiving response with syntax error
         ReceivedIkePacket mockInvalidPacket =
@@ -3200,12 +3394,7 @@
         mLooper.dispatchAll();
 
         // Verify no more request out
-        verify(mMockIkeMessageHelper, never())
-                .encryptAndEncode(
-                        anyObject(),
-                        anyObject(),
-                        eq(mSpyCurrentIkeSaRecord),
-                        any(IkeMessage.class));
+        verifyEncryptAndEncodeNeverCalled(mSpyCurrentIkeSaRecord);
 
         // Verify state machine quit properly
         verify(mMockIkeSessionCallback).onError(any(InvalidSyntaxException.class));
@@ -3247,12 +3436,9 @@
         verifyRetransmissionStarted();
 
         // Verify delete sent out.
-        verify(mMockIkeMessageHelper)
-                .encryptAndEncode(
-                        anyObject(), anyObject(), eq(mSpyCurrentIkeSaRecord), anyObject());
-        reset(mMockIkeMessageHelper); // Discard value.
-        when(mMockIkeMessageHelper.encryptAndEncode(any(), any(), any(), any()))
-                .thenReturn(new byte[0]);
+        verifyEncryptAndEncodeAndGetMessage(mSpyCurrentIkeSaRecord);
+
+        resetMockIkeMessageHelper(); // Discard value.
 
         ReceivedIkePacket received = makeRekeyIkeRequest();
         mIkeSessionStateMachine.sendMessage(
@@ -3262,15 +3448,8 @@
         verifyRetransmissionStarted();
         verifyIncrementRemoteReqMsgId();
 
-        verify(mMockIkeMessageHelper)
-                .encryptAndEncode(
-                        anyObject(),
-                        anyObject(),
-                        eq(mSpyCurrentIkeSaRecord),
-                        mIkeMessageCaptor.capture());
-
         // Verify outbound response
-        IkeMessage resp = mIkeMessageCaptor.getValue();
+        IkeMessage resp = verifyEncryptAndEncodeAndGetMessage(mSpyCurrentIkeSaRecord);
 
         IkeHeader ikeHeader = resp.ikeHeader;
         assertEquals(IkePayload.PAYLOAD_TYPE_SK, ikeHeader.nextPayloadType);
@@ -3296,15 +3475,8 @@
         mLooper.dispatchAll();
         verifyIncrementRemoteReqMsgId();
 
-        verify(mMockIkeMessageHelper)
-                .encryptAndEncode(
-                        anyObject(),
-                        anyObject(),
-                        eq(mSpyCurrentIkeSaRecord),
-                        mIkeMessageCaptor.capture());
-
         // Verify outbound message
-        IkeMessage delMsg = mIkeMessageCaptor.getValue();
+        IkeMessage delMsg = verifyEncryptAndEncodeAndGetMessage(mSpyCurrentIkeSaRecord);
 
         IkeHeader ikeHeader = delMsg.ikeHeader;
         assertEquals(IkePayload.PAYLOAD_TYPE_SK, ikeHeader.nextPayloadType);
@@ -3333,15 +3505,9 @@
                 mIkeSessionStateMachine.getCurrentState() instanceof IkeSessionStateMachine.Idle);
 
         verifyDecodeEncryptedMessage(mSpyCurrentIkeSaRecord, dummyDpdRequest);
-        verify(mMockIkeMessageHelper)
-                .encryptAndEncode(
-                        anyObject(),
-                        anyObject(),
-                        eq(mSpyCurrentIkeSaRecord),
-                        mIkeMessageCaptor.capture());
 
         // Verify outbound response
-        IkeMessage resp = mIkeMessageCaptor.getValue();
+        IkeMessage resp = verifyEncryptAndEncodeAndGetMessage(mSpyCurrentIkeSaRecord);
         IkeHeader ikeHeader = resp.ikeHeader;
         assertEquals(IkePayload.PAYLOAD_TYPE_SK, ikeHeader.nextPayloadType);
         assertEquals(IkeHeader.EXCHANGE_TYPE_INFORMATIONAL, ikeHeader.exchangeType);
@@ -3370,15 +3536,9 @@
                         instanceof IkeSessionStateMachine.RekeyIkeRemoteDelete);
 
         verifyDecodeEncryptedMessage(mSpyCurrentIkeSaRecord, dummyDpdRequest);
-        verify(mMockIkeMessageHelper)
-                .encryptAndEncode(
-                        anyObject(),
-                        anyObject(),
-                        eq(mSpyCurrentIkeSaRecord),
-                        mIkeMessageCaptor.capture());
 
         // Verify outbound response
-        IkeMessage resp = mIkeMessageCaptor.getValue();
+        IkeMessage resp = verifyEncryptAndEncodeAndGetMessage(mSpyCurrentIkeSaRecord);
         IkeHeader ikeHeader = resp.ikeHeader;
         assertEquals(IkePayload.PAYLOAD_TYPE_SK, ikeHeader.nextPayloadType);
         assertEquals(IkeHeader.EXCHANGE_TYPE_INFORMATIONAL, ikeHeader.exchangeType);
@@ -3411,7 +3571,7 @@
         mIkeSessionStateMachine.sendMessage(
                 IkeSessionStateMachine.CMD_FORCE_TRANSITION, mIkeSessionStateMachine.mReceiving);
         mLooper.dispatchAll();
-        verify(mMockIkeMessageHelper, never()).encryptAndEncode(any(), any(), any(), any());
+        verifyEncryptAndEncodeNeverCalled();
 
         // Queue a local request, and expect that it is not run (yet)
         mIkeSessionStateMachine.sendMessage(
@@ -3420,7 +3580,7 @@
         mLooper.dispatchAll();
 
         // Verify that the state machine is still in the Receiving state
-        verify(mMockIkeMessageHelper, never()).encryptAndEncode(any(), any(), any(), any());
+        verifyEncryptAndEncodeNeverCalled();
         assertTrue(
                 mIkeSessionStateMachine.getCurrentState()
                         instanceof IkeSessionStateMachine.Receiving);
@@ -3433,7 +3593,7 @@
         assertTrue(
                 mIkeSessionStateMachine.getCurrentState()
                         instanceof IkeSessionStateMachine.RekeyIkeLocalCreate);
-        verify(mMockIkeMessageHelper, times(1)).encryptAndEncode(any(), any(), any(), any());
+        verifyEncryptAndEncodeAndGetMessage(mSpyCurrentIkeSaRecord);
     }
 
     @Test
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/SaRecordTest.java b/tests/iketests/src/java/com/android/ike/ikev2/SaRecordTest.java
index 0aea35a..d66a864 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/SaRecordTest.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/SaRecordTest.java
@@ -252,8 +252,8 @@
 
         byte[] initAuthKey = TestUtils.hexStringToByteArray(FIRST_CHILD_AUTH_INIT_HEX_STRING);
         byte[] respAuthKey = TestUtils.hexStringToByteArray(FIRST_CHILD_AUTH_RESP_HEX_STRING);
-        byte[] initEncryptKey = TestUtils.hexStringToByteArray(FIRST_CHILD_ENCR_INIT_HEX_STRING);
-        byte[] respEncryptKey = TestUtils.hexStringToByteArray(FIRST_CHILD_ENCR_RESP_HEX_STRING);
+        byte[] initEncryptionKey = TestUtils.hexStringToByteArray(FIRST_CHILD_ENCR_INIT_HEX_STRING);
+        byte[] respEncryptionKey = TestUtils.hexStringToByteArray(FIRST_CHILD_ENCR_RESP_HEX_STRING);
 
         IIpSecTransformHelper mockIpSecHelper;
         mockIpSecHelper = mock(IIpSecTransformHelper.class);
@@ -271,7 +271,7 @@
                         eq(mHmacSha1IntegrityMac),
                         eq(mAesCbcCipher),
                         aryEq(initAuthKey),
-                        aryEq(initEncryptKey),
+                        aryEq(initEncryptionKey),
                         eq(false)))
                 .thenReturn(mockOutTransform);
 
@@ -283,7 +283,7 @@
                         eq(mHmacSha1IntegrityMac),
                         eq(mAesCbcCipher),
                         aryEq(respAuthKey),
-                        aryEq(respEncryptKey),
+                        aryEq(respEncryptionKey),
                         eq(false)))
                 .thenReturn(mockInTransform);
 
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/crypto/IkeCipherTest.java b/tests/iketests/src/java/com/android/ike/ikev2/crypto/IkeCipherTest.java
index c51d58f..be1052c 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/crypto/IkeCipherTest.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/crypto/IkeCipherTest.java
@@ -115,10 +115,10 @@
 
     @Test
     public void testEncryptWithWrongKey() throws Exception {
-        byte[] encryptKey = TestUtils.hexStringToByteArray(ENCR_KEY_FROM_INIT_TO_RESP + "00");
+        byte[] encryptionKey = TestUtils.hexStringToByteArray(ENCR_KEY_FROM_INIT_TO_RESP + "00");
 
         try {
-            mAesCbcCipher.decrypt(mEncryptedPaddedData, mAesCbcKey, encryptKey);
+            mAesCbcCipher.decrypt(mEncryptedPaddedData, mAesCbcKey, encryptionKey);
             fail("Expected to fail due to encryption key with wrong length.");
         } catch (IllegalArgumentException expected) {
 
@@ -150,10 +150,10 @@
 
     @Test
     public void buildIpSecAlgorithmWithInvalidKey() throws Exception {
-        byte[] encryptKey = TestUtils.hexStringToByteArray(ENCR_KEY_FROM_INIT_TO_RESP + "00");
+        byte[] encryptionKey = TestUtils.hexStringToByteArray(ENCR_KEY_FROM_INIT_TO_RESP + "00");
 
         try {
-            mAesCbcCipher.buildIpSecAlgorithmWithKey(encryptKey);
+            mAesCbcCipher.buildIpSecAlgorithmWithKey(encryptionKey);
 
             fail("Expected to fail due to encryption key with wrong length.");
         } catch (IllegalArgumentException expected) {
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/crypto/IkeMacIntegrityTest.java b/tests/iketests/src/java/com/android/ike/ikev2/crypto/IkeMacIntegrityTest.java
index 5301ab6..d3c857c 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/crypto/IkeMacIntegrityTest.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/crypto/IkeMacIntegrityTest.java
@@ -115,10 +115,10 @@
 
     @Test
     public void buildIpSecAlgorithmWithInvalidKey() throws Exception {
-        byte[] encryptKey = TestUtils.hexStringToByteArray(INTEGRITY_KEY_HEX_STRING + "00");
+        byte[] encryptionKey = TestUtils.hexStringToByteArray(INTEGRITY_KEY_HEX_STRING + "00");
 
         try {
-            mHmacSha1IntegrityMac.buildIpSecAlgorithmWithKey(encryptKey);
+            mHmacSha1IntegrityMac.buildIpSecAlgorithmWithKey(encryptionKey);
 
             fail("Expected to fail due to integrity key with wrong length.");
         } catch (IllegalArgumentException expected) {
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeMessageTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeMessageTest.java
index d19b869..2354c80 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeMessageTest.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeMessageTest.java
@@ -17,26 +17,46 @@
 package com.android.ike.ikev2.message;
 
 import static com.android.ike.ikev2.message.IkeMessage.DECODE_STATUS_OK;
+import static com.android.ike.ikev2.message.IkeMessage.DECODE_STATUS_PROTECTED_ERROR;
+import static com.android.ike.ikev2.message.IkeMessage.DECODE_STATUS_UNPROTECTED_ERROR;
+import static com.android.ike.ikev2.message.IkePayload.PAYLOAD_TYPE_AUTH;
+import static com.android.ike.ikev2.message.IkePayload.PAYLOAD_TYPE_ID_INITIATOR;
+import static com.android.ike.ikev2.message.IkePayload.PAYLOAD_TYPE_NO_NEXT;
+import static com.android.ike.ikev2.message.IkeTestUtils.makeDummySkfPayload;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import com.android.ike.TestUtils;
 import com.android.ike.ikev2.SaRecord.IkeSaRecord;
 import com.android.ike.ikev2.crypto.IkeCipher;
 import com.android.ike.ikev2.crypto.IkeMacIntegrity;
+import com.android.ike.ikev2.exceptions.IkeException;
 import com.android.ike.ikev2.exceptions.IkeInternalException;
-import com.android.ike.ikev2.exceptions.IkeProtocolException;
 import com.android.ike.ikev2.exceptions.InvalidMessageIdException;
 import com.android.ike.ikev2.exceptions.InvalidSyntaxException;
 import com.android.ike.ikev2.exceptions.UnsupportedCriticalPayloadException;
 import com.android.ike.ikev2.message.IkeMessage.DecodeResult;
+import com.android.ike.ikev2.message.IkeMessage.DecodeResultError;
+import com.android.ike.ikev2.message.IkeMessage.DecodeResultOk;
+import com.android.ike.ikev2.message.IkeMessage.DecodeResultPartial;
+import com.android.ike.ikev2.message.IkePayloadFactory.IIkePayloadDecoder;
 
 import org.junit.After;
 import org.junit.Before;
@@ -105,6 +125,18 @@
                     + "29000008000040000000000c000040010000000100000000"
                     + "000000000000000b";
 
+    private static final String IKE_FRAG_HEX_STRING =
+            "939ae1251d18eb9077a99551b15c6e9335202320000000010000"
+                    + "00c0000000a400020002fd7c7931705af184b7be76bbd45a"
+                    + "8ecbb3ffd58b9438b93f67e9fe86b06229f80e9b52d2ff6a"
+                    + "fde3f2c13ae93ce55a801f62e1a818c9003880a36bbe986f"
+                    + "e6979ba233b9f4f0ddc992d06dbad5a2b998be18fae947e5"
+                    + "ccfb37775d069344e711fbf499bb289cf4cca245bd450ad8"
+                    + "9d18689207759507ba18d47247e920b9e000a25a7596e413"
+                    + "0929e5cdc37d5c1b0d90bbaae946c260f4d3cf815f6d";
+    private static final String ID_INIT_PAYLOAD_HEX_STRING = "2400000c010000000a50500d";
+    private static final String ID_RESP_PAYLOAD_HEX_STRING = "0000000c010000000a505050";
+
     private static final long INIT_SPI = 0x5f54bf6d8b48e6e1L;
     private static final long RESP_SPI = 0x909232b3d1edcb5cL;
     private static final String IKE_EMPTY_INFO_MSG_HEX_STRING =
@@ -118,6 +150,17 @@
             "f83b48ba0dbc68bcc4a78136832100aa";
     private static final String IKE_EMPTY_INFO_MSG_CHECKSUM_HEX_STRING = "4192f251cd4d1b97d298e550";
 
+    private static final byte[] FRAGMENT_ONE_UNENCRYPTED_DATA = "fragmentOne".getBytes();
+    private static final byte[] FRAGMENT_TWO_UNENCRYPTED_DATA = "fragmentTwo".getBytes();
+
+    private static final int TOTAL_FRAGMENTS = 2;
+    private static final int FRAGMENT_NUM_ONE = 1;
+    private static final int FRAGMENT_NUM_TWO = 2;
+
+    private static final int IKE_FRAG_EXPECTED_MESSAGE_ID = 1;
+    private static final int TOTAL_FRAGMENTS_OFFSET =
+            IkeHeader.IKE_HEADER_LENGTH + IkePayload.GENERIC_HEADER_LENGTH + 2;
+
     private static final int IKE_AUTH_EXPECTED_MESSAGE_ID = 1;
     private static final int IKE_AUTH_CIPHER_IV_SIZE = 16;
     private static final int IKE_AUTH_CIPHER_BLOCK_SIZE = 16;
@@ -127,10 +170,19 @@
     private byte[] mUnencryptedPaddedData;
     private IkeHeader mIkeAuthHeader;
 
+    private IIkePayloadDecoder mSpyIkePayloadDecoder;
+
     private IkeMacIntegrity mMockIntegrity;
     private IkeCipher mMockCipher;
     private IkeSaRecord mMockIkeSaRecord;
 
+    private byte[] mIkeFragPacket;
+    private IkeHeader mFragOneHeader;
+    private IkeHeader mFragTwoHeader;
+
+    private IkeSkfPayload mDummySkfPayloadOne;
+    private IkeSkfPayload mDummySkfPayloadTwo;
+
     private static final int[] EXPECTED_IKE_INIT_PAYLOAD_LIST = {
         IkePayload.PAYLOAD_TYPE_SA,
         IkePayload.PAYLOAD_TYPE_KE,
@@ -165,20 +217,20 @@
 
     @Before
     public void setUp() throws Exception {
-        IkePayloadFactory.sDecoderInstance =
-                new IkePayloadFactory.IIkePayloadDecoder() {
-
-                    @Override
-                    public IkePayload decodeIkePayload(
-                            int payloadType, boolean isCritical, boolean isResp, byte[] payloadBody)
-                            throws IkeProtocolException {
-                        if (support(payloadType)) {
-                            return new TestIkeSupportedPayload(payloadType, isCritical);
-                        } else {
-                            return new IkeUnsupportedPayload(payloadType, isCritical);
-                        }
+        mSpyIkePayloadDecoder = spy(new IkePayloadFactory.IkePayloadDecoder());
+        doAnswer(
+                (invocation) -> {
+                    int payloadType = (int) invocation.getArguments()[0];
+                    boolean isCritical = (boolean) invocation.getArguments()[1];
+                    if (support(payloadType)) {
+                        return new TestIkeSupportedPayload(payloadType, isCritical);
                     }
-                };
+                    return new IkeUnsupportedPayload(payloadType, isCritical);
+                })
+                .when(mSpyIkePayloadDecoder)
+                .decodeIkePayload(anyInt(), anyBoolean(), anyBoolean(), any());
+
+        IkePayloadFactory.sDecoderInstance = mSpyIkePayloadDecoder;
 
         mIkeAuthPacket = TestUtils.hexStringToByteArray(IKE_AUTH_HEX_STRING);
         mUnencryptedPaddedData =
@@ -199,6 +251,17 @@
         mMockIkeSaRecord = mock(IkeSaRecord.class);
         when(mMockIkeSaRecord.getInboundDecryptionKey()).thenReturn(new byte[0]);
         when(mMockIkeSaRecord.getInboundIntegrityKey()).thenReturn(new byte[0]);
+
+        mIkeFragPacket = TestUtils.hexStringToByteArray(IKE_FRAG_HEX_STRING);
+        mFragOneHeader = new IkeHeader(mIkeFragPacket);
+        mFragTwoHeader = new IkeHeader(mIkeFragPacket);
+
+        mDummySkfPayloadOne =
+                makeDummySkfPayload(
+                        FRAGMENT_ONE_UNENCRYPTED_DATA, FRAGMENT_NUM_ONE, TOTAL_FRAGMENTS);
+        mDummySkfPayloadTwo =
+                makeDummySkfPayload(
+                        FRAGMENT_TWO_UNENCRYPTED_DATA, FRAGMENT_NUM_TWO, TOTAL_FRAGMENTS);
     }
 
     @After
@@ -206,17 +269,35 @@
         IkePayloadFactory.sDecoderInstance = new IkePayloadFactory.IkePayloadDecoder();
     }
 
+    private IkeMessage verifyDecodeResultOkAndGetMessage(DecodeResult decodeResult)
+            throws Exception {
+        assertEquals(DECODE_STATUS_OK, decodeResult.status);
+
+        DecodeResultOk resultOk = (DecodeResultOk) decodeResult;
+        assertNotNull(resultOk.ikeMessage);
+
+        return resultOk.ikeMessage;
+    }
+
+    private IkeException verifyDecodeResultErrorAndGetIkeException(
+            DecodeResult decodeResult, int decodeStatus) throws Exception {
+        assertEquals(decodeStatus, decodeResult.status);
+
+        DecodeResultError resultError = (DecodeResultError) decodeResult;
+        assertNotNull(resultError.ikeException);
+
+        return resultError.ikeException;
+    }
+
     @Test
     public void testDecodeIkeMessage() throws Exception {
         byte[] inputPacket = TestUtils.hexStringToByteArray(IKE_SA_INIT_RAW_PACKET);
         IkeHeader header = new IkeHeader(inputPacket);
 
         DecodeResult decodeResult = IkeMessage.decode(0, header, inputPacket);
-        assertEquals(DECODE_STATUS_OK, decodeResult.status);
-        assertNotNull(decodeResult.ikeMessage);
-        assertNull(decodeResult.ikeException);
 
-        IkeMessage message = decodeResult.ikeMessage;
+        IkeMessage message = verifyDecodeResultOkAndGetMessage(decodeResult);
+
         assertEquals(EXPECTED_IKE_INIT_PAYLOAD_LIST.length, message.ikePayloadList.size());
         for (int i = 0; i < EXPECTED_IKE_INIT_PAYLOAD_LIST.length; i++) {
             assertEquals(
@@ -232,11 +313,9 @@
         IkeHeader header = new IkeHeader(inputPacket);
 
         DecodeResult decodeResult = IkeMessage.decode(0, header, inputPacket);
-        assertEquals(DECODE_STATUS_OK, decodeResult.status);
-        assertNotNull(decodeResult.ikeMessage);
-        assertNull(decodeResult.ikeException);
 
-        IkeMessage message = decodeResult.ikeMessage;
+        IkeMessage message = verifyDecodeResultOkAndGetMessage(decodeResult);
+
         assertEquals(EXPECTED_IKE_INIT_PAYLOAD_LIST.length - 1, message.ikePayloadList.size());
         for (int i = 0; i < EXPECTED_IKE_INIT_PAYLOAD_LIST.length - 1; i++) {
             assertEquals(
@@ -294,12 +373,11 @@
                         mMockCipher,
                         mMockIkeSaRecord,
                         mIkeAuthHeader,
-                        mIkeAuthPacket);
-        assertEquals(IkeMessage.DECODE_STATUS_OK, decodeResult.status);
-        assertNotNull(decodeResult.ikeMessage);
-        assertNull(decodeResult.ikeException);
+                        mIkeAuthPacket,
+                        null /*collectedFragments*/);
+        IkeMessage ikeMessage = verifyDecodeResultOkAndGetMessage(decodeResult);
 
-        assertEquals(IKE_AUTH_PAYLOAD_SIZE, decodeResult.ikeMessage.ikePayloadList.size());
+        assertEquals(IKE_AUTH_PAYLOAD_SIZE, ikeMessage.ikePayloadList.size());
     }
 
     @Test
@@ -311,12 +389,13 @@
                         mMockCipher,
                         mMockIkeSaRecord,
                         mIkeAuthHeader,
-                        mIkeAuthPacket);
-        assertEquals(IkeMessage.DECODE_STATUS_UNPROTECTED_ERROR_MESSAGE, decodeResult.status);
-        assertNull(decodeResult.ikeMessage);
-        assertNotNull(decodeResult.ikeException);
+                        mIkeAuthPacket,
+                        null /*collectedFragments*/);
+        IkeException ikeException =
+                verifyDecodeResultErrorAndGetIkeException(
+                        decodeResult, DECODE_STATUS_UNPROTECTED_ERROR);
 
-        assertTrue(decodeResult.ikeException instanceof InvalidMessageIdException);
+        assertTrue(ikeException instanceof InvalidMessageIdException);
     }
 
     @Test
@@ -330,13 +409,14 @@
                         mMockCipher,
                         mMockIkeSaRecord,
                         mIkeAuthHeader,
-                        mIkeAuthPacket);
-        assertEquals(IkeMessage.DECODE_STATUS_UNPROTECTED_ERROR_MESSAGE, decodeResult.status);
-        assertNull(decodeResult.ikeMessage);
-        assertNotNull(decodeResult.ikeException);
+                        mIkeAuthPacket,
+                        null /*collectedFragments*/);
+        IkeException ikeException =
+                verifyDecodeResultErrorAndGetIkeException(
+                        decodeResult, DECODE_STATUS_UNPROTECTED_ERROR);
 
         assertTrue(
-                ((IkeInternalException) decodeResult.ikeException).getCause()
+                ((IkeInternalException) ikeException).getCause()
                         instanceof GeneralSecurityException);
     }
 
@@ -351,13 +431,14 @@
                         mMockCipher,
                         mMockIkeSaRecord,
                         mIkeAuthHeader,
-                        mIkeAuthPacket);
-        assertEquals(IkeMessage.DECODE_STATUS_UNPROTECTED_ERROR_MESSAGE, decodeResult.status);
-        assertNull(decodeResult.ikeMessage);
-        assertNotNull(decodeResult.ikeException);
+                        mIkeAuthPacket,
+                        null /*collectedFragments*/);
 
+        IkeException ikeException =
+                verifyDecodeResultErrorAndGetIkeException(
+                        decodeResult, DECODE_STATUS_UNPROTECTED_ERROR);
         assertTrue(
-                ((IkeInternalException) decodeResult.ikeException).getCause()
+                ((IkeInternalException) ikeException).getCause()
                         instanceof IllegalBlockSizeException);
     }
 
@@ -377,12 +458,13 @@
                         mMockCipher,
                         mMockIkeSaRecord,
                         mIkeAuthHeader,
-                        mIkeAuthPacket);
-        assertEquals(IkeMessage.DECODE_STATUS_PROTECTED_ERROR_MESSAGE, decodeResult.status);
-        assertNull(decodeResult.ikeMessage);
-        assertNotNull(decodeResult.ikeException);
+                        mIkeAuthPacket,
+                        null /*collectedFragments*/);
+        IkeException ikeException =
+                verifyDecodeResultErrorAndGetIkeException(
+                        decodeResult, DECODE_STATUS_PROTECTED_ERROR);
 
-        assertTrue(decodeResult.ikeException instanceof InvalidSyntaxException);
+        assertTrue(ikeException instanceof InvalidSyntaxException);
     }
 
     private boolean support(int payloadType) {
@@ -395,7 +477,8 @@
         byte[] inputPacket = TestUtils.hexStringToByteArray(IKE_SA_INIT_RAW_PACKET);
         byte[] ikeBodyBytes = TestUtils.hexStringToByteArray(IKE_SA_INIT_BODY_RAW_PACKET);
         IkeHeader header = new IkeHeader(inputPacket);
-        IkeMessage message = IkeMessage.decode(0, header, inputPacket).ikeMessage;
+        IkeMessage message =
+                ((DecodeResultOk) IkeMessage.decode(0, header, inputPacket)).ikeMessage;
 
         byte[] encodedIkeMessage = message.attachEncodedHeader(ikeBodyBytes);
         assertArrayEquals(inputPacket, encodedIkeMessage);
@@ -425,12 +508,398 @@
                         0);
         IkeMessage ikeMessage = new IkeMessage(ikeHeader, new LinkedList<>());
 
-        byte[] ikeMessageBytes =
-                ikeMessage.encryptAndEncode(mMockIntegrity, mMockCipher, mMockIkeSaRecord);
-        byte[] expectedBytes = TestUtils.hexStringToByteArray(IKE_EMPTY_INFO_MSG_HEX_STRING);
+        byte[][] ikeMessageBytes =
+                ikeMessage.encryptAndEncode(
+                        mMockIntegrity,
+                        mMockCipher,
+                        mMockIkeSaRecord,
+                        true /*supportFragment*/,
+                        1280 /*fragSize*/);
+        byte[][] expectedBytes =
+                new byte[][] {TestUtils.hexStringToByteArray(IKE_EMPTY_INFO_MSG_HEX_STRING)};
 
         assertArrayEquals(expectedBytes, ikeMessageBytes);
     }
 
-    // TODO: Implement encodeToByteBuffer() of each payload and add test for encoding message
+    private DecodeResultPartial makeDecodeResultForFragOne(DecodeResultPartial collectedFrags) {
+        return new DecodeResultPartial(
+                mFragOneHeader, mDummySkfPayloadOne, PAYLOAD_TYPE_AUTH, collectedFrags);
+    }
+
+    private DecodeResultPartial makeDecodeResultForFragTwo(DecodeResultPartial collectedFrags) {
+        return new DecodeResultPartial(
+                mFragTwoHeader, mDummySkfPayloadTwo, PAYLOAD_TYPE_NO_NEXT, collectedFrags);
+    }
+
+    @Test
+    public void testConstructDecodePartialFirstFragArriveFirst() throws Exception {
+        DecodeResultPartial resultPartial = makeDecodeResultForFragOne(null /*collectedFragments*/);
+
+        assertEquals(mFragOneHeader, resultPartial.ikeHeader);
+
+        assertEquals(TOTAL_FRAGMENTS, resultPartial.collectedFragsList.length);
+        assertArrayEquals(
+                FRAGMENT_ONE_UNENCRYPTED_DATA,
+                resultPartial.collectedFragsList[FRAGMENT_NUM_ONE - 1]);
+        assertFalse(resultPartial.isAllFragmentsReceived());
+    }
+
+    @Test
+    public void testConstructDecodePartialSecondFragArriveFirst() throws Exception {
+        DecodeResultPartial resultPartial = makeDecodeResultForFragTwo(null /*collectedFragments*/);
+
+        assertEquals(PAYLOAD_TYPE_NO_NEXT, resultPartial.firstPayloadType);
+        assertEquals(mFragTwoHeader, resultPartial.ikeHeader);
+
+        assertEquals(TOTAL_FRAGMENTS, resultPartial.collectedFragsList.length);
+        assertArrayEquals(
+                FRAGMENT_TWO_UNENCRYPTED_DATA,
+                resultPartial.collectedFragsList[FRAGMENT_NUM_TWO - 1]);
+        assertFalse(resultPartial.isAllFragmentsReceived());
+    }
+
+    @Test
+    public void testConstructDecodeResultPartialWithCollectedFrags() throws Exception {
+        DecodeResultPartial resultPartialIncomplete =
+                makeDecodeResultForFragTwo(null /*collectedFragments*/);
+        DecodeResultPartial resultPartialComplete =
+                makeDecodeResultForFragOne(resultPartialIncomplete);
+
+        assertEquals(PAYLOAD_TYPE_AUTH, resultPartialComplete.firstPayloadType);
+        assertEquals(mFragTwoHeader, resultPartialComplete.ikeHeader);
+
+        assertEquals(TOTAL_FRAGMENTS, resultPartialComplete.collectedFragsList.length);
+        assertTrue(resultPartialComplete.isAllFragmentsReceived());
+    }
+
+    @Test
+    public void testReassembleAllFrags() throws Exception {
+        DecodeResultPartial resultPartialIncomplete =
+                makeDecodeResultForFragOne(null /*collectedFragments*/);
+        DecodeResultPartial resultPartialComplete =
+                makeDecodeResultForFragTwo(resultPartialIncomplete);
+
+        assertEquals(PAYLOAD_TYPE_AUTH, resultPartialIncomplete.firstPayloadType);
+        assertEquals(mFragOneHeader, resultPartialIncomplete.ikeHeader);
+
+        assertEquals(TOTAL_FRAGMENTS, resultPartialIncomplete.collectedFragsList.length);
+        assertTrue(resultPartialIncomplete.isAllFragmentsReceived());
+
+        // Verify reassembly result
+        ByteBuffer expectedBuffer =
+                ByteBuffer.allocate(
+                        FRAGMENT_ONE_UNENCRYPTED_DATA.length
+                                + FRAGMENT_TWO_UNENCRYPTED_DATA.length);
+        expectedBuffer.put(FRAGMENT_ONE_UNENCRYPTED_DATA).put(FRAGMENT_TWO_UNENCRYPTED_DATA);
+
+        byte[] reassembledBytes = resultPartialComplete.reassembleAllFrags();
+        assertArrayEquals(expectedBuffer.array(), reassembledBytes);
+    }
+
+    @Test
+    public void testReassembleIncompleteFragmentsThrows() throws Exception {
+        DecodeResultPartial resultPartial = makeDecodeResultForFragTwo(null /*collectedFragments*/);
+
+        assertFalse(resultPartial.isAllFragmentsReceived());
+
+        try {
+            resultPartial.reassembleAllFrags();
+            fail("Expected to fail because reassembly is not done");
+        } catch (IllegalStateException expected) {
+
+        }
+    }
+
+    private void setDecryptSkfPayload(IkeSkfPayload skf) throws Exception {
+        doReturn(skf)
+                .when(mSpyIkePayloadDecoder)
+                .decodeIkeSkPayload(
+                        eq(true),
+                        anyBoolean(),
+                        any(),
+                        eq(mMockIntegrity),
+                        eq(mMockCipher),
+                        any(),
+                        any());
+    }
+
+    private DecodeResult decodeSkf(
+            int expectedMsgId,
+            IkeHeader header,
+            byte[] packet,
+            DecodeResultPartial collectFragments)
+            throws Exception {
+        return IkeMessage.decode(
+                expectedMsgId,
+                mMockIntegrity,
+                mMockCipher,
+                mMockIkeSaRecord,
+                header,
+                packet,
+                collectFragments);
+    }
+
+    @Test
+    public void testRcvFirstArrivedFrag() throws Exception {
+        setDecryptSkfPayload(mDummySkfPayloadTwo);
+        DecodeResult decodeResult =
+                decodeSkf(
+                        IKE_FRAG_EXPECTED_MESSAGE_ID,
+                        mFragTwoHeader,
+                        mIkeFragPacket,
+                        null /* collectedFragments*/);
+
+        // Verify decoding result
+        assertTrue(decodeResult instanceof DecodeResultPartial);
+        DecodeResultPartial resultPartial = (DecodeResultPartial) decodeResult;
+
+        assertEquals(PAYLOAD_TYPE_NO_NEXT, resultPartial.firstPayloadType);
+        assertEquals(mFragTwoHeader, resultPartial.ikeHeader);
+
+        assertEquals(TOTAL_FRAGMENTS, resultPartial.collectedFragsList.length);
+        assertArrayEquals(
+                FRAGMENT_TWO_UNENCRYPTED_DATA,
+                resultPartial.collectedFragsList[FRAGMENT_NUM_TWO - 1]);
+        assertFalse(resultPartial.isAllFragmentsReceived());
+    }
+
+    @Test
+    public void testRcvLastArrivedFrag() throws Exception {
+        // Create two dummy SKF Payloads so that the complete unencrypted data is two ID payloads
+        byte[] idInitPayloadBytes = TestUtils.hexStringToByteArray(ID_INIT_PAYLOAD_HEX_STRING);
+        byte[] idRespPayloadBytes = TestUtils.hexStringToByteArray(ID_RESP_PAYLOAD_HEX_STRING);
+        IkeSkfPayload skfOne =
+                makeDummySkfPayload(idInitPayloadBytes, FRAGMENT_NUM_ONE, TOTAL_FRAGMENTS);
+        IkeSkfPayload skfTwo =
+                makeDummySkfPayload(idRespPayloadBytes, FRAGMENT_NUM_TWO, TOTAL_FRAGMENTS);
+
+        DecodeResultPartial resultPartialIncomplete =
+                new DecodeResultPartial(
+                        mFragOneHeader,
+                        skfOne,
+                        PAYLOAD_TYPE_ID_INITIATOR,
+                        null /* collectedFragments*/);
+
+        setDecryptSkfPayload(skfTwo);
+        DecodeResult decodeResult =
+                decodeSkf(
+                        IKE_FRAG_EXPECTED_MESSAGE_ID,
+                        mFragTwoHeader,
+                        mIkeFragPacket,
+                        resultPartialIncomplete);
+
+        // Verify fragments reassembly has been finished and complete message has been decoded.
+        assertTrue(decodeResult instanceof DecodeResultOk);
+        DecodeResultOk resultOk = (DecodeResultOk) decodeResult;
+        assertEquals(2, resultOk.ikeMessage.ikePayloadList.size());
+    }
+
+    @Test
+    public void testRcvFirstArrivedFragWithUnprotectedError() throws Exception {
+        DecodeResult decodeResult =
+                decodeSkf(
+                        IKE_FRAG_EXPECTED_MESSAGE_ID + 1,
+                        mFragTwoHeader,
+                        mIkeFragPacket,
+                        null /* collectedFragments*/);
+
+        // Verify that unprotected error was returned
+        IkeException ikeException =
+                verifyDecodeResultErrorAndGetIkeException(
+                        decodeResult, DECODE_STATUS_UNPROTECTED_ERROR);
+        assertTrue(ikeException instanceof InvalidMessageIdException);
+    }
+
+    @Test
+    public void testRcvLastArrivedFragWithUnprotectedError() throws Exception {
+        DecodeResultPartial resultPartialIncomplete =
+                makeDecodeResultForFragOne(null /* collectedFragments*/);
+
+        DecodeResult decodeResult =
+                decodeSkf(
+                        IKE_FRAG_EXPECTED_MESSAGE_ID + 1,
+                        mFragTwoHeader,
+                        mIkeFragPacket,
+                        resultPartialIncomplete);
+
+        // Verify that newly received fragment was discarded
+        assertEquals(resultPartialIncomplete, decodeResult);
+    }
+
+    @Test
+    public void testRcvFragWithLargerTotalFragments() throws Exception {
+        DecodeResultPartial resultPartialIncomplete =
+                new DecodeResultPartial(
+                        mFragOneHeader,
+                        mDummySkfPayloadOne,
+                        PAYLOAD_TYPE_NO_NEXT,
+                        null /* collectedFragments*/);
+
+        // Set total fragments of inbound fragment to 5
+        int totalFragments = 5;
+        byte[] fragPacket = TestUtils.hexStringToByteArray(IKE_FRAG_HEX_STRING);
+        fragPacket[TOTAL_FRAGMENTS_OFFSET] = 0;
+        fragPacket[TOTAL_FRAGMENTS_OFFSET] = 5;
+
+        byte[] unencryptedData = "testRcvFragWithLargerTotalFragments".getBytes();
+        IkeSkfPayload skfPayload =
+                makeDummySkfPayload(unencryptedData, FRAGMENT_NUM_TWO, totalFragments);
+        setDecryptSkfPayload(skfPayload);
+        DecodeResult decodeResult =
+                decodeSkf(
+                        IKE_FRAG_EXPECTED_MESSAGE_ID,
+                        mFragTwoHeader,
+                        fragPacket,
+                        resultPartialIncomplete);
+
+        // Verify that previously collected fragments were all discarded
+        assertTrue(decodeResult instanceof DecodeResultPartial);
+        DecodeResultPartial resultPartial = (DecodeResultPartial) decodeResult;
+
+        assertEquals(PAYLOAD_TYPE_NO_NEXT, resultPartial.firstPayloadType);
+        assertEquals(mFragTwoHeader, resultPartial.ikeHeader);
+
+        assertEquals(totalFragments, resultPartial.collectedFragsList.length);
+        assertArrayEquals(unencryptedData, resultPartial.collectedFragsList[FRAGMENT_NUM_TWO - 1]);
+        assertFalse(resultPartial.isAllFragmentsReceived());
+    }
+
+    @Test
+    public void testRcvFragWithSmallerTotalFragments() throws Exception {
+        int totalFragments = 5;
+        byte[] unencryptedData = "testRcvFragWithSmallerTotalFragments".getBytes();
+        IkeSkfPayload skfPayload =
+                makeDummySkfPayload(unencryptedData, FRAGMENT_NUM_ONE, totalFragments);
+
+        DecodeResultPartial resultPartialIncomplete =
+                new DecodeResultPartial(
+                        mFragOneHeader,
+                        skfPayload,
+                        PAYLOAD_TYPE_AUTH,
+                        null /* collectedFragments*/);
+
+        setDecryptSkfPayload(mDummySkfPayloadTwo);
+        DecodeResult decodeResult =
+                decodeSkf(
+                        IKE_FRAG_EXPECTED_MESSAGE_ID,
+                        mFragTwoHeader,
+                        mIkeFragPacket,
+                        resultPartialIncomplete);
+
+        // Verify that newly received fragment was discarded
+        assertEquals(resultPartialIncomplete, decodeResult);
+    }
+
+    @Test
+    public void testRcvReplayFrag() throws Exception {
+        DecodeResultPartial resultPartialIncomplete =
+                makeDecodeResultForFragTwo(null /* collectedFragments*/);
+
+        setDecryptSkfPayload(mDummySkfPayloadTwo);
+        DecodeResult decodeResult =
+                decodeSkf(
+                        IKE_FRAG_EXPECTED_MESSAGE_ID,
+                        mFragTwoHeader,
+                        mIkeFragPacket,
+                        resultPartialIncomplete);
+
+        // Verify that newly received fragment was discarded
+        assertEquals(resultPartialIncomplete, decodeResult);
+    }
+
+    @Test
+    public void testRcvCompleteMessageDuringReassembly() throws Exception {
+        DecodeResultPartial resultPartialIncomplete =
+                makeDecodeResultForFragTwo(null /* collectedFragments*/);
+
+        DecodeResult decodeResult =
+                decodeSkf(
+                        IKE_AUTH_EXPECTED_MESSAGE_ID,
+                        mIkeAuthHeader,
+                        mIkeAuthPacket,
+                        resultPartialIncomplete);
+
+        // Verify that newly received IKE message was discarded
+        assertEquals(resultPartialIncomplete, decodeResult);
+    }
+
+    @Test
+    public void testEncodeAndEncryptFragments() throws Exception {
+        int messageId = 1;
+        int fragSize = 140;
+        int expectedTotalFragments = 3;
+
+        byte[] integrityKey = new byte[0];
+        byte[] encryptionKey = new byte[0];
+        byte[] iv = new byte[IKE_AUTH_CIPHER_IV_SIZE];
+
+        when(mMockCipher.generateIv()).thenReturn(iv);
+        when(mMockCipher.encrypt(any(), any(), any()))
+                .thenAnswer(
+                        (invocation) -> {
+                            return (byte[]) invocation.getArguments()[0];
+                        });
+
+        IkeHeader ikeHeader =
+                new IkeHeader(
+                        INIT_SPI,
+                        RESP_SPI,
+                        IkePayload.PAYLOAD_TYPE_SK,
+                        IkeHeader.EXCHANGE_TYPE_IKE_AUTH,
+                        true /*isResp*/,
+                        false /*fromInit*/,
+                        messageId);
+
+        byte[][] packetList =
+                new IkeMessage.IkeMessageHelper()
+                        .encryptAndEncode(
+                                ikeHeader,
+                                IkePayload.PAYLOAD_TYPE_AUTH,
+                                mUnencryptedPaddedData,
+                                mMockIntegrity,
+                                mMockCipher,
+                                integrityKey,
+                                encryptionKey,
+                                true /*supportFragment*/,
+                                fragSize);
+
+        assertEquals(expectedTotalFragments, packetList.length);
+
+        IkeHeader expectedIkeHeader =
+                new IkeHeader(
+                        INIT_SPI,
+                        RESP_SPI,
+                        IkePayload.PAYLOAD_TYPE_SKF,
+                        IkeHeader.EXCHANGE_TYPE_IKE_AUTH,
+                        true /*isResp*/,
+                        false /*fromInit*/,
+                        messageId);
+        for (int i = 0; i < packetList.length; i++) {
+            byte[] p = packetList[i];
+
+            // Verify fragment length
+            assertNotNull(p);
+            assertTrue(p.length <= fragSize);
+
+            ByteBuffer packetBuffer = ByteBuffer.wrap(p);
+
+            // Verify IKE header
+            byte[] headerBytes = new byte[IkeHeader.IKE_HEADER_LENGTH];
+            packetBuffer.get(headerBytes);
+
+            ByteBuffer expectedHeadBuffer = ByteBuffer.allocate(IkeHeader.IKE_HEADER_LENGTH);
+            expectedIkeHeader.encodeToByteBuffer(
+                    expectedHeadBuffer, p.length - IkeHeader.IKE_HEADER_LENGTH);
+
+            assertArrayEquals(expectedHeadBuffer.array(), headerBytes);
+
+            // Verify fragment payload header
+            packetBuffer.get(new byte[IkePayload.GENERIC_HEADER_LENGTH]);
+            assertEquals(i + 1 /*expetced fragNum*/, Short.toUnsignedInt(packetBuffer.getShort()));
+            assertEquals(expectedTotalFragments, Short.toUnsignedInt(packetBuffer.getShort()));
+        }
+
+        verify(mMockCipher, times(expectedTotalFragments + 1))
+                .encrypt(any(), eq(encryptionKey), eq(iv));
+    }
 }
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSkPayloadTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSkPayloadTest.java
index 26f3d01..f3af5b6 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSkPayloadTest.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSkPayloadTest.java
@@ -63,7 +63,7 @@
     private static final int CHECKSUM_LEN = 12;
 
     private IkeCipher mAesCbcDecryptCipher;
-    private byte[] mAesCbcDecryptKey;
+    private byte[] mAesCbcDecryptionKey;
 
     private IkeMacIntegrity mHmacSha1IntegrityMac;
     private byte[] mHmacSha1IntegrityKey;
@@ -76,7 +76,7 @@
                                 SaProposal.ENCRYPTION_ALGORITHM_AES_CBC,
                                 SaProposal.KEY_LEN_AES_128),
                         IkeMessage.getSecurityProvider());
-        mAesCbcDecryptKey = TestUtils.hexStringToByteArray(ENCR_KEY_FROM_INIT_TO_RESP);
+        mAesCbcDecryptionKey = TestUtils.hexStringToByteArray(ENCR_KEY_FROM_INIT_TO_RESP);
         mHmacSha1IntegrityMac =
                 IkeMacIntegrity.create(
                         new IntegrityTransform(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96),
@@ -97,7 +97,7 @@
                                 mHmacSha1IntegrityMac,
                                 mAesCbcDecryptCipher,
                                 mHmacSha1IntegrityKey,
-                                mAesCbcDecryptKey)
+                                mAesCbcDecryptionKey)
                         .first;
         int payloadLength = payload.getPayloadLength();
         ByteBuffer buffer = ByteBuffer.allocate(payloadLength);
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSkfPayloadTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSkfPayloadTest.java
index e4c25d3..df81904 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSkfPayloadTest.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSkfPayloadTest.java
@@ -34,6 +34,9 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
 public final class IkeSkfPayloadTest {
     private static final String IKE_FRAG_MSG_HEX_STRING =
             "bb3d5237aa558779db24aeb6c9ea183e352023200000000100000134"
@@ -111,7 +114,7 @@
                                         mSpyHmacSha256IntegrityMac,
                                         mSpyAesCbcCipher,
                                         new byte[0] /*integrityKey*/,
-                                        new byte[0] /*decryptKey*/)
+                                        new byte[0] /*decryptionKey*/)
                                 .first;
         return payload;
     }
@@ -178,4 +181,18 @@
         } catch (InvalidSyntaxException expected) {
         }
     }
+
+    @Test
+    public void testEncode() throws Exception {
+        byte[] message = TestUtils.hexStringToByteArray(IKE_FRAG_MSG_HEX_STRING);
+        IkeSkfPayload payload = decodeAndDecryptFragMsg(message);
+
+        int payloadLength = payload.getPayloadLength();
+        ByteBuffer buffer = ByteBuffer.allocate(payloadLength);
+        payload.encodeToByteBuffer(IkePayload.PAYLOAD_TYPE_NO_NEXT, buffer);
+
+        byte[] expectedPayloadBytes =
+                Arrays.copyOfRange(message, IkeHeader.IKE_HEADER_LENGTH, message.length);
+        assertArrayEquals(expectedPayloadBytes, buffer.array());
+    }
 }
diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeTestUtils.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeTestUtils.java
index 4c4b256..a87f8ad 100644
--- a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeTestUtils.java
+++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeTestUtils.java
@@ -16,18 +16,20 @@
 
 package com.android.ike.ikev2.message;
 
-import static com.android.ike.ikev2.message.IkeMessage.DECODE_STATUS_UNPROTECTED_ERROR_MESSAGE;
+import static com.android.ike.ikev2.message.IkeMessage.DECODE_STATUS_UNPROTECTED_ERROR;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import android.util.Pair;
 
 import com.android.ike.TestUtils;
 import com.android.ike.ikev2.exceptions.IkeProtocolException;
 import com.android.ike.ikev2.message.IkeMessage.DecodeResult;
+import com.android.ike.ikev2.message.IkeMessage.DecodeResultError;
 
 import java.nio.ByteBuffer;
 
@@ -52,11 +54,18 @@
         IkeHeader header = new IkeHeader(inputPacket);
         DecodeResult decodeResult = IkeMessage.decode(0, header, inputPacket);
 
-        assertEquals(DECODE_STATUS_UNPROTECTED_ERROR_MESSAGE, decodeResult.status);
-        assertNull(decodeResult.ikeMessage);
-        assertNotNull(decodeResult.ikeException);
-        assertTrue(expectedException.isInstance(decodeResult.ikeException));
+        assertEquals(DECODE_STATUS_UNPROTECTED_ERROR, decodeResult.status);
+        DecodeResultError resultError = (DecodeResultError) decodeResult;
+        assertNotNull(resultError.ikeException);
+        assertTrue(expectedException.isInstance(resultError.ikeException));
 
-        return (T) decodeResult.ikeException;
+        return (T) resultError.ikeException;
+    }
+
+    public static IkeSkfPayload makeDummySkfPayload(
+            byte[] unencryptedData, int fragNum, int totalFrags) throws Exception {
+        IkeEncryptedPayloadBody mockEncryptedBody = mock(IkeEncryptedPayloadBody.class);
+        when(mockEncryptedBody.getUnencryptedData()).thenReturn(unencryptedData);
+        return new IkeSkfPayload(mockEncryptedBody, fragNum, totalFrags);
     }
 }