Add cleaned up unit tests moved from platform am: 35ef4b5382
am: 8acff00705

Change-Id: I6f3a572c8ddf2ef119552ac44223f3c981ada8d4
diff --git a/src/com/android/cellbroadcastservice/BearerData.java b/src/com/android/cellbroadcastservice/BearerData.java
index 6ebd041..cc7a155 100644
--- a/src/com/android/cellbroadcastservice/BearerData.java
+++ b/src/com/android/cellbroadcastservice/BearerData.java
@@ -66,6 +66,13 @@
     public static final int LANGUAGE_HEBREW = 0x07;
 
     /**
+     * Supported message types for CDMA SMS messages
+     * (See 3GPP2 C.S0015-B, v2.0, table 4.5.1-1)
+     * Used for CdmaSmsCbTest.
+     */
+    public static final int MESSAGE_TYPE_DELIVER        = 0x01;
+
+    /**
      * 16-bit value indicating the message ID, which increments modulo 65536.
      * (Special rules apply for WAP-messages.)
      * (See 3GPP2 C.S0015-B, v2, 4.5.1)
diff --git a/src/com/android/cellbroadcastservice/DefaultCellBroadcastService.java b/src/com/android/cellbroadcastservice/DefaultCellBroadcastService.java
index fdf3b4a..15e550b 100644
--- a/src/com/android/cellbroadcastservice/DefaultCellBroadcastService.java
+++ b/src/com/android/cellbroadcastservice/DefaultCellBroadcastService.java
@@ -26,6 +26,8 @@
 import android.telephony.cdma.CdmaSmsCbProgramData;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Consumer;
@@ -76,7 +78,8 @@
         } else {
             plmn = "";
         }
-        SmsCbMessage message = parseBroadcastSms(slotIndex, plmn, bearerData, serviceCategory);
+        SmsCbMessage message = parseBroadcastSms(getApplicationContext(), slotIndex, plmn,
+                bearerData, serviceCategory);
         if (message != null) {
             mCdmaCellBroadcastHandler.onCdmaCellBroadcastSms(message);
         }
@@ -90,7 +93,6 @@
                 originatingAddress, callback);
     }
 
-
     /**
      * Parses a CDMA broadcast SMS
      *
@@ -99,9 +101,11 @@
      * @param bearerData the bearerData of the SMS
      * @param serviceCategory the service category of the broadcast
      */
-    private SmsCbMessage parseBroadcastSms(int slotIndex, String plmn, byte[] bearerData,
+    @VisibleForTesting
+    public static SmsCbMessage parseBroadcastSms(Context context, int slotIndex, String plmn,
+            byte[] bearerData,
             int serviceCategory) {
-        BearerData bData = BearerData.decode(getApplicationContext(), bearerData, serviceCategory);
+        BearerData bData = BearerData.decode(context, bearerData, serviceCategory);
         if (bData == null) {
             Log.w(TAG, "BearerData.decode() returned null");
             return null;
diff --git a/tests/src/com/android/cellbroadcastservice/CdmaSmsMessageTest.java b/tests/src/com/android/cellbroadcastservice/CdmaSmsMessageTest.java
new file mode 100644
index 0000000..e24dbd5
--- /dev/null
+++ b/tests/src/com/android/cellbroadcastservice/CdmaSmsMessageTest.java
@@ -0,0 +1,875 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cellbroadcastservice;
+
+import android.content.Context;
+import android.hardware.radio.V1_0.CdmaSmsMessage;
+import android.telephony.SmsCbCmasInfo;
+import android.telephony.SmsCbMessage;
+import android.telephony.cdma.CdmaSmsCbProgramData;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.Log;
+
+import com.android.internal.telephony.GsmAlphabet;
+import com.android.internal.telephony.cdma.SmsMessage;
+import com.android.internal.telephony.cdma.SmsMessageConverter;
+import com.android.internal.util.BitwiseOutputStream;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Test cases to verify that our parseBroadcastSms function correctly works with the
+ * CdmaSmsMessage class.
+ */
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class CdmaSmsMessageTest extends CellBroadcastServiceTestBase {
+
+    private static final String TAG = "CdmaSmsMessageTest";
+
+    /* Copy of private subparameter identifier constants from BearerData class. */
+    private static final byte SUBPARAM_MESSAGE_IDENTIFIER = (byte) 0x00;
+    private static final byte SUBPARAM_USER_DATA = (byte) 0x01;
+    private static final byte SUBPARAM_PRIORITY_INDICATOR = (byte) 0x08;
+    private static final byte SUBPARAM_LANGUAGE_INDICATOR = (byte) 0x0D;
+    private static final byte SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA = 0x12;
+
+    private static final int TELESERVICE_NOT_SET = 0x0000;
+    private static final int TELESERVICE_SCPT = 0x1006;
+
+    /**
+     * Digit Mode Indicator is a 1-bit value that indicates whether
+     * the address digits are 4-bit DTMF codes or 8-bit codes.  (See
+     * 3GPP2 C.S0015-B, v2, 3.4.3.3)
+     */
+    private static final int DIGIT_MODE_4BIT_DTMF = 0x00;
+    private static final int DIGIT_MODE_8BIT_CHAR = 0x01;
+
+    /**
+     * Number Mode Indicator is 1-bit value that indicates whether the
+     * address type is a data network address or not.  (See 3GPP2
+     * C.S0015-B, v2, 3.4.3.3)
+     */
+    private static final int NUMBER_MODE_NOT_DATA_NETWORK = 0x00;
+    private static final int NUMBER_MODE_DATA_NETWORK = 0x01;
+
+    /**
+     * Number Types for data networks.
+     * (See 3GPP2 C.S005-D, table2.7.1.3.2.4-2 for complete table)
+     * (See 3GPP2 C.S0015-B, v2, 3.4.3.3 for data network subset)
+     * NOTE: value is stored in the parent class ton field.
+     */
+    private static final int TON_UNKNOWN = 0x00;
+
+    /**
+     * Numbering Plan identification is a 0 or 4-bit value that
+     * indicates which numbering plan identification is set.  (See
+     * 3GPP2, C.S0015-B, v2, 3.4.3.3 and C.S005-D, table2.7.1.3.2.4-3)
+     */
+    private static final int NUMBERING_PLAN_ISDN_TELEPHONY = 0x1;
+
+    /**
+     * User data encoding types.
+     * (See 3GPP2 C.R1001-F, v1.0, table 9.1-1)
+     */
+    public static final int ENCODING_OCTET = 0x00;
+    public static final int ENCODING_IS91_EXTENDED_PROTOCOL = 0x01;
+    public static final int ENCODING_7BIT_ASCII = 0x02;
+    public static final int ENCODING_IA5 = 0x03;
+    public static final int ENCODING_UNICODE_16 = 0x04;
+    public static final int ENCODING_SHIFT_JIS = 0x05;
+    public static final int ENCODING_KOREAN = 0x06;
+    public static final int ENCODING_LATIN_HEBREW = 0x07;
+    public static final int ENCODING_LATIN = 0x08;
+    public static final int ENCODING_GSM_7BIT_ALPHABET = 0x09;
+    public static final int ENCODING_GSM_DCS = 0x0A;
+
+    /**
+     * IS-91 message types.
+     * (See TIA/EIS/IS-91-A-ENGL 1999, table 3.7.1.1-3)
+     */
+    public static final int IS91_MSG_TYPE_VOICEMAIL_STATUS = 0x82;
+    public static final int IS91_MSG_TYPE_SHORT_MESSAGE_FULL = 0x83;
+    public static final int IS91_MSG_TYPE_CLI = 0x84;
+    public static final int IS91_MSG_TYPE_SHORT_MESSAGE = 0x85;
+
+    /**
+     * Supported message types for CDMA SMS messages
+     * (See 3GPP2 C.S0015-B, v2.0, table 4.5.1-1)
+     */
+    public static final int MESSAGE_TYPE_DELIVER = 0x01;
+    public static final int MESSAGE_TYPE_SUBMIT = 0x02;
+    public static final int MESSAGE_TYPE_CANCELLATION = 0x03;
+    public static final int MESSAGE_TYPE_DELIVERY_ACK = 0x04;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        putResources(R.bool.config_sms_utf8_support, false);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    /**
+     * Initialize a Parcel for an incoming CDMA cell broadcast. The caller will write the
+     * bearer data and then convert it to an SmsMessage.
+     *
+     * @param serviceCategory the CDMA service category
+     * @return the initialized Parcel
+     */
+    private static CdmaSmsMessage createBroadcastParcel(int serviceCategory) {
+        CdmaSmsMessage msg = new CdmaSmsMessage();
+
+        msg.teleserviceId = TELESERVICE_NOT_SET;
+        msg.isServicePresent = true;
+        msg.serviceCategory = serviceCategory;
+
+        // dummy address (RIL may generate a different dummy address for broadcasts)
+        msg.address.digitMode = DIGIT_MODE_4BIT_DTMF;
+        msg.address.numberMode = NUMBER_MODE_NOT_DATA_NETWORK;
+        msg.address.numberType = TON_UNKNOWN;
+        msg.address.numberPlan = NUMBERING_PLAN_ISDN_TELEPHONY;
+        msg.subAddress.subaddressType = 0;
+        msg.subAddress.odd = false;
+        return msg;
+    }
+
+    /**
+     * Initialize a BitwiseOutputStream with the CDMA bearer data subparameters except for
+     * user data. The caller will append the user data and add it to the parcel.
+     *
+     * @param messageId the 16-bit message identifier
+     * @param priority  message priority
+     * @param language  message language code
+     * @return the initialized BitwiseOutputStream
+     */
+    private static BitwiseOutputStream createBearerDataStream(int messageId, int priority,
+            int language) throws BitwiseOutputStream.AccessException {
+        BitwiseOutputStream bos = new BitwiseOutputStream(10);
+        bos.write(8, SUBPARAM_MESSAGE_IDENTIFIER);
+        bos.write(8, 3);    // length: 3 bytes
+        bos.write(4, BearerData.MESSAGE_TYPE_DELIVER);
+        bos.write(8, ((messageId >>> 8) & 0xff));
+        bos.write(8, (messageId & 0xff));
+        bos.write(1, 0);    // no User Data Header
+        bos.write(3, 0);    // reserved
+
+        if (priority != -1) {
+            bos.write(8, SUBPARAM_PRIORITY_INDICATOR);
+            bos.write(8, 1);    // length: 1 byte
+            bos.write(2, (priority & 0x03));
+            bos.write(6, 0);    // reserved
+        }
+
+        if (language != -1) {
+            bos.write(8, SUBPARAM_LANGUAGE_INDICATOR);
+            bos.write(8, 1);    // length: 1 byte
+            bos.write(8, (language & 0xff));
+        }
+
+        return bos;
+    }
+
+    /**
+     * Write the bearer data array to the parcel, then return a new SmsMessage from the parcel.
+     *
+     * @param msg        CdmaSmsMessage containing the CDMA SMS headers
+     * @param bearerData the bearer data byte array to append to the parcel
+     * @return the new SmsMessage created from the parcel
+     */
+    private static SmsMessage createMessageFromParcel(CdmaSmsMessage msg, byte[] bearerData) {
+        for (byte b : bearerData) {
+            msg.bearerData.add(b);
+        }
+        SmsMessage message = SmsMessageConverter.newCdmaSmsMessageFromRil(msg);
+        return message;
+    }
+
+    /**
+     * Create a parcel for an incoming CMAS broadcast, then return a new SmsMessage created
+     * from the parcel.
+     *
+     * @param serviceCategory the CDMA service category
+     * @param messageId       the 16-bit message identifier
+     * @param priority        message priority
+     * @param language        message language code
+     * @param body            message body
+     * @param cmasCategory    CMAS category (or -1 to skip adding CMAS type 1 elements record)
+     * @param responseType    CMAS response type
+     * @param severity        CMAS severity
+     * @param urgency         CMAS urgency
+     * @param certainty       CMAS certainty
+     * @return the newly created SmsMessage object
+     */
+    private static SmsMessage createCmasSmsMessage(int serviceCategory, int messageId, int priority,
+            int language, int encoding, String body, int cmasCategory, int responseType,
+            int severity, int urgency, int certainty) throws Exception {
+        BitwiseOutputStream cmasBos = new BitwiseOutputStream(10);
+        cmasBos.write(8, 0);    // CMAE protocol version 0
+
+        if (body != null) {
+            cmasBos.write(8, 0);        // Type 0 elements (alert text)
+            encodeBody(encoding, body, true, cmasBos);
+        }
+
+        if (cmasCategory != SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN) {
+            cmasBos.write(8, 1);    // Type 1 elements
+            cmasBos.write(8, 4);    // length: 4 bytes
+            cmasBos.write(8, (cmasCategory & 0xff));
+            cmasBos.write(8, (responseType & 0xff));
+            cmasBos.write(4, (severity & 0x0f));
+            cmasBos.write(4, (urgency & 0x0f));
+            cmasBos.write(4, (certainty & 0x0f));
+            cmasBos.write(4, 0);    // pad to octet boundary
+        }
+
+        byte[] cmasUserData = cmasBos.toByteArray();
+
+        CdmaSmsMessage msg = createBroadcastParcel(serviceCategory);
+        BitwiseOutputStream bos = createBearerDataStream(messageId, priority, language);
+
+        bos.write(8, SUBPARAM_USER_DATA);
+        bos.write(8, cmasUserData.length + 2);  // add 2 bytes for msg_encoding and num_fields
+        bos.write(5, ENCODING_OCTET);
+        bos.write(8, cmasUserData.length);
+        bos.writeByteArray(cmasUserData.length * 8, cmasUserData);
+        bos.write(3, 0);    // pad to byte boundary
+
+        return createMessageFromParcel(msg, bos.toByteArray());
+    }
+
+    /**
+     * Create a parcel for an incoming CDMA cell broadcast, then return a new SmsMessage created
+     * from the parcel.
+     *
+     * @param serviceCategory the CDMA service category
+     * @param messageId       the 16-bit message identifier
+     * @param priority        message priority
+     * @param language        message language code
+     * @param encoding        user data encoding method
+     * @param body            the message body
+     * @return the newly created SmsMessage object
+     */
+    private static SmsMessage createBroadcastSmsMessage(int serviceCategory, int messageId,
+            int priority, int language, int encoding, String body) throws Exception {
+        CdmaSmsMessage msg = createBroadcastParcel(serviceCategory);
+        BitwiseOutputStream bos = createBearerDataStream(messageId, priority, language);
+
+        bos.write(8, SUBPARAM_USER_DATA);
+        encodeBody(encoding, body, false, bos);
+
+        return createMessageFromParcel(msg, bos.toByteArray());
+    }
+
+    /**
+     * Append the message length, encoding, and body to the BearerData output stream.
+     * This is used for writing the User Data subparameter for non-CMAS broadcasts and for
+     * writing the alert text for CMAS broadcasts.
+     *
+     * @param encoding     one of the CDMA UserData encoding values
+     * @param body         the message body
+     * @param isCmasRecord true if this is a CMAS type 0 elements record; false for user data
+     * @param bos          the BitwiseOutputStream to write to
+     * @throws Exception on any encoding error
+     */
+    private static void encodeBody(int encoding, String body, boolean isCmasRecord,
+            BitwiseOutputStream bos) throws Exception {
+        if (encoding == ENCODING_7BIT_ASCII || encoding == ENCODING_IA5) {
+            int charCount = body.length();
+            int recordBits = (charCount * 7) + 5;       // add 5 bits for char set field
+            int recordOctets = (recordBits + 7) / 8;    // round up to octet boundary
+            int padBits = (recordOctets * 8) - recordBits;
+
+            if (!isCmasRecord) {
+                recordOctets++;                         // add 8 bits for num_fields
+            }
+
+            bos.write(8, recordOctets);
+            bos.write(5, (encoding & 0x1f));
+
+            if (!isCmasRecord) {
+                bos.write(8, charCount);
+            }
+
+            for (int i = 0; i < charCount; i++) {
+                bos.write(7, body.charAt(i));
+            }
+
+            bos.write(padBits, 0);      // pad to octet boundary
+        } else if (encoding == ENCODING_GSM_7BIT_ALPHABET
+                || encoding == ENCODING_GSM_DCS) {
+            // convert to 7-bit packed encoding with septet count in index 0 of byte array
+            byte[] encodedBody = GsmAlphabet.stringToGsm7BitPacked(body);
+
+            int charCount = encodedBody[0];             // septet count
+            int recordBits = (charCount * 7) + 5;       // add 5 bits for char set field
+            int recordOctets = (recordBits + 7) / 8;    // round up to octet boundary
+            int padBits = (recordOctets * 8) - recordBits;
+
+            if (!isCmasRecord) {
+                recordOctets++;                         // add 8 bits for num_fields
+                if (encoding == ENCODING_GSM_DCS) {
+                    recordOctets++;                     // add 8 bits for DCS (message type)
+                }
+            }
+
+            bos.write(8, recordOctets);
+            bos.write(5, (encoding & 0x1f));
+
+            if (!isCmasRecord && encoding == ENCODING_GSM_DCS) {
+                bos.write(8, 0);        // GSM DCS: 7 bit default alphabet, no msg class
+            }
+
+            if (!isCmasRecord) {
+                bos.write(8, charCount);
+            }
+            byte[] bodySeptets = Arrays.copyOfRange(encodedBody, 1, encodedBody.length);
+            bos.writeByteArray(charCount * 7, bodySeptets);
+            bos.write(padBits, 0);      // pad to octet boundary
+        } else if (encoding == ENCODING_IS91_EXTENDED_PROTOCOL) {
+            // 6 bit packed encoding with 0x20 offset (ASCII 0x20 - 0x60)
+            int charCount = body.length();
+            int recordBits = (charCount * 6) + 21;      // add 21 bits for header fields
+            int recordOctets = (recordBits + 7) / 8;    // round up to octet boundary
+            int padBits = (recordOctets * 8) - recordBits;
+
+            bos.write(8, recordOctets);
+
+            bos.write(5, (encoding & 0x1f));
+            bos.write(8, IS91_MSG_TYPE_SHORT_MESSAGE);
+            bos.write(8, charCount);
+
+            for (int i = 0; i < charCount; i++) {
+                bos.write(6, ((int) body.charAt(i) - 0x20));
+            }
+
+            bos.write(padBits, 0);      // pad to octet boundary
+        } else {
+            byte[] encodedBody;
+            switch (encoding) {
+                case ENCODING_UNICODE_16:
+                    encodedBody = body.getBytes("UTF-16BE");
+                    break;
+
+                case ENCODING_SHIFT_JIS:
+                    encodedBody = body.getBytes("Shift_JIS");
+                    break;
+
+                case ENCODING_KOREAN:
+                    encodedBody = body.getBytes("KSC5601");
+                    break;
+
+                case ENCODING_LATIN_HEBREW:
+                    encodedBody = body.getBytes("ISO-8859-8");
+                    break;
+
+                case ENCODING_LATIN:
+                default:
+                    encodedBody = body.getBytes("ISO-8859-1");
+                    break;
+            }
+            int charCount = body.length();              // use actual char count for num fields
+            int recordOctets = encodedBody.length + 1;  // add 1 byte for encoding and pad bits
+            if (!isCmasRecord) {
+                recordOctets++;                         // add 8 bits for num_fields
+            }
+            bos.write(8, recordOctets);
+            bos.write(5, (encoding & 0x1f));
+            if (!isCmasRecord) {
+                bos.write(8, charCount);
+            }
+            bos.writeByteArray(encodedBody.length * 8, encodedBody);
+            bos.write(3, 0);            // pad to octet boundary
+        }
+    }
+
+    private static final String TEST_TEXT = "This is a test CDMA cell broadcast message..."
+            + "678901234567890123456789012345678901234567890";
+
+    private static final String PRES_ALERT =
+            "THE PRESIDENT HAS ISSUED AN EMERGENCY ALERT. CHECK LOCAL MEDIA FOR MORE DETAILS";
+
+    private static final String EXTREME_ALERT = "FLASH FLOOD WARNING FOR SOUTH COCONINO COUNTY"
+            + " - NORTH CENTRAL ARIZONA UNTIL 415 PM MST";
+
+    private static final String SEVERE_ALERT = "SEVERE WEATHER WARNING FOR SOMERSET COUNTY"
+            + " - NEW JERSEY UNTIL 415 PM MST";
+
+    private static final String AMBER_ALERT =
+            "AMBER ALERT:Mountain View,CA VEH'07 Blue Honda Civic CA LIC 5ABC123";
+
+    private static final String MONTHLY_TEST_ALERT = "This is a test of the emergency alert system."
+            + " This is only a test. 89012345678901234567890";
+
+    private static final String IS91_TEXT = "IS91 SHORT MSG";   // max length 14 chars
+
+    /**
+     * Verify that the SmsCbMessage has the correct values for CDMA.
+     *
+     * @param cbMessage the message to test
+     */
+    private static void verifyCbValues(SmsCbMessage cbMessage) {
+        assertEquals(SmsCbMessage.MESSAGE_FORMAT_3GPP2, cbMessage.getMessageFormat());
+        assertEquals(SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE, cbMessage.getGeographicalScope());
+        assertEquals(false, cbMessage.isEtwsMessage()); // ETWS on CDMA not currently supported
+    }
+
+    private static void doTestNonEmergencyBroadcast(Context context, int encoding)
+            throws Exception {
+        SmsMessage msg = createBroadcastSmsMessage(123, 456, BearerData.PRIORITY_NORMAL,
+                BearerData.LANGUAGE_ENGLISH, encoding, TEST_TEXT);
+
+        SmsCbMessage cbMessage =
+                DefaultCellBroadcastService.parseBroadcastSms(context,
+                        0, "", msg.getEnvelopeBearerData(), msg.getEnvelopeServiceCategory());
+        //SmsCbMessage cbMessage = msg.parseBroadcastSms("", 0);
+        verifyCbValues(cbMessage);
+        assertEquals(123, cbMessage.getServiceCategory());
+        assertEquals(456, cbMessage.getSerialNumber());
+        assertEquals(SmsCbMessage.MESSAGE_PRIORITY_NORMAL, cbMessage.getMessagePriority());
+        assertEquals("en", cbMessage.getLanguageCode());
+        assertEquals(TEST_TEXT, cbMessage.getMessageBody());
+        assertEquals(false, cbMessage.isEmergencyMessage());
+        assertEquals(false, cbMessage.isCmasMessage());
+    }
+
+    @Test
+    public void testNonEmergencyBroadcast7bitAscii() throws Exception {
+        doTestNonEmergencyBroadcast(mMockedContext, ENCODING_7BIT_ASCII);
+    }
+
+    @Test
+    public void testNonEmergencyBroadcast7bitGsm() throws Exception {
+        doTestNonEmergencyBroadcast(mMockedContext, ENCODING_GSM_7BIT_ALPHABET);
+    }
+
+    @Test
+    public void testNonEmergencyBroadcast16bitUnicode() throws Exception {
+        doTestNonEmergencyBroadcast(mMockedContext, ENCODING_UNICODE_16);
+    }
+
+    private static void doTestCmasBroadcast(Context context, int serviceCategory, int messageClass,
+            String body) throws Exception {
+        SmsMessage msg = createCmasSmsMessage(
+                serviceCategory, 1234, BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH,
+                ENCODING_7BIT_ASCII, body, -1, -1, -1, -1, -1);
+
+        SmsCbMessage cbMessage =
+                DefaultCellBroadcastService.parseBroadcastSms(context,
+                        0, "", msg.getEnvelopeBearerData(), msg.getEnvelopeServiceCategory());
+        //SmsCbMessage cbMessage = msg.parseBroadcastSms("", 0);
+        verifyCbValues(cbMessage);
+        assertEquals(serviceCategory, cbMessage.getServiceCategory());
+        assertEquals(1234, cbMessage.getSerialNumber());
+        assertEquals(SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, cbMessage.getMessagePriority());
+        assertEquals("en", cbMessage.getLanguageCode());
+        assertEquals(body, cbMessage.getMessageBody());
+        assertEquals(true, cbMessage.isEmergencyMessage());
+        assertEquals(true, cbMessage.isCmasMessage());
+        SmsCbCmasInfo cmasInfo = cbMessage.getCmasWarningInfo();
+        assertEquals(messageClass, cmasInfo.getMessageClass());
+        assertEquals(SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN, cmasInfo.getCategory());
+        assertEquals(SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, cmasInfo.getResponseType());
+        assertEquals(SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN, cmasInfo.getSeverity());
+        assertEquals(SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN, cmasInfo.getUrgency());
+        assertEquals(SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN, cmasInfo.getCertainty());
+    }
+
+    @Test
+    public void testCmasPresidentialAlert() throws Exception {
+        doTestCmasBroadcast(mMockedContext,
+                CdmaSmsCbProgramData.CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT,
+                SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT, PRES_ALERT);
+    }
+
+    @Test
+    public void testCmasExtremeAlert() throws Exception {
+        doTestCmasBroadcast(mMockedContext, CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT,
+                SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT, EXTREME_ALERT);
+    }
+
+    @Test
+    public void testCmasSevereAlert() throws Exception {
+        doTestCmasBroadcast(mMockedContext, CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT,
+                SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT, SEVERE_ALERT);
+    }
+
+    @Test
+    public void testCmasAmberAlert() throws Exception {
+        doTestCmasBroadcast(mMockedContext,
+                CdmaSmsCbProgramData.CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY,
+                SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY, AMBER_ALERT);
+    }
+
+    @Test
+    public void testCmasTestMessage() throws Exception {
+        doTestCmasBroadcast(mMockedContext, CdmaSmsCbProgramData.CATEGORY_CMAS_TEST_MESSAGE,
+                SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST, MONTHLY_TEST_ALERT);
+    }
+
+    @Test
+    public void testCmasExtremeAlertType1Elements() throws Exception {
+        SmsMessage msg = createCmasSmsMessage(CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT,
+                5678, BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH,
+                ENCODING_7BIT_ASCII, EXTREME_ALERT, SmsCbCmasInfo.CMAS_CATEGORY_ENV,
+                SmsCbCmasInfo.CMAS_RESPONSE_TYPE_MONITOR, SmsCbCmasInfo.CMAS_SEVERITY_SEVERE,
+                SmsCbCmasInfo.CMAS_URGENCY_EXPECTED, SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY);
+
+        SmsCbMessage cbMessage =
+                DefaultCellBroadcastService.parseBroadcastSms(mMockedContext,
+                        0, "", msg.getEnvelopeBearerData(), msg.getEnvelopeServiceCategory());
+        //SmsCbMessage cbMessage = msg.parseBroadcastSms("", 0);
+        verifyCbValues(cbMessage);
+        assertEquals(CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT,
+                cbMessage.getServiceCategory());
+        assertEquals(5678, cbMessage.getSerialNumber());
+        assertEquals(SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, cbMessage.getMessagePriority());
+        assertEquals("en", cbMessage.getLanguageCode());
+        assertEquals(EXTREME_ALERT, cbMessage.getMessageBody());
+        assertEquals(true, cbMessage.isEmergencyMessage());
+        assertEquals(true, cbMessage.isCmasMessage());
+        SmsCbCmasInfo cmasInfo = cbMessage.getCmasWarningInfo();
+        assertEquals(SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT, cmasInfo.getMessageClass());
+        assertEquals(SmsCbCmasInfo.CMAS_CATEGORY_ENV, cmasInfo.getCategory());
+        assertEquals(SmsCbCmasInfo.CMAS_RESPONSE_TYPE_MONITOR, cmasInfo.getResponseType());
+        assertEquals(SmsCbCmasInfo.CMAS_SEVERITY_SEVERE, cmasInfo.getSeverity());
+        assertEquals(SmsCbCmasInfo.CMAS_URGENCY_EXPECTED, cmasInfo.getUrgency());
+        assertEquals(SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY, cmasInfo.getCertainty());
+    }
+
+    // VZW requirement is to discard message with unsupported charset. Verify that we return null
+    // for this unsupported character set.
+    @Ignore
+    @Test
+    public void testCmasUnsupportedCharSet() throws Exception {
+        SmsMessage msg = createCmasSmsMessage(CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT,
+                12345, BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH,
+                0x1F, EXTREME_ALERT, -1, -1, -1, -1, -1);
+
+        SmsCbMessage cbMessage =
+                DefaultCellBroadcastService.parseBroadcastSms(mMockedContext,
+                        0, "", msg.getEnvelopeBearerData(), msg.getEnvelopeServiceCategory());
+        //SmsCbMessage cbMessage = msg.parseBroadcastSms("", 0);
+        assertNull("expected null for unsupported charset", cbMessage);
+    }
+
+    // VZW requirement is to discard message with unsupported charset. Verify that we return null
+    // for this unsupported character set.
+    @Test
+    public void testCmasUnsupportedCharSet2() throws Exception {
+        SmsMessage msg = createCmasSmsMessage(CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT,
+                67890, BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH,
+                ENCODING_KOREAN, EXTREME_ALERT, -1, -1, -1, -1, -1);
+
+        SmsCbMessage cbMessage =
+                DefaultCellBroadcastService.parseBroadcastSms(mMockedContext,
+                        0, "", msg.getEnvelopeBearerData(), msg.getEnvelopeServiceCategory());
+        //SmsCbMessage cbMessage = msg.parseBroadcastSms("", 0);
+        assertNull("expected null for unsupported charset", cbMessage);
+    }
+
+    // VZW requirement is to discard message without record type 0. The framework will decode it
+    // and the app will discard it.
+    @Test
+    public void testCmasNoRecordType0() throws Exception {
+        SmsMessage msg = createCmasSmsMessage(
+                CdmaSmsCbProgramData.CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT, 1234,
+                BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH,
+                ENCODING_7BIT_ASCII, null, -1, -1, -1, -1, -1);
+
+        SmsCbMessage cbMessage =
+                DefaultCellBroadcastService.parseBroadcastSms(mMockedContext,
+                        0, "", msg.getEnvelopeBearerData(), msg.getEnvelopeServiceCategory());
+        //SmsCbMessage cbMessage = msg.parseBroadcastSms("", 0);
+        verifyCbValues(cbMessage);
+        assertEquals(CdmaSmsCbProgramData.CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT,
+                cbMessage.getServiceCategory());
+        assertEquals(1234, cbMessage.getSerialNumber());
+        assertEquals(SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, cbMessage.getMessagePriority());
+        assertEquals("en", cbMessage.getLanguageCode());
+        assertEquals(null, cbMessage.getMessageBody());
+        assertEquals(true, cbMessage.isEmergencyMessage());
+        assertEquals(true, cbMessage.isCmasMessage());
+        SmsCbCmasInfo cmasInfo = cbMessage.getCmasWarningInfo();
+        assertEquals(SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT, cmasInfo.getMessageClass());
+        assertEquals(SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN, cmasInfo.getCategory());
+        assertEquals(SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, cmasInfo.getResponseType());
+        assertEquals(SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN, cmasInfo.getSeverity());
+        assertEquals(SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN, cmasInfo.getUrgency());
+        assertEquals(SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN, cmasInfo.getCertainty());
+    }
+
+    // Make sure we don't throw an exception if we feed completely random data to BearerStream.
+    @Test
+    public void testRandomBearerStreamData() {
+        Random r = new Random(54321);
+        for (int run = 0; run < 1000; run++) {
+            int len = r.nextInt(140);
+            byte[] data = new byte[len];
+            for (int i = 0; i < len; i++) {
+                data[i] = (byte) r.nextInt(256);
+            }
+            // Log.d(TAG, "trying random bearer data run " + run + " length " + len);
+            try {
+                int category = 0x0ff0 + r.nextInt(32);  // half CMAS, half non-CMAS
+                CdmaSmsMessage cdmaSmsMessage = createBroadcastParcel(category);
+                SmsMessage msg = createMessageFromParcel(cdmaSmsMessage, data);
+                SmsCbMessage cbMessage =
+                        DefaultCellBroadcastService.parseBroadcastSms(
+                                mMockedContext,
+                                0, "", msg.getEnvelopeBearerData(),
+                                msg.getEnvelopeServiceCategory());
+                //SmsCbMessage cbMessage = msg.parseBroadcastSms("", 0);
+                // with random input, cbMessage will almost always be null (log when it isn't)
+                if (cbMessage != null) {
+                    Log.d(TAG, "success: " + cbMessage);
+                }
+            } catch (Exception e) {
+                Log.d(TAG, "exception thrown", e);
+                fail("Exception in decoder at run " + run + " length " + len + ": " + e);
+            }
+        }
+    }
+
+    // Make sure we don't throw an exception if we put random data in the UserData subparam.
+    @Test
+    public void testRandomUserData() {
+        Random r = new Random(94040);
+        for (int run = 0; run < 1000; run++) {
+            int category = 0x0ff0 + r.nextInt(32);  // half CMAS, half non-CMAS
+            CdmaSmsMessage cdmaSmsMessage = createBroadcastParcel(category);
+            int len = r.nextInt(140);
+            // Log.d(TAG, "trying random user data run " + run + " length " + len);
+
+            try {
+                BitwiseOutputStream bos = createBearerDataStream(r.nextInt(65536), r.nextInt(4),
+                        r.nextInt(256));
+
+                bos.write(8, SUBPARAM_USER_DATA);
+                bos.write(8, len);
+
+                for (int i = 0; i < len; i++) {
+                    bos.write(8, r.nextInt(256));
+                }
+
+                SmsMessage msg = createMessageFromParcel(cdmaSmsMessage, bos.toByteArray());
+                SmsCbMessage cbMessage =
+                        DefaultCellBroadcastService.parseBroadcastSms(mMockedContext, 0, "",
+                                msg.getEnvelopeBearerData(), msg.getEnvelopeServiceCategory());
+                //SmsCbMessage cbMessage = msg.parseBroadcastSms("", 0);
+            } catch (Exception e) {
+                Log.d(TAG, "exception thrown", e);
+                fail("Exception in decoder at run " + run + " length " + len + ": " + e);
+            }
+        }
+    }
+
+    /**
+     * Initialize a Parcel for incoming Service Category Program Data teleservice. The caller will
+     * write the bearer data and then convert it to an SmsMessage.
+     *
+     * @return the initialized Parcel
+     */
+    private static CdmaSmsMessage createServiceCategoryProgramDataParcel() {
+        CdmaSmsMessage msg = new CdmaSmsMessage();
+
+        msg.teleserviceId = TELESERVICE_SCPT;
+        msg.isServicePresent = false;
+        msg.serviceCategory = 0;
+
+        // dummy address (RIL may generate a different dummy address for broadcasts)
+        msg.address.digitMode = DIGIT_MODE_4BIT_DTMF;
+        msg.address.numberMode = NUMBER_MODE_NOT_DATA_NETWORK;
+        msg.address.numberType = TON_UNKNOWN;
+        msg.address.numberPlan = NUMBERING_PLAN_ISDN_TELEPHONY;
+        msg.subAddress.subaddressType = 0;
+        msg.subAddress.odd = false;
+        return msg;
+    }
+
+    private static final String CAT_EXTREME_THREAT = "Extreme Threat to Life and Property";
+    private static final String CAT_SEVERE_THREAT = "Severe Threat to Life and Property";
+    private static final String CAT_AMBER_ALERTS = "AMBER Alerts";
+
+    @Test
+    public void testServiceCategoryProgramDataAddCategory() throws Exception {
+        CdmaSmsMessage cdmaSmsMessage = createServiceCategoryProgramDataParcel();
+        BitwiseOutputStream bos = createBearerDataStream(123, -1, -1);
+
+        int categoryNameLength = CAT_EXTREME_THREAT.length();
+        int subparamLengthBits = (53 + (categoryNameLength * 7));
+        int subparamLengthBytes = (subparamLengthBits + 7) / 8;
+        int subparamPadBits = (subparamLengthBytes * 8) - subparamLengthBits;
+
+        bos.write(8, SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA);
+        bos.write(8, subparamLengthBytes);
+        bos.write(5, ENCODING_7BIT_ASCII);
+
+        bos.write(4, CdmaSmsCbProgramData.OPERATION_ADD_CATEGORY);
+        bos.write(8, (CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT >>> 8));
+        bos.write(8, (CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT & 0xff));
+        bos.write(8, BearerData.LANGUAGE_ENGLISH);
+        bos.write(8, 100);  // max messages
+        bos.write(4, CdmaSmsCbProgramData.ALERT_OPTION_DEFAULT_ALERT);
+
+        bos.write(8, categoryNameLength);
+        for (int i = 0; i < categoryNameLength; i++) {
+            bos.write(7, CAT_EXTREME_THREAT.charAt(i));
+        }
+        bos.write(subparamPadBits, 0);
+
+        SmsMessage msg = createMessageFromParcel(cdmaSmsMessage, bos.toByteArray());
+        assertNotNull(msg);
+        msg.parseSms();
+        List<CdmaSmsCbProgramData> programDataList = msg.getSmsCbProgramData();
+        assertNotNull(programDataList);
+        assertEquals(1, programDataList.size());
+        CdmaSmsCbProgramData programData = programDataList.get(0);
+        assertEquals(CdmaSmsCbProgramData.OPERATION_ADD_CATEGORY, programData.getOperation());
+        assertEquals(CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT,
+                programData.getCategory());
+        assertEquals(CAT_EXTREME_THREAT, programData.getCategoryName());
+        assertEquals(BearerData.LANGUAGE_ENGLISH, programData.getLanguage());
+        assertEquals(100, programData.getMaxMessages());
+        assertEquals(CdmaSmsCbProgramData.ALERT_OPTION_DEFAULT_ALERT, programData.getAlertOption());
+    }
+
+    @Test
+    public void testServiceCategoryProgramDataDeleteTwoCategories() throws Exception {
+        CdmaSmsMessage cdmaSmsMessage = createServiceCategoryProgramDataParcel();
+        BitwiseOutputStream bos = createBearerDataStream(456, -1, -1);
+
+        int category1NameLength = CAT_SEVERE_THREAT.length();
+        int category2NameLength = CAT_AMBER_ALERTS.length();
+
+        int subparamLengthBits = (101 + (category1NameLength * 7) + (category2NameLength * 7));
+        int subparamLengthBytes = (subparamLengthBits + 7) / 8;
+        int subparamPadBits = (subparamLengthBytes * 8) - subparamLengthBits;
+
+        bos.write(8, SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA);
+        bos.write(8, subparamLengthBytes);
+        bos.write(5, ENCODING_7BIT_ASCII);
+
+        bos.write(4, CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY);
+        bos.write(8, (CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT >>> 8));
+        bos.write(8, (CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT & 0xff));
+        bos.write(8, BearerData.LANGUAGE_ENGLISH);
+        bos.write(8, 0);  // max messages
+        bos.write(4, CdmaSmsCbProgramData.ALERT_OPTION_NO_ALERT);
+
+        bos.write(8, category1NameLength);
+        for (int i = 0; i < category1NameLength; i++) {
+            bos.write(7, CAT_SEVERE_THREAT.charAt(i));
+        }
+
+        bos.write(4, CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY);
+        bos.write(8, (CdmaSmsCbProgramData.CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY >>> 8));
+        bos.write(8, (CdmaSmsCbProgramData.CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY & 0xff));
+        bos.write(8, BearerData.LANGUAGE_ENGLISH);
+        bos.write(8, 0);  // max messages
+        bos.write(4, CdmaSmsCbProgramData.ALERT_OPTION_NO_ALERT);
+
+        bos.write(8, category2NameLength);
+        for (int i = 0; i < category2NameLength; i++) {
+            bos.write(7, CAT_AMBER_ALERTS.charAt(i));
+        }
+
+        bos.write(subparamPadBits, 0);
+
+        SmsMessage msg = createMessageFromParcel(cdmaSmsMessage, bos.toByteArray());
+        assertNotNull(msg);
+        msg.parseSms();
+        List<CdmaSmsCbProgramData> programDataList = msg.getSmsCbProgramData();
+        assertNotNull(programDataList);
+        assertEquals(2, programDataList.size());
+
+        CdmaSmsCbProgramData programData = programDataList.get(0);
+        assertEquals(CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY, programData.getOperation());
+        assertEquals(CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT,
+                programData.getCategory());
+        assertEquals(CAT_SEVERE_THREAT, programData.getCategoryName());
+        assertEquals(BearerData.LANGUAGE_ENGLISH, programData.getLanguage());
+        assertEquals(0, programData.getMaxMessages());
+        assertEquals(CdmaSmsCbProgramData.ALERT_OPTION_NO_ALERT, programData.getAlertOption());
+
+        programData = programDataList.get(1);
+        assertEquals(CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY, programData.getOperation());
+        assertEquals(CdmaSmsCbProgramData.CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY,
+                programData.getCategory());
+        assertEquals(CAT_AMBER_ALERTS, programData.getCategoryName());
+        assertEquals(BearerData.LANGUAGE_ENGLISH, programData.getLanguage());
+        assertEquals(0, programData.getMaxMessages());
+        assertEquals(CdmaSmsCbProgramData.ALERT_OPTION_NO_ALERT, programData.getAlertOption());
+    }
+
+    private static final byte[] CMAS_TEST_BEARER_DATA = {
+            0x00, 0x03, 0x1C, 0x78, 0x00, 0x01, 0x59, 0x02, (byte) 0xB8, 0x00, 0x02, 0x10,
+            (byte) 0xAA,
+            0x68, (byte) 0xD3, (byte) 0xCD, 0x06, (byte) 0x9E, 0x68, 0x30, (byte) 0xA0, (byte) 0xE9,
+            (byte) 0x97, (byte) 0x9F, 0x44, 0x1B, (byte) 0xF3, 0x20, (byte) 0xE9, (byte) 0xA3,
+            0x2A, 0x08, 0x7B, (byte) 0xF6, (byte) 0xED, (byte) 0xCB, (byte) 0xCB, 0x1E, (byte) 0x9C,
+            0x3B, 0x10, 0x4D, (byte) 0xDF, (byte) 0x8B, 0x4E,
+            (byte) 0xCC, (byte) 0xA8, 0x20, (byte) 0xEC, (byte) 0xCB, (byte) 0xCB, (byte) 0xA2,
+            0x0A,
+            0x7E, 0x79, (byte) 0xF4, (byte) 0xCB, (byte) 0xB5, 0x72, 0x0A, (byte) 0x9A, 0x34,
+            (byte) 0xF3, 0x41, (byte) 0xA7, (byte) 0x9A, 0x0D, (byte) 0xFB, (byte) 0xB6, 0x79, 0x41,
+            (byte) 0x85, 0x07, 0x4C, (byte) 0xBC, (byte) 0xFA, 0x2E, 0x00, 0x08, 0x20, 0x58, 0x38,
+            (byte) 0x88, (byte) 0x80, 0x10, 0x54, 0x06, 0x38, 0x20, 0x60,
+            0x30, (byte) 0xA8, (byte) 0x81, (byte) 0x90, 0x20, 0x08
+    };
+
+    // Test case for CMAS test message received on the Sprint network.
+    @Test
+    public void testDecodeRawBearerData() {
+        CdmaSmsMessage cdmaSmsMessage =
+                createBroadcastParcel(CdmaSmsCbProgramData.CATEGORY_CMAS_TEST_MESSAGE);
+        SmsMessage msg = createMessageFromParcel(cdmaSmsMessage, CMAS_TEST_BEARER_DATA);
+
+        SmsCbMessage cbMessage =
+                DefaultCellBroadcastService.parseBroadcastSms(mMockedContext,
+                        0, "", msg.getEnvelopeBearerData(), msg.getEnvelopeServiceCategory());
+        //SmsCbMessage cbMessage = msg.parseBroadcastSms("", 0);
+        assertNotNull("expected non-null for bearer data", cbMessage);
+        assertEquals("geoScope", cbMessage.getGeographicalScope(), 1);
+        assertEquals("serialNumber", cbMessage.getSerialNumber(), 51072);
+        assertEquals("serviceCategory", cbMessage.getServiceCategory(),
+                CdmaSmsCbProgramData.CATEGORY_CMAS_TEST_MESSAGE);
+        assertEquals("payload", cbMessage.getMessageBody(),
+                "This is a test of the Commercial Mobile Alert System. This is only a test.");
+
+        SmsCbCmasInfo cmasInfo = cbMessage.getCmasWarningInfo();
+        assertNotNull("expected non-null for CMAS info", cmasInfo);
+        assertEquals("category", cmasInfo.getCategory(), SmsCbCmasInfo.CMAS_CATEGORY_OTHER);
+        assertEquals("responseType", cmasInfo.getResponseType(),
+                SmsCbCmasInfo.CMAS_RESPONSE_TYPE_NONE);
+        assertEquals("severity", cmasInfo.getSeverity(), SmsCbCmasInfo.CMAS_SEVERITY_SEVERE);
+        assertEquals("urgency", cmasInfo.getUrgency(), SmsCbCmasInfo.CMAS_URGENCY_EXPECTED);
+        assertEquals("certainty", cmasInfo.getCertainty(), SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY);
+    }
+}
diff --git a/tests/src/com/android/cellbroadcastservice/GsmSmsCbMessageTest.java b/tests/src/com/android/cellbroadcastservice/GsmSmsCbMessageTest.java
index de80095..c39f650 100644
--- a/tests/src/com/android/cellbroadcastservice/GsmSmsCbMessageTest.java
+++ b/tests/src/com/android/cellbroadcastservice/GsmSmsCbMessageTest.java
@@ -19,12 +19,14 @@
 import android.telephony.CbGeoUtils;
 import android.telephony.SmsCbCmasInfo;
 import android.telephony.SmsCbEtwsInfo;
+import android.telephony.SmsCbLocation;
 import android.telephony.SmsCbMessage;
-import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.util.Log;
 
+import androidx.test.InstrumentationRegistry;
+
 import com.android.cellbroadcastservice.CbGeoUtils.Circle;
 import com.android.cellbroadcastservice.CbGeoUtils.Polygon;
 
@@ -34,6 +36,7 @@
 import org.junit.runner.RunWith;
 
 import java.util.List;
+import java.util.Random;
 
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
@@ -41,6 +44,9 @@
 
     private static final String TAG = "GsmSmsCbMessageTest";
 
+    private static final SmsCbLocation TEST_LOCATION = new SmsCbLocation("94040", 1234, 5678);
+
+
     @Before
     public void setUp() throws Exception {
         super.setUp();
@@ -52,7 +58,6 @@
     }
 
     @Test
-    @SmallTest
     public void testGetEtwsPrimaryMessage() {
         String testMessage1 = "Testmessage1";
         String testMessage2 = "Testmessage2";
@@ -93,7 +98,6 @@
     }
 
     @Test
-    @SmallTest
     public void testCreateMessageFromBinary() throws Exception {
         final byte[] pdu = hexStringToBytes("0111130F6A0101C8329BFD06559BD429E8FE96B3C92C101D9D9"
                 + "E83D27350B22E1C7EAFF234BDFCADB962AE9A6BCE06A1DCE57B0AD40241C3E73208147B81622E000"
@@ -158,7 +162,6 @@
     }
 
     @Test
-    @SmallTest
     public void testCreateTriggerMessage() throws Exception {
         final byte[] pdu = hexStringToBytes("0001113001010010C0111204D2");
         GsmSmsCbMessage.GeoFencingTriggerMessage triggerMessage =
@@ -172,4 +175,887 @@
         assertEquals(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL,
                 triggerMessage.cbIdentifiers.get(0).messageIdentifier);
     }
+
+    private SmsCbMessage createFromPdu(byte[] pdu) {
+        try {
+            SmsCbHeader header = new SmsCbHeader(pdu);
+            byte[][] pdus = new byte[1][];
+            pdus[0] = pdu;
+            return GsmSmsCbMessage.createSmsCbMessage(InstrumentationRegistry.getContext(), header,
+                    TEST_LOCATION, pdus, /* slotIndex */ 0);
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+    }
+
+    private void doTestGeographicalScopeValue(byte[] pdu, byte b, int expectedGs) {
+        pdu[0] = b;
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals("Unexpected geographical scope decoded", expectedGs, msg
+                .getGeographicalScope());
+    }
+
+    @Test
+    public void testCreateNullPdu() {
+        SmsCbMessage msg = createFromPdu(null);
+        assertNull("createFromPdu(byte[] with null pdu should return null", msg);
+    }
+
+    @Test
+    public void testCreateTooShortPdu() {
+        byte[] pdu = new byte[4];
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertNull("createFromPdu(byte[] with too short pdu should return null", msg);
+    }
+
+    @Test
+    public void testGetGeographicalScope() {
+        byte[] pdu = {
+                (byte) 0xC0, (byte) 0x00, (byte) 0x00, (byte) 0x32, (byte) 0x40, (byte) 0x11,
+                (byte) 0x41,
+                (byte) 0xD0, (byte) 0x71, (byte) 0xDA, (byte) 0x04, (byte) 0x91, (byte) 0xCB,
+                (byte) 0xE6,
+                (byte) 0x70, (byte) 0x9D, (byte) 0x4D, (byte) 0x07, (byte) 0x85, (byte) 0xD9,
+                (byte) 0x70,
+                (byte) 0x74, (byte) 0x58, (byte) 0x5C, (byte) 0xA6, (byte) 0x83, (byte) 0xDA,
+                (byte) 0xE5,
+                (byte) 0xF9, (byte) 0x3C, (byte) 0x7C, (byte) 0x2E, (byte) 0x83, (byte) 0xEE,
+                (byte) 0x69,
+                (byte) 0x3A, (byte) 0x1A, (byte) 0x34, (byte) 0x0E, (byte) 0xCB, (byte) 0xE5,
+                (byte) 0xE9,
+                (byte) 0xF0, (byte) 0xB9, (byte) 0x0C, (byte) 0x92, (byte) 0x97, (byte) 0xE9,
+                (byte) 0x75,
+                (byte) 0xB9, (byte) 0x1B, (byte) 0x04, (byte) 0x0F, (byte) 0x93, (byte) 0xC9,
+                (byte) 0x69,
+                (byte) 0xF7, (byte) 0xB9, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x00
+        };
+
+        doTestGeographicalScopeValue(pdu, (byte) 0x00,
+                SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE);
+        doTestGeographicalScopeValue(pdu, (byte) 0x40, SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE);
+        doTestGeographicalScopeValue(pdu, (byte) 0x80,
+                SmsCbMessage.GEOGRAPHICAL_SCOPE_LOCATION_AREA_WIDE);
+        doTestGeographicalScopeValue(pdu, (byte) 0xC0, SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE);
+    }
+
+    @Test
+    public void testGetGeographicalScopeUmts() {
+        byte[] pdu = {
+                (byte) 0x01, (byte) 0x00, (byte) 0x32, (byte) 0xC0, (byte) 0x00, (byte) 0x40,
+
+                (byte) 0x01,
+
+                (byte) 0x41, (byte) 0xD0, (byte) 0x71, (byte) 0xDA, (byte) 0x04, (byte) 0x91,
+                (byte) 0xCB, (byte) 0xE6, (byte) 0x70, (byte) 0x9D, (byte) 0x4D, (byte) 0x07,
+                (byte) 0x85, (byte) 0xD9, (byte) 0x70, (byte) 0x74, (byte) 0x58, (byte) 0x5C,
+                (byte) 0xA6, (byte) 0x83, (byte) 0xDA, (byte) 0xE5, (byte) 0xF9, (byte) 0x3C,
+                (byte) 0x7C, (byte) 0x2E, (byte) 0x83, (byte) 0xEE, (byte) 0x69, (byte) 0x3A,
+                (byte) 0x1A, (byte) 0x34, (byte) 0x0E, (byte) 0xCB, (byte) 0xE5, (byte) 0xE9,
+                (byte) 0xF0, (byte) 0xB9, (byte) 0x0C, (byte) 0x92, (byte) 0x97, (byte) 0xE9,
+                (byte) 0x75, (byte) 0xB9, (byte) 0x1B, (byte) 0x04, (byte) 0x0F, (byte) 0x93,
+                (byte) 0xC9, (byte) 0x69, (byte) 0xF7, (byte) 0xB9, (byte) 0xD1, (byte) 0x68,
+                (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3, (byte) 0xD1,
+                (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3,
+                (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46,
+                (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x00,
+
+                (byte) 0x34
+        };
+
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals("Unexpected geographical scope decoded",
+                SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE, msg.getGeographicalScope());
+    }
+
+    @Test
+    public void testGetMessageBody7Bit() {
+        byte[] pdu = {
+                (byte) 0xC0, (byte) 0x00, (byte) 0x00, (byte) 0x32, (byte) 0x40, (byte) 0x11,
+                (byte) 0x41,
+                (byte) 0xD0, (byte) 0x71, (byte) 0xDA, (byte) 0x04, (byte) 0x91, (byte) 0xCB,
+                (byte) 0xE6,
+                (byte) 0x70, (byte) 0x9D, (byte) 0x4D, (byte) 0x07, (byte) 0x85, (byte) 0xD9,
+                (byte) 0x70,
+                (byte) 0x74, (byte) 0x58, (byte) 0x5C, (byte) 0xA6, (byte) 0x83, (byte) 0xDA,
+                (byte) 0xE5,
+                (byte) 0xF9, (byte) 0x3C, (byte) 0x7C, (byte) 0x2E, (byte) 0x83, (byte) 0xEE,
+                (byte) 0x69,
+                (byte) 0x3A, (byte) 0x1A, (byte) 0x34, (byte) 0x0E, (byte) 0xCB, (byte) 0xE5,
+                (byte) 0xE9,
+                (byte) 0xF0, (byte) 0xB9, (byte) 0x0C, (byte) 0x92, (byte) 0x97, (byte) 0xE9,
+                (byte) 0x75,
+                (byte) 0xB9, (byte) 0x1B, (byte) 0x04, (byte) 0x0F, (byte) 0x93, (byte) 0xC9,
+                (byte) 0x69,
+                (byte) 0xF7, (byte) 0xB9, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x00
+        };
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals("Unexpected 7-bit string decoded",
+                "A GSM default alphabet message with carriage return padding",
+                msg.getMessageBody());
+    }
+
+    @Test
+    public void testGetMessageBody7BitUmts() {
+        byte[] pdu = {
+                (byte) 0x01, (byte) 0x00, (byte) 0x32, (byte) 0xC0, (byte) 0x00, (byte) 0x40,
+
+                (byte) 0x01,
+
+                (byte) 0x41, (byte) 0xD0, (byte) 0x71, (byte) 0xDA, (byte) 0x04, (byte) 0x91,
+                (byte) 0xCB, (byte) 0xE6, (byte) 0x70, (byte) 0x9D, (byte) 0x4D, (byte) 0x07,
+                (byte) 0x85, (byte) 0xD9, (byte) 0x70, (byte) 0x74, (byte) 0x58, (byte) 0x5C,
+                (byte) 0xA6, (byte) 0x83, (byte) 0xDA, (byte) 0xE5, (byte) 0xF9, (byte) 0x3C,
+                (byte) 0x7C, (byte) 0x2E, (byte) 0x83, (byte) 0xEE, (byte) 0x69, (byte) 0x3A,
+                (byte) 0x1A, (byte) 0x34, (byte) 0x0E, (byte) 0xCB, (byte) 0xE5, (byte) 0xE9,
+                (byte) 0xF0, (byte) 0xB9, (byte) 0x0C, (byte) 0x92, (byte) 0x97, (byte) 0xE9,
+                (byte) 0x75, (byte) 0xB9, (byte) 0x1B, (byte) 0x04, (byte) 0x0F, (byte) 0x93,
+                (byte) 0xC9, (byte) 0x69, (byte) 0xF7, (byte) 0xB9, (byte) 0xD1, (byte) 0x68,
+                (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3, (byte) 0xD1,
+                (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3,
+                (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46,
+                (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x00,
+
+                (byte) 0x34
+        };
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals("Unexpected 7-bit string decoded",
+                "A GSM default alphabet message with carriage return padding",
+                msg.getMessageBody());
+    }
+
+    @Test
+    public void testGetMessageBody7BitMultipageUmts() {
+        byte[] pdu = {
+                (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0xC0, (byte) 0x00, (byte) 0x40,
+
+                (byte) 0x02,
+
+                (byte) 0xC6, (byte) 0xB4, (byte) 0x7C, (byte) 0x4E, (byte) 0x07, (byte) 0xC1,
+                (byte) 0xC3, (byte) 0xE7, (byte) 0xF2, (byte) 0xAA, (byte) 0xD1, (byte) 0x68,
+                (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3, (byte) 0xD1,
+                (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3,
+                (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46,
+                (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D, (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34,
+                (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68,
+                (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3, (byte) 0xD1,
+                (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3,
+                (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46,
+                (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x00,
+
+                (byte) 0x0A,
+
+                (byte) 0xD3, (byte) 0xF2, (byte) 0xF8, (byte) 0xED, (byte) 0x26, (byte) 0x83,
+                (byte) 0xE0, (byte) 0xE1, (byte) 0x73, (byte) 0xB9, (byte) 0xD1, (byte) 0x68,
+                (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3, (byte) 0xD1,
+                (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3,
+                (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46,
+                (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D, (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34,
+                (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68,
+                (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3, (byte) 0xD1,
+                (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3,
+                (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46,
+                (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x00,
+
+                (byte) 0x0A
+        };
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals("Unexpected multipage 7-bit string decoded",
+                "First page+Second page",
+                msg.getMessageBody());
+    }
+
+    @Test
+    public void testGetMessageBody7BitFull() {
+        byte[] pdu = {
+                (byte) 0xC0, (byte) 0x00, (byte) 0x00, (byte) 0x32, (byte) 0x40, (byte) 0x11,
+                (byte) 0x41,
+                (byte) 0xD0, (byte) 0x71, (byte) 0xDA, (byte) 0x04, (byte) 0x91, (byte) 0xCB,
+                (byte) 0xE6,
+                (byte) 0x70, (byte) 0x9D, (byte) 0x4D, (byte) 0x07, (byte) 0x85, (byte) 0xD9,
+                (byte) 0x70,
+                (byte) 0x74, (byte) 0x58, (byte) 0x5C, (byte) 0xA6, (byte) 0x83, (byte) 0xDA,
+                (byte) 0xE5,
+                (byte) 0xF9, (byte) 0x3C, (byte) 0x7C, (byte) 0x2E, (byte) 0x83, (byte) 0xC4,
+                (byte) 0xE5,
+                (byte) 0xB4, (byte) 0xFB, (byte) 0x0C, (byte) 0x2A, (byte) 0xE3, (byte) 0xC3,
+                (byte) 0x63,
+                (byte) 0x3A, (byte) 0x3B, (byte) 0x0F, (byte) 0xCA, (byte) 0xCD, (byte) 0x40,
+                (byte) 0x63,
+                (byte) 0x74, (byte) 0x58, (byte) 0x1E, (byte) 0x1E, (byte) 0xD3, (byte) 0xCB,
+                (byte) 0xF2,
+                (byte) 0x39, (byte) 0x88, (byte) 0xFD, (byte) 0x76, (byte) 0x9F, (byte) 0x59,
+                (byte) 0xA0,
+                (byte) 0x76, (byte) 0x39, (byte) 0xEC, (byte) 0x4E, (byte) 0xBB, (byte) 0xCF,
+                (byte) 0x20,
+                (byte) 0x3A, (byte) 0xBA, (byte) 0x2C, (byte) 0x2F, (byte) 0x83, (byte) 0xD2,
+                (byte) 0x73,
+                (byte) 0x90, (byte) 0xFB, (byte) 0x0D, (byte) 0x82, (byte) 0x87, (byte) 0xC9,
+                (byte) 0xE4,
+                (byte) 0xB4, (byte) 0xFB, (byte) 0x1C, (byte) 0x02
+        };
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals(
+                "Unexpected 7-bit string decoded",
+                "A GSM default alphabet message being exactly 93 characters long, "
+                        + "meaning there is no padding!",
+                msg.getMessageBody());
+    }
+
+    @Test
+    public void testGetMessageBody7BitFullUmts() {
+        byte[] pdu = {
+                (byte) 0x01, (byte) 0x00, (byte) 0x32, (byte) 0xC0, (byte) 0x00, (byte) 0x40,
+
+                (byte) 0x01,
+
+                (byte) 0x41, (byte) 0xD0, (byte) 0x71, (byte) 0xDA, (byte) 0x04, (byte) 0x91,
+                (byte) 0xCB, (byte) 0xE6, (byte) 0x70, (byte) 0x9D, (byte) 0x4D, (byte) 0x07,
+                (byte) 0x85, (byte) 0xD9, (byte) 0x70, (byte) 0x74, (byte) 0x58, (byte) 0x5C,
+                (byte) 0xA6, (byte) 0x83, (byte) 0xDA, (byte) 0xE5, (byte) 0xF9, (byte) 0x3C,
+                (byte) 0x7C, (byte) 0x2E, (byte) 0x83, (byte) 0xC4, (byte) 0xE5, (byte) 0xB4,
+                (byte) 0xFB, (byte) 0x0C, (byte) 0x2A, (byte) 0xE3, (byte) 0xC3, (byte) 0x63,
+                (byte) 0x3A, (byte) 0x3B, (byte) 0x0F, (byte) 0xCA, (byte) 0xCD, (byte) 0x40,
+                (byte) 0x63, (byte) 0x74, (byte) 0x58, (byte) 0x1E, (byte) 0x1E, (byte) 0xD3,
+                (byte) 0xCB, (byte) 0xF2, (byte) 0x39, (byte) 0x88, (byte) 0xFD, (byte) 0x76,
+                (byte) 0x9F, (byte) 0x59, (byte) 0xA0, (byte) 0x76, (byte) 0x39, (byte) 0xEC,
+                (byte) 0x4E, (byte) 0xBB, (byte) 0xCF, (byte) 0x20, (byte) 0x3A, (byte) 0xBA,
+                (byte) 0x2C, (byte) 0x2F, (byte) 0x83, (byte) 0xD2, (byte) 0x73, (byte) 0x90,
+                (byte) 0xFB, (byte) 0x0D, (byte) 0x82, (byte) 0x87, (byte) 0xC9, (byte) 0xE4,
+                (byte) 0xB4, (byte) 0xFB, (byte) 0x1C, (byte) 0x02,
+
+                (byte) 0x52
+        };
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals(
+                "Unexpected 7-bit string decoded",
+                "A GSM default alphabet message being exactly 93 characters long, "
+                        + "meaning there is no padding!",
+                msg.getMessageBody());
+    }
+
+    @Test
+    public void testGetMessageBody7BitWithLanguage() {
+        byte[] pdu = {
+                (byte) 0xC0, (byte) 0x00, (byte) 0x00, (byte) 0x32, (byte) 0x04, (byte) 0x11,
+                (byte) 0x41,
+                (byte) 0xD0, (byte) 0x71, (byte) 0xDA, (byte) 0x04, (byte) 0x91, (byte) 0xCB,
+                (byte) 0xE6,
+                (byte) 0x70, (byte) 0x9D, (byte) 0x4D, (byte) 0x07, (byte) 0x85, (byte) 0xD9,
+                (byte) 0x70,
+                (byte) 0x74, (byte) 0x58, (byte) 0x5C, (byte) 0xA6, (byte) 0x83, (byte) 0xDA,
+                (byte) 0xE5,
+                (byte) 0xF9, (byte) 0x3C, (byte) 0x7C, (byte) 0x2E, (byte) 0x83, (byte) 0xEE,
+                (byte) 0x69,
+                (byte) 0x3A, (byte) 0x1A, (byte) 0x34, (byte) 0x0E, (byte) 0xCB, (byte) 0xE5,
+                (byte) 0xE9,
+                (byte) 0xF0, (byte) 0xB9, (byte) 0x0C, (byte) 0x92, (byte) 0x97, (byte) 0xE9,
+                (byte) 0x75,
+                (byte) 0xB9, (byte) 0x1B, (byte) 0x04, (byte) 0x0F, (byte) 0x93, (byte) 0xC9,
+                (byte) 0x69,
+                (byte) 0xF7, (byte) 0xB9, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x00
+        };
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals("Unexpected 7-bit string decoded",
+                "A GSM default alphabet message with carriage return padding",
+                msg.getMessageBody());
+
+        assertEquals("Unexpected language indicator decoded", "es", msg.getLanguageCode());
+    }
+
+    @Test
+    public void testGetMessageBody7BitWithLanguageInBody() {
+        byte[] pdu = {
+                (byte) 0xC0, (byte) 0x00, (byte) 0x00, (byte) 0x32, (byte) 0x10, (byte) 0x11,
+                (byte) 0x73,
+                (byte) 0x7B, (byte) 0x23, (byte) 0x08, (byte) 0x3A, (byte) 0x4E, (byte) 0x9B,
+                (byte) 0x20,
+                (byte) 0x72, (byte) 0xD9, (byte) 0x1C, (byte) 0xAE, (byte) 0xB3, (byte) 0xE9,
+                (byte) 0xA0,
+                (byte) 0x30, (byte) 0x1B, (byte) 0x8E, (byte) 0x0E, (byte) 0x8B, (byte) 0xCB,
+                (byte) 0x74,
+                (byte) 0x50, (byte) 0xBB, (byte) 0x3C, (byte) 0x9F, (byte) 0x87, (byte) 0xCF,
+                (byte) 0x65,
+                (byte) 0xD0, (byte) 0x3D, (byte) 0x4D, (byte) 0x47, (byte) 0x83, (byte) 0xC6,
+                (byte) 0x61,
+                (byte) 0xB9, (byte) 0x3C, (byte) 0x1D, (byte) 0x3E, (byte) 0x97, (byte) 0x41,
+                (byte) 0xF2,
+                (byte) 0x32, (byte) 0xBD, (byte) 0x2E, (byte) 0x77, (byte) 0x83, (byte) 0xE0,
+                (byte) 0x61,
+                (byte) 0x32, (byte) 0x39, (byte) 0xED, (byte) 0x3E, (byte) 0x37, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x00
+        };
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals("Unexpected 7-bit string decoded",
+                "A GSM default alphabet message with carriage return padding",
+                msg.getMessageBody());
+
+        assertEquals("Unexpected language indicator decoded", "sv", msg.getLanguageCode());
+    }
+
+    @Test
+    public void testGetMessageBody7BitWithLanguageInBodyUmts() {
+        byte[] pdu = {
+                (byte) 0x01, (byte) 0x00, (byte) 0x32, (byte) 0xC0, (byte) 0x00, (byte) 0x10,
+
+                (byte) 0x01,
+
+                (byte) 0x73, (byte) 0x7B, (byte) 0x23, (byte) 0x08, (byte) 0x3A, (byte) 0x4E,
+                (byte) 0x9B, (byte) 0x20, (byte) 0x72, (byte) 0xD9, (byte) 0x1C, (byte) 0xAE,
+                (byte) 0xB3, (byte) 0xE9, (byte) 0xA0, (byte) 0x30, (byte) 0x1B, (byte) 0x8E,
+                (byte) 0x0E, (byte) 0x8B, (byte) 0xCB, (byte) 0x74, (byte) 0x50, (byte) 0xBB,
+                (byte) 0x3C, (byte) 0x9F, (byte) 0x87, (byte) 0xCF, (byte) 0x65, (byte) 0xD0,
+                (byte) 0x3D, (byte) 0x4D, (byte) 0x47, (byte) 0x83, (byte) 0xC6, (byte) 0x61,
+                (byte) 0xB9, (byte) 0x3C, (byte) 0x1D, (byte) 0x3E, (byte) 0x97, (byte) 0x41,
+                (byte) 0xF2, (byte) 0x32, (byte) 0xBD, (byte) 0x2E, (byte) 0x77, (byte) 0x83,
+                (byte) 0xE0, (byte) 0x61, (byte) 0x32, (byte) 0x39, (byte) 0xED, (byte) 0x3E,
+                (byte) 0x37, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3, (byte) 0xD1,
+                (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3,
+                (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46,
+                (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x00,
+
+                (byte) 0x37
+        };
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals("Unexpected 7-bit string decoded",
+                "A GSM default alphabet message with carriage return padding",
+                msg.getMessageBody());
+
+        assertEquals("Unexpected language indicator decoded", "sv", msg.getLanguageCode());
+    }
+
+    @Test
+    public void testGetMessageBody8Bit() {
+        byte[] pdu = {
+                (byte) 0xC0, (byte) 0x00, (byte) 0x00, (byte) 0x32, (byte) 0x44, (byte) 0x11,
+                (byte) 0x41,
+                (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47,
+                (byte) 0x41,
+                (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47,
+                (byte) 0x41,
+                (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47,
+                (byte) 0x41,
+                (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47,
+                (byte) 0x41,
+                (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47,
+                (byte) 0x41,
+                (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47,
+                (byte) 0x41,
+                (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47,
+                (byte) 0x41,
+                (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47,
+                (byte) 0x41,
+                (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47,
+                (byte) 0x41,
+                (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47,
+                (byte) 0x41,
+                (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47,
+                (byte) 0x41,
+                (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45
+        };
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals("8-bit message body should be empty", "", msg.getMessageBody());
+    }
+
+    @Test
+    public void testGetMessageBodyUcs2() {
+        byte[] pdu = {
+                (byte) 0xC0, (byte) 0x00, (byte) 0x00, (byte) 0x32, (byte) 0x48, (byte) 0x11,
+                (byte) 0x00,
+                (byte) 0x41, (byte) 0x00, (byte) 0x20, (byte) 0x00, (byte) 0x55, (byte) 0x00,
+                (byte) 0x43,
+                (byte) 0x00, (byte) 0x53, (byte) 0x00, (byte) 0x32, (byte) 0x00, (byte) 0x20,
+                (byte) 0x00,
+                (byte) 0x6D, (byte) 0x00, (byte) 0x65, (byte) 0x00, (byte) 0x73, (byte) 0x00,
+                (byte) 0x73,
+                (byte) 0x00, (byte) 0x61, (byte) 0x00, (byte) 0x67, (byte) 0x00, (byte) 0x65,
+                (byte) 0x00,
+                (byte) 0x20, (byte) 0x00, (byte) 0x63, (byte) 0x00, (byte) 0x6F, (byte) 0x00,
+                (byte) 0x6E,
+                (byte) 0x00, (byte) 0x74, (byte) 0x00, (byte) 0x61, (byte) 0x00, (byte) 0x69,
+                (byte) 0x00,
+                (byte) 0x6E, (byte) 0x00, (byte) 0x69, (byte) 0x00, (byte) 0x6E, (byte) 0x00,
+                (byte) 0x67,
+                (byte) 0x00, (byte) 0x20, (byte) 0x00, (byte) 0x61, (byte) 0x00, (byte) 0x20,
+                (byte) 0x04,
+                (byte) 0x34, (byte) 0x00, (byte) 0x20, (byte) 0x00, (byte) 0x63, (byte) 0x00,
+                (byte) 0x68,
+                (byte) 0x00, (byte) 0x61, (byte) 0x00, (byte) 0x72, (byte) 0x00, (byte) 0x61,
+                (byte) 0x00,
+                (byte) 0x63, (byte) 0x00, (byte) 0x74, (byte) 0x00, (byte) 0x65, (byte) 0x00,
+                (byte) 0x72,
+                (byte) 0x00, (byte) 0x0D, (byte) 0x00, (byte) 0x0D
+        };
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals("Unexpected 7-bit string decoded",
+                "A UCS2 message containing a \u0434 character", msg.getMessageBody());
+    }
+
+    @Test
+    public void testGetMessageBodyUcs2Umts() {
+        byte[] pdu = {
+                (byte) 0x01, (byte) 0x00, (byte) 0x32, (byte) 0xC0, (byte) 0x00, (byte) 0x48,
+
+                (byte) 0x01,
+
+                (byte) 0x00, (byte) 0x41, (byte) 0x00, (byte) 0x20, (byte) 0x00, (byte) 0x55,
+                (byte) 0x00, (byte) 0x43, (byte) 0x00, (byte) 0x53, (byte) 0x00, (byte) 0x32,
+                (byte) 0x00, (byte) 0x20, (byte) 0x00, (byte) 0x6D, (byte) 0x00, (byte) 0x65,
+                (byte) 0x00, (byte) 0x73, (byte) 0x00, (byte) 0x73, (byte) 0x00, (byte) 0x61,
+                (byte) 0x00, (byte) 0x67, (byte) 0x00, (byte) 0x65, (byte) 0x00, (byte) 0x20,
+                (byte) 0x00, (byte) 0x63, (byte) 0x00, (byte) 0x6F, (byte) 0x00, (byte) 0x6E,
+                (byte) 0x00, (byte) 0x74, (byte) 0x00, (byte) 0x61, (byte) 0x00, (byte) 0x69,
+                (byte) 0x00, (byte) 0x6E, (byte) 0x00, (byte) 0x69, (byte) 0x00, (byte) 0x6E,
+                (byte) 0x00, (byte) 0x67, (byte) 0x00, (byte) 0x20, (byte) 0x00, (byte) 0x61,
+                (byte) 0x00, (byte) 0x20, (byte) 0x04, (byte) 0x34, (byte) 0x00, (byte) 0x20,
+                (byte) 0x00, (byte) 0x63, (byte) 0x00, (byte) 0x68, (byte) 0x00, (byte) 0x61,
+                (byte) 0x00, (byte) 0x72, (byte) 0x00, (byte) 0x61, (byte) 0x00, (byte) 0x63,
+                (byte) 0x00, (byte) 0x74, (byte) 0x00, (byte) 0x65, (byte) 0x00, (byte) 0x72,
+                (byte) 0x00, (byte) 0x0D, (byte) 0x00, (byte) 0x0D,
+
+                (byte) 0x4E
+        };
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals("Unexpected 7-bit string decoded",
+                "A UCS2 message containing a \u0434 character", msg.getMessageBody());
+    }
+
+    @Test
+    public void testGetMessageBodyUcs2MultipageUmts() {
+        byte[] pdu = {
+                (byte) 0x01, (byte) 0x00, (byte) 0x32, (byte) 0xC0, (byte) 0x00, (byte) 0x48,
+
+                (byte) 0x02,
+
+                (byte) 0x00, (byte) 0x41, (byte) 0x00, (byte) 0x41, (byte) 0x00, (byte) 0x41,
+                (byte) 0x00, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+
+                (byte) 0x06,
+
+                (byte) 0x00, (byte) 0x42, (byte) 0x00, (byte) 0x42, (byte) 0x00, (byte) 0x42,
+                (byte) 0x00, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+                (byte) 0x0D, (byte) 0x0D, (byte) 0x0D, (byte) 0x0D,
+
+                (byte) 0x06
+        };
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals("Unexpected multipage UCS2 string decoded",
+                "AAABBB", msg.getMessageBody());
+    }
+
+    @Test
+    public void testGetMessageBodyUcs2WithLanguageInBody() {
+        byte[] pdu = {
+                (byte) 0xC0, (byte) 0x00, (byte) 0x00, (byte) 0x32, (byte) 0x11, (byte) 0x11,
+                (byte) 0x78,
+                (byte) 0x3C, (byte) 0x00, (byte) 0x41, (byte) 0x00, (byte) 0x20, (byte) 0x00,
+                (byte) 0x55,
+                (byte) 0x00, (byte) 0x43, (byte) 0x00, (byte) 0x53, (byte) 0x00, (byte) 0x32,
+                (byte) 0x00,
+                (byte) 0x20, (byte) 0x00, (byte) 0x6D, (byte) 0x00, (byte) 0x65, (byte) 0x00,
+                (byte) 0x73,
+                (byte) 0x00, (byte) 0x73, (byte) 0x00, (byte) 0x61, (byte) 0x00, (byte) 0x67,
+                (byte) 0x00,
+                (byte) 0x65, (byte) 0x00, (byte) 0x20, (byte) 0x00, (byte) 0x63, (byte) 0x00,
+                (byte) 0x6F,
+                (byte) 0x00, (byte) 0x6E, (byte) 0x00, (byte) 0x74, (byte) 0x00, (byte) 0x61,
+                (byte) 0x00,
+                (byte) 0x69, (byte) 0x00, (byte) 0x6E, (byte) 0x00, (byte) 0x69, (byte) 0x00,
+                (byte) 0x6E,
+                (byte) 0x00, (byte) 0x67, (byte) 0x00, (byte) 0x20, (byte) 0x00, (byte) 0x61,
+                (byte) 0x00,
+                (byte) 0x20, (byte) 0x04, (byte) 0x34, (byte) 0x00, (byte) 0x20, (byte) 0x00,
+                (byte) 0x63,
+                (byte) 0x00, (byte) 0x68, (byte) 0x00, (byte) 0x61, (byte) 0x00, (byte) 0x72,
+                (byte) 0x00,
+                (byte) 0x61, (byte) 0x00, (byte) 0x63, (byte) 0x00, (byte) 0x74, (byte) 0x00,
+                (byte) 0x65,
+                (byte) 0x00, (byte) 0x72, (byte) 0x00, (byte) 0x0D
+        };
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals("Unexpected 7-bit string decoded",
+                "A UCS2 message containing a \u0434 character", msg.getMessageBody());
+
+        assertEquals("Unexpected language indicator decoded", "xx", msg.getLanguageCode());
+    }
+
+    @Test
+    public void testGetMessageBodyUcs2WithLanguageInBodyUmts() {
+        byte[] pdu = {
+                (byte) 0x01, (byte) 0x00, (byte) 0x32, (byte) 0xC0, (byte) 0x00, (byte) 0x11,
+
+                (byte) 0x01,
+
+                (byte) 0x78, (byte) 0x3C, (byte) 0x00, (byte) 0x41, (byte) 0x00, (byte) 0x20,
+                (byte) 0x00, (byte) 0x55, (byte) 0x00, (byte) 0x43, (byte) 0x00, (byte) 0x53,
+                (byte) 0x00, (byte) 0x32, (byte) 0x00, (byte) 0x20, (byte) 0x00, (byte) 0x6D,
+                (byte) 0x00, (byte) 0x65, (byte) 0x00, (byte) 0x73, (byte) 0x00, (byte) 0x73,
+                (byte) 0x00, (byte) 0x61, (byte) 0x00, (byte) 0x67, (byte) 0x00, (byte) 0x65,
+                (byte) 0x00, (byte) 0x20, (byte) 0x00, (byte) 0x63, (byte) 0x00, (byte) 0x6F,
+                (byte) 0x00, (byte) 0x6E, (byte) 0x00, (byte) 0x74, (byte) 0x00, (byte) 0x61,
+                (byte) 0x00, (byte) 0x69, (byte) 0x00, (byte) 0x6E, (byte) 0x00, (byte) 0x69,
+                (byte) 0x00, (byte) 0x6E, (byte) 0x00, (byte) 0x67, (byte) 0x00, (byte) 0x20,
+                (byte) 0x00, (byte) 0x61, (byte) 0x00, (byte) 0x20, (byte) 0x04, (byte) 0x34,
+                (byte) 0x00, (byte) 0x20, (byte) 0x00, (byte) 0x63, (byte) 0x00, (byte) 0x68,
+                (byte) 0x00, (byte) 0x61, (byte) 0x00, (byte) 0x72, (byte) 0x00, (byte) 0x61,
+                (byte) 0x00, (byte) 0x63, (byte) 0x00, (byte) 0x74, (byte) 0x00, (byte) 0x65,
+                (byte) 0x00, (byte) 0x72, (byte) 0x00, (byte) 0x0D,
+
+                (byte) 0x50
+        };
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals("Unexpected 7-bit string decoded",
+                "A UCS2 message containing a \u0434 character", msg.getMessageBody());
+
+        assertEquals("Unexpected language indicator decoded", "xx", msg.getLanguageCode());
+    }
+
+    @Test
+    public void testGetMessageIdentifier() {
+        byte[] pdu = {
+                (byte) 0xC0, (byte) 0x00, (byte) 0x30, (byte) 0x39, (byte) 0x40, (byte) 0x11,
+                (byte) 0x41,
+                (byte) 0xD0, (byte) 0x71, (byte) 0xDA, (byte) 0x04, (byte) 0x91, (byte) 0xCB,
+                (byte) 0xE6,
+                (byte) 0x70, (byte) 0x9D, (byte) 0x4D, (byte) 0x07, (byte) 0x85, (byte) 0xD9,
+                (byte) 0x70,
+                (byte) 0x74, (byte) 0x58, (byte) 0x5C, (byte) 0xA6, (byte) 0x83, (byte) 0xDA,
+                (byte) 0xE5,
+                (byte) 0xF9, (byte) 0x3C, (byte) 0x7C, (byte) 0x2E, (byte) 0x83, (byte) 0xEE,
+                (byte) 0x69,
+                (byte) 0x3A, (byte) 0x1A, (byte) 0x34, (byte) 0x0E, (byte) 0xCB, (byte) 0xE5,
+                (byte) 0xE9,
+                (byte) 0xF0, (byte) 0xB9, (byte) 0x0C, (byte) 0x92, (byte) 0x97, (byte) 0xE9,
+                (byte) 0x75,
+                (byte) 0xB9, (byte) 0x1B, (byte) 0x04, (byte) 0x0F, (byte) 0x93, (byte) 0xC9,
+                (byte) 0x69,
+                (byte) 0xF7, (byte) 0xB9, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x00
+        };
+
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals("Unexpected message identifier decoded", 12345, msg.getServiceCategory());
+    }
+
+    @Test
+    public void testGetMessageIdentifierUmts() {
+        byte[] pdu = {
+                (byte) 0x01, (byte) 0x30, (byte) 0x39, (byte) 0x2A, (byte) 0xA5, (byte) 0x40,
+
+                (byte) 0x01,
+
+                (byte) 0x41, (byte) 0xD0, (byte) 0x71, (byte) 0xDA, (byte) 0x04, (byte) 0x91,
+                (byte) 0xCB, (byte) 0xE6, (byte) 0x70, (byte) 0x9D, (byte) 0x4D, (byte) 0x07,
+                (byte) 0x85, (byte) 0xD9, (byte) 0x70, (byte) 0x74, (byte) 0x58, (byte) 0x5C,
+                (byte) 0xA6, (byte) 0x83, (byte) 0xDA, (byte) 0xE5, (byte) 0xF9, (byte) 0x3C,
+                (byte) 0x7C, (byte) 0x2E, (byte) 0x83, (byte) 0xEE, (byte) 0x69, (byte) 0x3A,
+                (byte) 0x1A, (byte) 0x34, (byte) 0x0E, (byte) 0xCB, (byte) 0xE5, (byte) 0xE9,
+                (byte) 0xF0, (byte) 0xB9, (byte) 0x0C, (byte) 0x92, (byte) 0x97, (byte) 0xE9,
+                (byte) 0x75, (byte) 0xB9, (byte) 0x1B, (byte) 0x04, (byte) 0x0F, (byte) 0x93,
+                (byte) 0xC9, (byte) 0x69, (byte) 0xF7, (byte) 0xB9, (byte) 0xD1, (byte) 0x68,
+                (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3, (byte) 0xD1,
+                (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3,
+                (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46,
+                (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x00,
+
+                (byte) 0x34
+        };
+
+        SmsCbMessage msg = createFromPdu(pdu);
+
+        assertEquals("Unexpected message identifier decoded", 12345, msg.getServiceCategory());
+    }
+
+    @Test
+    public void testGetMessageCode() {
+        byte[] pdu = {
+                (byte) 0x2A, (byte) 0xA5, (byte) 0x30, (byte) 0x39, (byte) 0x40, (byte) 0x11,
+                (byte) 0x41,
+                (byte) 0xD0, (byte) 0x71, (byte) 0xDA, (byte) 0x04, (byte) 0x91, (byte) 0xCB,
+                (byte) 0xE6,
+                (byte) 0x70, (byte) 0x9D, (byte) 0x4D, (byte) 0x07, (byte) 0x85, (byte) 0xD9,
+                (byte) 0x70,
+                (byte) 0x74, (byte) 0x58, (byte) 0x5C, (byte) 0xA6, (byte) 0x83, (byte) 0xDA,
+                (byte) 0xE5,
+                (byte) 0xF9, (byte) 0x3C, (byte) 0x7C, (byte) 0x2E, (byte) 0x83, (byte) 0xEE,
+                (byte) 0x69,
+                (byte) 0x3A, (byte) 0x1A, (byte) 0x34, (byte) 0x0E, (byte) 0xCB, (byte) 0xE5,
+                (byte) 0xE9,
+                (byte) 0xF0, (byte) 0xB9, (byte) 0x0C, (byte) 0x92, (byte) 0x97, (byte) 0xE9,
+                (byte) 0x75,
+                (byte) 0xB9, (byte) 0x1B, (byte) 0x04, (byte) 0x0F, (byte) 0x93, (byte) 0xC9,
+                (byte) 0x69,
+                (byte) 0xF7, (byte) 0xB9, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x00
+        };
+
+        SmsCbMessage msg = createFromPdu(pdu);
+        int messageCode = (msg.getSerialNumber() & 0x3ff0) >> 4;
+
+        assertEquals("Unexpected message code decoded", 682, messageCode);
+    }
+
+    @Test
+    public void testGetMessageCodeUmts() {
+        byte[] pdu = {
+                (byte) 0x01, (byte) 0x30, (byte) 0x39, (byte) 0x2A, (byte) 0xA5, (byte) 0x40,
+
+                (byte) 0x01,
+
+                (byte) 0x41, (byte) 0xD0, (byte) 0x71, (byte) 0xDA, (byte) 0x04, (byte) 0x91,
+                (byte) 0xCB, (byte) 0xE6, (byte) 0x70, (byte) 0x9D, (byte) 0x4D, (byte) 0x07,
+                (byte) 0x85, (byte) 0xD9, (byte) 0x70, (byte) 0x74, (byte) 0x58, (byte) 0x5C,
+                (byte) 0xA6, (byte) 0x83, (byte) 0xDA, (byte) 0xE5, (byte) 0xF9, (byte) 0x3C,
+                (byte) 0x7C, (byte) 0x2E, (byte) 0x83, (byte) 0xEE, (byte) 0x69, (byte) 0x3A,
+                (byte) 0x1A, (byte) 0x34, (byte) 0x0E, (byte) 0xCB, (byte) 0xE5, (byte) 0xE9,
+                (byte) 0xF0, (byte) 0xB9, (byte) 0x0C, (byte) 0x92, (byte) 0x97, (byte) 0xE9,
+                (byte) 0x75, (byte) 0xB9, (byte) 0x1B, (byte) 0x04, (byte) 0x0F, (byte) 0x93,
+                (byte) 0xC9, (byte) 0x69, (byte) 0xF7, (byte) 0xB9, (byte) 0xD1, (byte) 0x68,
+                (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3, (byte) 0xD1,
+                (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3,
+                (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46,
+                (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x00,
+
+                (byte) 0x34
+        };
+
+        SmsCbMessage msg = createFromPdu(pdu);
+        int messageCode = (msg.getSerialNumber() & 0x3ff0) >> 4;
+
+        assertEquals("Unexpected message code decoded", 682, messageCode);
+    }
+
+    @Test
+    public void testGetUpdateNumber() {
+        byte[] pdu = {
+                (byte) 0x2A, (byte) 0xA5, (byte) 0x30, (byte) 0x39, (byte) 0x40, (byte) 0x11,
+                (byte) 0x41,
+                (byte) 0xD0, (byte) 0x71, (byte) 0xDA, (byte) 0x04, (byte) 0x91, (byte) 0xCB,
+                (byte) 0xE6,
+                (byte) 0x70, (byte) 0x9D, (byte) 0x4D, (byte) 0x07, (byte) 0x85, (byte) 0xD9,
+                (byte) 0x70,
+                (byte) 0x74, (byte) 0x58, (byte) 0x5C, (byte) 0xA6, (byte) 0x83, (byte) 0xDA,
+                (byte) 0xE5,
+                (byte) 0xF9, (byte) 0x3C, (byte) 0x7C, (byte) 0x2E, (byte) 0x83, (byte) 0xEE,
+                (byte) 0x69,
+                (byte) 0x3A, (byte) 0x1A, (byte) 0x34, (byte) 0x0E, (byte) 0xCB, (byte) 0xE5,
+                (byte) 0xE9,
+                (byte) 0xF0, (byte) 0xB9, (byte) 0x0C, (byte) 0x92, (byte) 0x97, (byte) 0xE9,
+                (byte) 0x75,
+                (byte) 0xB9, (byte) 0x1B, (byte) 0x04, (byte) 0x0F, (byte) 0x93, (byte) 0xC9,
+                (byte) 0x69,
+                (byte) 0xF7, (byte) 0xB9, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A,
+                (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x00
+        };
+
+        SmsCbMessage msg = createFromPdu(pdu);
+        int updateNumber = msg.getSerialNumber() & 0x000f;
+
+        assertEquals("Unexpected update number decoded", 5, updateNumber);
+    }
+
+    @Test
+    public void testGetUpdateNumberUmts() {
+        byte[] pdu = {
+                (byte) 0x01, (byte) 0x30, (byte) 0x39, (byte) 0x2A, (byte) 0xA5, (byte) 0x40,
+
+                (byte) 0x01,
+
+                (byte) 0x41, (byte) 0xD0, (byte) 0x71, (byte) 0xDA, (byte) 0x04, (byte) 0x91,
+                (byte) 0xCB, (byte) 0xE6, (byte) 0x70, (byte) 0x9D, (byte) 0x4D, (byte) 0x07,
+                (byte) 0x85, (byte) 0xD9, (byte) 0x70, (byte) 0x74, (byte) 0x58, (byte) 0x5C,
+                (byte) 0xA6, (byte) 0x83, (byte) 0xDA, (byte) 0xE5, (byte) 0xF9, (byte) 0x3C,
+                (byte) 0x7C, (byte) 0x2E, (byte) 0x83, (byte) 0xEE, (byte) 0x69, (byte) 0x3A,
+                (byte) 0x1A, (byte) 0x34, (byte) 0x0E, (byte) 0xCB, (byte) 0xE5, (byte) 0xE9,
+                (byte) 0xF0, (byte) 0xB9, (byte) 0x0C, (byte) 0x92, (byte) 0x97, (byte) 0xE9,
+                (byte) 0x75, (byte) 0xB9, (byte) 0x1B, (byte) 0x04, (byte) 0x0F, (byte) 0x93,
+                (byte) 0xC9, (byte) 0x69, (byte) 0xF7, (byte) 0xB9, (byte) 0xD1, (byte) 0x68,
+                (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3, (byte) 0xD1,
+                (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46, (byte) 0xA3,
+                (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D, (byte) 0x46,
+                (byte) 0xA3, (byte) 0xD1, (byte) 0x68, (byte) 0x34, (byte) 0x1A, (byte) 0x8D,
+                (byte) 0x46, (byte) 0xA3, (byte) 0xD1, (byte) 0x00,
+
+                (byte) 0x34
+        };
+
+        SmsCbMessage msg = createFromPdu(pdu);
+        int updateNumber = msg.getSerialNumber() & 0x000f;
+
+        assertEquals("Unexpected update number decoded", 5, updateNumber);
+    }
+
+    /* ETWS Test message including header */
+    private static final byte[] etwsMessageNormal = hexStringToBytes("000011001101"
+            + "0D0A5BAE57CE770C531790E85C716CBF3044573065B930675730"
+            + "9707767A751F30025F37304463FA308C306B5099304830664E0B30553044FF086C178C615E81FF09"
+            + "0000000000000000000000000000");
+
+    private static final byte[] etwsMessageCancel = hexStringToBytes("000011001101"
+            + "0D0A5148307B3069002800310030003A0035"
+            + "00320029306E7DCA602557309707901F5831309253D66D883057307E3059FF086C178C615E81FF09"
+            + "00000000000000000000000000000000000000000000");
+
+    private static final byte[] etwsMessageTest = hexStringToBytes("000011031101"
+            + "0D0A5BAE57CE770C531790E85C716CBF3044"
+            + "573065B9306757309707300263FA308C306B5099304830664E0B30553044FF086C178C615E81FF09"
+            + "00000000000000000000000000000000000000000000");
+
+    // FIXME: add example of ETWS primary notification PDU
+
+    @Test
+    public void testEtwsMessageNormal() {
+        SmsCbMessage msg = createFromPdu(etwsMessageNormal);
+        Log.d(TAG, msg.toString());
+        assertEquals("GS mismatch", 0, msg.getGeographicalScope());
+        assertEquals("serial number mismatch", 0, msg.getSerialNumber());
+        assertEquals("message ID mismatch", 0x1100, msg.getServiceCategory());
+        assertEquals("warning type mismatch", SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE,
+                msg.getEtwsWarningInfo().getWarningType());
+    }
+
+    @Test
+    public void testEtwsMessageCancel() {
+        SmsCbMessage msg = createFromPdu(etwsMessageCancel);
+        Log.d(TAG, msg.toString());
+        assertEquals("GS mismatch", 0, msg.getGeographicalScope());
+        assertEquals("serial number mismatch", 0, msg.getSerialNumber());
+        assertEquals("message ID mismatch", 0x1100, msg.getServiceCategory());
+        assertEquals("warning type mismatch", SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE,
+                msg.getEtwsWarningInfo().getWarningType());
+    }
+
+    @Test
+    public void testEtwsMessageTest() {
+        SmsCbMessage msg = createFromPdu(etwsMessageTest);
+        Log.d(TAG, msg.toString());
+        assertEquals("GS mismatch", 0, msg.getGeographicalScope());
+        assertEquals("serial number mismatch", 0, msg.getSerialNumber());
+        assertEquals("message ID mismatch", 0x1103, msg.getServiceCategory());
+        assertEquals("warning type mismatch", SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE,
+                msg.getEtwsWarningInfo().getWarningType());
+    }
+
+    // Make sure we don't throw an exception if we feed random data to the PDU parser.
+    @Test
+    public void testRandomPdus() {
+        Random r = new Random(94040);
+        for (int run = 0; run < 10000; run++) {
+            int len = r.nextInt(140);
+            byte[] data = new byte[len];
+            for (int i = 0; i < len; i++) {
+                data[i] = (byte) r.nextInt(256);
+            }
+            try {
+                // this should return a SmsCbMessage object or null for invalid data
+                SmsCbMessage msg = createFromPdu(data);
+            } catch (Exception e) {
+                Log.d(TAG, "exception thrown", e);
+                fail("Exception in decoder at run " + run + " length " + len + ": " + e);
+            }
+        }
+    }
 }