[automerger skipped] Clean up use of hidden APIs am: 4a2ff26de6 am: 11a4e13c0c
am: 7d5439d9e5 -s ours
am skip reason: change_id Iabf338fbf9213bcf69cac491ebe13e3437f98f04 with SHA1 8d7d0a10ca is in history

Change-Id: I7cdf30bfb9ed6dc8998052d9bc3b3a307a86dfe0
diff --git a/Android.bp b/Android.bp
index 5675fa2..3f82eeb 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,9 +1,10 @@
 // Copyright 2019 The Android Open Source Project
-
-android_app {
-    name: "CellBroadcastServiceModule",
+java_defaults {
+    name: "CellBroadcastServiceCommon",
+    min_sdk_version: "29",
     srcs: [
       "src/**/*.java",
+      ":framework-annotations",
       ":framework-cellbroadcast-shared-srcs",
     ],
     libs: ["telephony-common"],
@@ -14,6 +15,24 @@
     optimize: {
         proguard_flags_files: ["proguard.flags"],
     },
+    jarjar_rules: "cellbroadcast-jarjar-rules.txt",
+}
+
+android_app {
+    name: "CellBroadcastServiceModule",
+    defaults: ["CellBroadcastServiceCommon"],
+    certificate: "networkstack",
+    manifest: "AndroidManifest.xml",
+}
+
+android_app {
+     name: "CellBroadcastServiceModulePlatform",
+     defaults: ["CellBroadcastServiceCommon"],
+     certificate: "platform",
+     // CellBroadcastServicePlatformModule is a replacement for com.android.cellbroadcast apex
+     // which consists of CellBroadcastServiceModule
+     overrides: ["com.android.cellbroadcast"],
+     manifest: "AndroidManifest_Platform.xml",
 }
 
 // used to share common constants between cellbroadcastservice and cellbroadcastreceier
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 4dc0b97..cafdf25 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -17,7 +17,9 @@
  */
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        android:sharedUserId="android.uid.phone"
+        android:sharedUserId="android.uid.networkstack"
+        android:versionCode="300000000"
+        android:versionName="R-initial"
         package="com.android.cellbroadcastservice">
 
     <original-package android:name="com.android.cellbroadcastservice" />
@@ -29,15 +31,18 @@
     <uses-permission android:name="android.permission.READ_CELL_BROADCASTS" />
     <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.BROADCAST_SMS" />
 
     <uses-sdk android:minSdkVersion="29"/>
 
     <application android:label="Module used to handle cell broadcasts."
             android:defaultToDeviceProtectedStorage="true"
             android:directBootAware="true"
-            android:process="com.android.phone">
+            android:persistent="true"
+            android:process="com.android.networkstack.process">
 
         <service android:name="DefaultCellBroadcastService"
+                android:process="com.android.networkstack.process"
                 android:exported="true"
                 android:permission="android.permission.BIND_CELL_BROADCAST_SERVICE">
             <intent-filter>
diff --git a/AndroidManifest_Platform.xml b/AndroidManifest_Platform.xml
new file mode 100644
index 0000000..4ff8ed5
--- /dev/null
+++ b/AndroidManifest_Platform.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    android:sharedUserId="android.uid.phone"
+    package="com.android.cellbroadcastservice">
+
+    <original-package android:name="com.android.cellbroadcastservice" />
+
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.RECEIVE_SMS" />
+    <uses-permission android:name="android.permission.RECEIVE_EMERGENCY_BROADCAST" />
+    <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
+    <uses-permission android:name="android.permission.READ_CELL_BROADCASTS" />
+    <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.BROADCAST_SMS" />
+
+    <uses-sdk android:minSdkVersion="29"/>
+
+    <application android:label="Module used to handle cell broadcasts."
+        android:defaultToDeviceProtectedStorage="true"
+        android:persistent="true"
+        android:directBootAware="true"
+        android:process="com.android.phone">
+
+        <service android:name="DefaultCellBroadcastService"
+            android:process="com.android.phone"
+            android:exported="true"
+            android:permission="android.permission.BIND_CELL_BROADCAST_SERVICE">
+            <intent-filter>
+                <action android:name="android.telephony.CellBroadcastService" />
+            </intent-filter>
+        </service>
+    </application>
+</manifest>
diff --git a/CleanSpec.mk b/CleanSpec.mk
new file mode 100644
index 0000000..f69421d
--- /dev/null
+++ b/CleanSpec.mk
@@ -0,0 +1,50 @@
+# Copyright (C) 2007 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.
+#
+
+# If you don't need to do a full clean build but would like to touch
+# a file or delete some intermediate files, add a clean step to the end
+# of the list.  These steps will only be run once, if they haven't been
+# run before.
+#
+# E.g.:
+#     $(call add-clean-step, touch -c external/sqlite/sqlite3.h)
+#     $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libz_intermediates)
+#
+# Always use "touch -c" and "rm -f" or "rm -rf" to gracefully deal with
+# files that are missing or have been moved.
+#
+# Use $(PRODUCT_OUT) to get to the "out/target/product/blah/" directory.
+# Use $(OUT_DIR) to refer to the "out" directory.
+#
+# If you need to re-do something that's already mentioned, just copy
+# the command and add it to the bottom of the list.  E.g., if a change
+# that you made last week required touching a file and a change you
+# made today requires touching the same file, just copy the old
+# touch step and add it to the end of the list.
+#
+# ************************************************
+# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
+# ************************************************
+
+# For example:
+#$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/AndroidTests_intermediates)
+#$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/core_intermediates)
+#$(call add-clean-step, find $(OUT_DIR) -type f -name "IGTalkSession*" -print0 | xargs -0 rm -f)
+#$(call add-clean-step, rm -rf $(PRODUCT_OUT)/data/*)
+
+$(call add-clean-step, rm -rf $(PRODUCT_OUT)/system/priv-app/CellBroadcastServiceModule)
+# ************************************************
+# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
+# ************************************************
diff --git a/TEST_MAPPING b/TEST_MAPPING
new file mode 100644
index 0000000..77f6a76
--- /dev/null
+++ b/TEST_MAPPING
@@ -0,0 +1,17 @@
+{
+  "presubmit": [
+    {
+      "name": "CellBroadcastServiceTests",
+      "options": [
+        {
+          "include-annotation": "org.junit.Test"
+        }
+      ]
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "CellBroadcastServiceTests"
+    }
+  ]
+}
diff --git a/cellbroadcast-jarjar-rules.txt b/cellbroadcast-jarjar-rules.txt
index b50ec50..9d34267 100644
--- a/cellbroadcast-jarjar-rules.txt
+++ b/cellbroadcast-jarjar-rules.txt
@@ -1,4 +1 @@
-rule android.util.LocalLog* com.android.cellbroadcast.LocalLog@1
-rule android.util.Slog* com.android.cellbroadcast.Slog@1
-rule com.android.internal.util.State* com.android.cellbroadcastservice.State@1
-rule com.android.internal.util.StateMachine* com.android.cellbroadcastservice.StateMachine@1
+rule android.util.LocalLog* com.android.cellbroadcastservice.LocalLog@1
diff --git a/res/values-mcc310-mnc410/config.xml b/res/values-mcc310-mnc410/config.xml
index 2301d02..aed0016 100644
--- a/res/values-mcc310-mnc410/config.xml
+++ b/res/values-mcc310-mnc410/config.xml
@@ -15,6 +15,7 @@
 -->
 
 <resources>
-    <!-- Whether to reset alert message duplicate detection after toggling airplane mode -->
-    <bool name="reset_duplicate_detection_on_airplane_mode">true</bool>
+    <!-- Whether to reset alert message duplicate detection and geo-fencing check after
+    reboot or toggling airplane mode -->
+    <bool name="reset_on_power_cycle_or_airplane_mode">true</bool>
 </resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index f8e7112..4d640eb 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -30,6 +30,7 @@
     <!-- Whether to compare message body when deduping messages -->
     <bool name="duplicate_compare_body">false</bool>
 
-    <!-- Whether to reset alert message duplicate detection after toggling airplane mode -->
-    <bool name="reset_duplicate_detection_on_airplane_mode">false</bool>
+    <!-- Whether to reset alert message duplicate detection and geo-fencing check after
+    reboot or toggling airplane mode -->
+    <bool name="reset_on_power_cycle_or_airplane_mode">false</bool>
 </resources>
diff --git a/res/values/string.xml b/res/values/string.xml
new file mode 100644
index 0000000..3f4d44e
--- /dev/null
+++ b/res/values/string.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Primary ETWS (Earthquake and Tsunami Warning System) default message for earthquake -->
+    <string name="etws_primary_default_message_earthquake">Stay calm and seek shelter nearby.</string>
+
+    <!-- Primary ETWS (Earthquake and Tsunami Warning System) default message for Tsunami -->
+    <string name="etws_primary_default_message_tsunami">Evacuate immediately from coastal regions and riverside areas to a safer place such as high ground.</string>
+
+    <!-- Primary ETWS (Earthquake and Tsunami Warning System) default message for earthquake and Tsunami -->
+    <string name="etws_primary_default_message_earthquake_and_tsunami">Stay calm and seek shelter nearby.</string>
+
+    <!-- Primary ETWS (Earthquake and Tsunami Warning System) default message for test -->
+    <string name="etws_primary_default_message_test">Emergency messages test</string>
+
+    <!-- Primary ETWS (Earthquake and Tsunami Warning System) default message for others -->
+    <string name="etws_primary_default_message_others"></string>
+</resources>
diff --git a/res/values/symbols.xml b/res/values/symbols.xml
index 867c3a2..c2f50eb 100644
--- a/res/values/symbols.xml
+++ b/res/values/symbols.xml
@@ -22,5 +22,12 @@
       Can be referenced in java code as: com.android.internal.R.<type>.<name>
       and in layout xml as: "@*android:<type>/<name>" -->
 
-    <java-symbol type="array" name="config_defaultCellBroadcastReceiverPkgs" />
+	<java-symbol type="array" name="config_defaultCellBroadcastReceiverPkgs" />
+
+	<!-- ETWS primary messages -->
+	<java-symbol type="string" name="etws_primary_default_message_earthquake" />
+	<java-symbol type="string" name="etws_primary_default_message_tsunami" />
+	<java-symbol type="string" name="etws_primary_default_message_earthquake_and_tsunami" />
+	<java-symbol type="string" name="etws_primary_default_message_test" />
+	<java-symbol type="string" name="etws_primary_default_message_others" />
 </resources>
diff --git a/src/com/android/cellbroadcastservice/BearerData.java b/src/com/android/cellbroadcastservice/BearerData.java
index 584cd15..d73339c 100644
--- a/src/com/android/cellbroadcastservice/BearerData.java
+++ b/src/com/android/cellbroadcastservice/BearerData.java
@@ -16,14 +16,10 @@
 
 package com.android.cellbroadcastservice;
 
-import android.content.res.Resources;
+import android.content.Context;
+import android.telephony.Rlog;
 import android.telephony.SmsCbCmasInfo;
 import android.telephony.cdma.CdmaSmsCbProgramData;
-import android.util.Log;
-
-import com.android.internal.telephony.GsmAlphabet;
-import com.android.internal.telephony.SmsHeader;
-import com.android.internal.util.BitwiseInputStream;
 
 /**
  * An object to decode CDMA SMS bearer data.
@@ -36,22 +32,22 @@
      * (See 3GPP2 C.S0015-B, v2.0, table 4.5-1)
      * NOTE: Unneeded subparameter types are not included
      */
-    private final static byte SUBPARAM_MESSAGE_IDENTIFIER               = 0x00;
-    private final static byte SUBPARAM_USER_DATA                        = 0x01;
-    private final static byte SUBPARAM_PRIORITY_INDICATOR               = 0x08;
-    private final static byte SUBPARAM_LANGUAGE_INDICATOR               = 0x0D;
+    private static final byte SUBPARAM_MESSAGE_IDENTIFIER = 0x00;
+    private static final byte SUBPARAM_USER_DATA = 0x01;
+    private static final byte SUBPARAM_PRIORITY_INDICATOR = 0x08;
+    private static final byte SUBPARAM_LANGUAGE_INDICATOR = 0x0D;
 
     // All other values after this are reserved.
-    private final static byte SUBPARAM_ID_LAST_DEFINED                  = 0x17;
+    private static final byte SUBPARAM_ID_LAST_DEFINED = 0x17;
 
     /**
      * Supported priority modes for CDMA SMS messages
      * (See 3GPP2 C.S0015-B, v2.0, table 4.5.9-1)
      */
-    public static final int PRIORITY_NORMAL        = 0x0;
-    public static final int PRIORITY_INTERACTIVE   = 0x1;
-    public static final int PRIORITY_URGENT        = 0x2;
-    public static final int PRIORITY_EMERGENCY     = 0x3;
+    public static final int PRIORITY_NORMAL = 0x0;
+    public static final int PRIORITY_INTERACTIVE = 0x1;
+    public static final int PRIORITY_URGENT = 0x2;
+    public static final int PRIORITY_EMERGENCY = 0x3;
 
     /**
      * Language Indicator values.  NOTE: the spec (3GPP2 C.S0015-B,
@@ -60,14 +56,21 @@
      * It would seem reasonable to assume the values from C.R1001-F
      * (table 9.2-1) are to be used instead.
      */
-    public static final int LANGUAGE_UNKNOWN  = 0x00;
-    public static final int LANGUAGE_ENGLISH  = 0x01;
-    public static final int LANGUAGE_FRENCH   = 0x02;
-    public static final int LANGUAGE_SPANISH  = 0x03;
+    public static final int LANGUAGE_UNKNOWN = 0x00;
+    public static final int LANGUAGE_ENGLISH = 0x01;
+    public static final int LANGUAGE_FRENCH = 0x02;
+    public static final int LANGUAGE_SPANISH = 0x03;
     public static final int LANGUAGE_JAPANESE = 0x04;
-    public static final int LANGUAGE_KOREAN   = 0x05;
-    public static final int LANGUAGE_CHINESE  = 0x06;
-    public static final int LANGUAGE_HEBREW   = 0x07;
+    public static final int LANGUAGE_KOREAN = 0x05;
+    public static final int LANGUAGE_CHINESE = 0x06;
+    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.
@@ -105,6 +108,7 @@
 
     /**
      * CMAS warning notification information.
+     *
      * @see #decodeCmasUserData(BearerData, int)
      */
     public SmsCbCmasInfo cmasWarningInfo;
@@ -112,7 +116,8 @@
     /**
      * Construct an empty BearerData.
      */
-    private BearerData() {}
+    private BearerData() {
+    }
 
     private static class CodingException extends Exception {
         public CodingException(String s) {
@@ -122,6 +127,7 @@
 
     /**
      * Returns the language indicator as a two-character ISO 639 string.
+     *
      * @return a two character ISO 639 language code
      */
     public String getLanguage() {
@@ -130,6 +136,7 @@
 
     /**
      * Converts a CDMA language indicator value to an ISO 639 two character language code.
+     *
      * @param languageValue the CDMA language value to convert
      * @return the two character ISO 639 language code for the specified value, or null if unknown
      */
@@ -186,18 +193,17 @@
             bData.hasUserDataHeader = (inStream.read(1) == 1);
             inStream.skip(3);
         }
-        if ((! decodeSuccess) || (paramBits > 0)) {
-            Log.d(LOG_TAG, "MESSAGE_IDENTIFIER decode " +
-                    (decodeSuccess ? "succeeded" : "failed") +
-                    " (extra bits = " + paramBits + ")");
+        if ((!decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "MESSAGE_IDENTIFIER decode "
+                    + (decodeSuccess ? "succeeded" : "failed")
+                    + " (extra bits = " + paramBits + ")");
         }
         inStream.skip(paramBits);
         return decodeSuccess;
     }
 
     private static boolean decodeReserved(BitwiseInputStream inStream, int subparamId)
-            throws BitwiseInputStream.AccessException, CodingException
-    {
+            throws BitwiseInputStream.AccessException, CodingException {
         boolean decodeSuccess = false;
         int subparamLen = inStream.read(8); // SUBPARAM_LEN
         int paramBits = subparamLen * 8;
@@ -205,7 +211,7 @@
             decodeSuccess = true;
             inStream.skip(paramBits);
         }
-        Log.d(LOG_TAG, "RESERVED bearer data subparameter " + subparamId + " decode "
+        Rlog.d(LOG_TAG, "RESERVED bearer data subparameter " + subparamId + " decode "
                 + (decodeSuccess ? "succeeded" : "failed") + " (param bits = " + paramBits + ")");
         if (!decodeSuccess) {
             throw new CodingException("RESERVED bearer data subparameter " + subparamId
@@ -216,8 +222,7 @@
     }
 
     private static boolean decodeUserData(BearerData bData, BitwiseInputStream inStream)
-            throws BitwiseInputStream.AccessException
-    {
+            throws BitwiseInputStream.AccessException {
         int paramBits = inStream.read(8) * 8;
         bData.userData = new UserData();
         bData.userData.msgEncoding = inStream.read(5);
@@ -237,14 +242,12 @@
     }
 
     private static String decodeUtf8(byte[] data, int offset, int numFields)
-            throws CodingException
-    {
+            throws CodingException {
         return decodeCharset(data, offset, numFields, 1, "UTF-8");
     }
 
     private static String decodeUtf16(byte[] data, int offset, int numFields)
-            throws CodingException
-    {
+            throws CodingException {
         // Subtract header and possible padding byte (at end) from num fields.
         int padding = offset % 2;
         numFields -= (offset + padding) / 2;
@@ -252,8 +255,7 @@
     }
 
     private static String decodeCharset(byte[] data, int offset, int numFields, int width,
-            String charset) throws CodingException
-    {
+            String charset) throws CodingException {
         if (numFields < 0 || (numFields * width + offset) > data.length) {
             // Try to decode the max number of characters in payload
             int padding = offset % width;
@@ -261,7 +263,7 @@
             if (maxNumFields < 0) {
                 throw new CodingException(charset + " decode failed: offset out of range");
             }
-            Log.e(LOG_TAG, charset + " decode error: offset = " + offset + " numFields = "
+            Rlog.e(LOG_TAG, charset + " decode error: offset = " + offset + " numFields = "
                     + numFields + " data.length = " + data.length + " maxNumFields = "
                     + maxNumFields);
             numFields = maxNumFields;
@@ -274,8 +276,7 @@
     }
 
     private static String decode7bitAscii(byte[] data, int offset, int numFields)
-            throws CodingException
-    {
+            throws CodingException {
         try {
             int offsetBits = offset * 8;
             int offsetSeptets = (offsetBits + 6) / 7;
@@ -310,15 +311,14 @@
     }
 
     private static String decode7bitGsm(byte[] data, int offset, int numFields)
-            throws CodingException
-    {
+            throws CodingException {
         // Start reading from the next 7-bit aligned boundary after offset.
         int offsetBits = offset * 8;
         int offsetSeptets = (offsetBits + 6) / 7;
         numFields -= offsetSeptets;
         int paddingBits = (offsetSeptets * 7) - offsetBits;
-        String result = GsmAlphabet.gsm7BitPackedToString(data, offset, numFields, paddingBits,
-                0, 0);
+        String result = GsmAlphabet.gsm7BitPackedToString(data, offset, numFields,
+                paddingBits, 0, 0);
         if (result == null) {
             throw new CodingException("7bit GSM decoding failed");
         }
@@ -326,20 +326,18 @@
     }
 
     private static String decodeLatin(byte[] data, int offset, int numFields)
-            throws CodingException
-    {
+            throws CodingException {
         return decodeCharset(data, offset, numFields, 1, "ISO-8859-1");
     }
 
     private static String decodeShiftJis(byte[] data, int offset, int numFields)
-            throws CodingException
-    {
+            throws CodingException {
         return decodeCharset(data, offset, numFields, 1, "Shift_JIS");
     }
 
-    private static String decodeGsmDcs(byte[] data, int offset, int numFields, int msgType)
-            throws CodingException
-    {
+    private static String decodeGsmDcs(byte[] data, int offset, int numFields,
+            int msgType)
+            throws CodingException {
         if ((msgType & 0xC0) != 0) {
             throw new CodingException("unsupported coding group ("
                     + msgType + ")");
@@ -358,9 +356,8 @@
         }
     }
 
-    private static void decodeUserDataPayload(UserData userData, boolean hasUserDataHeader)
-            throws CodingException
-    {
+    private static void decodeUserDataPayload(Context context, UserData userData,
+            boolean hasUserDataHeader) throws CodingException {
         int offset = 0;
         if (hasUserDataHeader) {
             int udhLen = userData.payload[0] & 0x00FF;
@@ -374,7 +371,7 @@
                 /*
                  *  Octet decoding depends on the carrier service.
                  */
-                boolean decodingtypeUTF8 = Resources.getSystem()
+                boolean decodingtypeUTF8 = context.getResources()
                         .getBoolean(R.bool.config_sms_utf8_support);
 
                 // Strip off any padding bytes, meaning any differences between the length of the
@@ -388,7 +385,8 @@
                 userData.payload = payload;
 
                 if (!decodingtypeUTF8) {
-                    // There are many devices in the market that send 8bit text sms (latin encoded) as
+                    // There are many devices in the market that send 8bit text sms (latin
+                    // encoded) as
                     // octet encoded.
                     userData.payloadStr = decodeLatin(userData.payload, offset, userData.numFields);
                 } else {
@@ -404,7 +402,8 @@
                 userData.payloadStr = decodeUtf16(userData.payload, offset, userData.numFields);
                 break;
             case UserData.ENCODING_GSM_7BIT_ALPHABET:
-                userData.payloadStr = decode7bitGsm(userData.payload, offset, userData.numFields);
+                userData.payloadStr = decode7bitGsm(userData.payload, offset,
+                        userData.numFields);
                 break;
             case UserData.ENCODING_LATIN:
                 userData.payloadStr = decodeLatin(userData.payload, offset, userData.numFields);
@@ -432,10 +431,10 @@
             decodeSuccess = true;
             bData.language = inStream.read(8);
         }
-        if ((! decodeSuccess) || (paramBits > 0)) {
-            Log.d(LOG_TAG, "LANGUAGE_INDICATOR decode " +
-                    (decodeSuccess ? "succeeded" : "failed") +
-                    " (extra bits = " + paramBits + ")");
+        if ((!decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "LANGUAGE_INDICATOR decode "
+                    + (decodeSuccess ? "succeeded" : "failed")
+                    + " (extra bits = " + paramBits + ")");
         }
         inStream.skip(paramBits);
         return decodeSuccess;
@@ -452,10 +451,10 @@
             bData.priority = inStream.read(2);
             inStream.skip(6);
         }
-        if ((! decodeSuccess) || (paramBits > 0)) {
-            Log.d(LOG_TAG, "PRIORITY_INDICATOR decode " +
-                    (decodeSuccess ? "succeeded" : "failed") +
-                    " (extra bits = " + paramBits + ")");
+        if ((!decodeSuccess) || (paramBits > 0)) {
+            Rlog.d(LOG_TAG, "PRIORITY_INDICATOR decode "
+                    + (decodeSuccess ? "succeeded" : "failed")
+                    + " (extra bits = " + paramBits + ")");
         }
         inStream.skip(paramBits);
         return decodeSuccess;
@@ -489,7 +488,7 @@
      *
      * @param serviceCategory is the service category from the SMS envelope
      */
-    private static void decodeCmasUserData(BearerData bData, int serviceCategory)
+    private static void decodeCmasUserData(Context context, BearerData bData, int serviceCategory)
             throws BitwiseInputStream.AccessException, CodingException {
         BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload);
         if (inStream.available() < 8) {
@@ -540,7 +539,7 @@
 
                     alertUserData.numFields = numFields;
                     alertUserData.payload = inStream.readByteArray(recordLen * 8 - 5);
-                    decodeUserDataPayload(alertUserData, false);
+                    decodeUserDataPayload(context, alertUserData, false);
                     bData.userData = alertUserData;
                     break;
 
@@ -554,7 +553,7 @@
                     break;
 
                 default:
-                    Log.w(LOG_TAG, "skipping unsupported CMAS record type " + recordType);
+                    Rlog.w(LOG_TAG, "skipping unsupported CMAS record type " + recordType);
                     inStream.skip(recordLen * 8);
                     break;
             }
@@ -573,11 +572,11 @@
      * Create BearerData object from serialized representation.
      * (See 3GPP2 C.R1001-F, v1.0, section 4.5 for layout details)
      *
-     * @param smsData byte array of raw encoded SMS bearer data.
+     * @param smsData         byte array of raw encoded SMS bearer data.
      * @param serviceCategory the envelope service category (for CMAS alert handling)
      * @return an instance of BearerData.
      */
-    public static BearerData decode(byte[] smsData, int serviceCategory) {
+    public static BearerData decode(Context context, byte[] smsData, int serviceCategory) {
         try {
             BitwiseInputStream inStream = new BitwiseInputStream(smsData);
             BearerData bData = new BearerData();
@@ -593,30 +592,30 @@
                 // reserved subparams are just skipped.
                 if ((foundSubparamMask & subparamIdBit) != 0 &&
                         (subparamId >= SUBPARAM_MESSAGE_IDENTIFIER &&
-                        subparamId <= SUBPARAM_ID_LAST_DEFINED)) {
+                                subparamId <= SUBPARAM_ID_LAST_DEFINED)) {
                     throw new CodingException("illegal duplicate subparameter (" +
-                                              subparamId + ")");
+                            subparamId + ")");
                 }
                 boolean decodeSuccess;
                 switch (subparamId) {
-                case SUBPARAM_MESSAGE_IDENTIFIER:
-                    decodeSuccess = decodeMessageId(bData, inStream);
-                    break;
-                case SUBPARAM_USER_DATA:
-                    decodeSuccess = decodeUserData(bData, inStream);
-                    break;
-                case SUBPARAM_LANGUAGE_INDICATOR:
-                    decodeSuccess = decodeLanguageIndicator(bData, inStream);
-                    break;
-                case SUBPARAM_PRIORITY_INDICATOR:
-                    decodeSuccess = decodePriorityIndicator(bData, inStream);
-                    break;
-                default:
-                    decodeSuccess = decodeReserved(inStream, subparamId);
+                    case SUBPARAM_MESSAGE_IDENTIFIER:
+                        decodeSuccess = decodeMessageId(bData, inStream);
+                        break;
+                    case SUBPARAM_USER_DATA:
+                        decodeSuccess = decodeUserData(bData, inStream);
+                        break;
+                    case SUBPARAM_LANGUAGE_INDICATOR:
+                        decodeSuccess = decodeLanguageIndicator(bData, inStream);
+                        break;
+                    case SUBPARAM_PRIORITY_INDICATOR:
+                        decodeSuccess = decodePriorityIndicator(bData, inStream);
+                        break;
+                    default:
+                        decodeSuccess = decodeReserved(inStream, subparamId);
                 }
                 if (decodeSuccess &&
                         (subparamId >= SUBPARAM_MESSAGE_IDENTIFIER &&
-                        subparamId <= SUBPARAM_ID_LAST_DEFINED)) {
+                                subparamId <= SUBPARAM_ID_LAST_DEFINED)) {
                     foundSubparamMask |= subparamIdBit;
                 }
             }
@@ -625,16 +624,16 @@
             }
             if (bData.userData != null) {
                 if (isCmasAlertCategory(serviceCategory)) {
-                    decodeCmasUserData(bData, serviceCategory);
+                    decodeCmasUserData(context, bData, serviceCategory);
                 } else {
-                    decodeUserDataPayload(bData.userData, bData.hasUserDataHeader);
+                    decodeUserDataPayload(context, bData.userData, bData.hasUserDataHeader);
                 }
             }
             return bData;
         } catch (BitwiseInputStream.AccessException ex) {
-            Log.e(LOG_TAG, "BearerData decode failed: " + ex);
+            Rlog.e(LOG_TAG, "BearerData decode failed: " + ex);
         } catch (CodingException ex) {
-            Log.e(LOG_TAG, "BearerData decode failed: " + ex);
+            Rlog.e(LOG_TAG, "BearerData decode failed: " + ex);
         }
         return null;
     }
diff --git a/src/com/android/cellbroadcastservice/BitwiseInputStream.java b/src/com/android/cellbroadcastservice/BitwiseInputStream.java
new file mode 100644
index 0000000..eb4ed38
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/BitwiseInputStream.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cellbroadcastservice;
+
+/**
+ * An object that provides bitwise incremental read access to a byte array.
+ *
+ * This is useful, for example, when accessing a series of fields that
+ * may not be aligned on byte boundaries.
+ *
+ * NOTE -- This class is not threadsafe.
+ */
+public class BitwiseInputStream {
+
+    // The byte array being read from.
+    private byte[] mBuf;
+
+    // The current position offset, in bits, from the msb in byte 0.
+    private int mPos;
+
+    // The last valid bit offset.
+    private int mEnd;
+
+    /**
+     * An exception to report access problems.
+     */
+    public static class AccessException extends Exception {
+        public AccessException(String s) {
+            super("BitwiseInputStream access failed: " + s);
+        }
+    }
+
+    /**
+     * Create object from byte array.
+     *
+     * @param buf a byte array containing data
+     */
+    public BitwiseInputStream(byte[] buf) {
+        mBuf = buf;
+        mEnd = buf.length << 3;
+        mPos = 0;
+    }
+
+    /**
+     * Return the number of bit still available for reading.
+     */
+    public int available() {
+        return mEnd - mPos;
+    }
+
+    /**
+     * Read some data and increment the current position.
+     *
+     * The 8-bit limit on access to bitwise streams is intentional to
+     * avoid endianness issues.
+     *
+     * @param bits the amount of data to read (gte 0, lte 8)
+     * @return byte of read data (possibly partially filled, from lsb)
+     */
+    public int read(int bits) throws AccessException {
+        int index = mPos >>> 3;
+        int offset = 16 - (mPos & 0x07) - bits;  // &7==%8
+        if ((bits < 0) || (bits > 8) || ((mPos + bits) > mEnd)) {
+            throw new AccessException(
+                    "illegal read (pos " + mPos + ", end " + mEnd + ", bits " + bits + ")");
+        }
+        int data = (mBuf[index] & 0xFF) << 8;
+        if (offset < 8) data |= mBuf[index + 1] & 0xFF;
+        data >>>= offset;
+        data &= (-1 >>> (32 - bits));
+        mPos += bits;
+        return data;
+    }
+
+    /**
+     * Read data in bulk into a byte array and increment the current position.
+     *
+     * @param bits the amount of data to read
+     * @return newly allocated byte array of read data
+     */
+    public byte[] readByteArray(int bits) throws AccessException {
+        int bytes = (bits >>> 3) + ((bits & 0x07) > 0 ? 1 : 0);  // &7==%8
+        byte[] arr = new byte[bytes];
+        for (int i = 0; i < bytes; i++) {
+            int increment = Math.min(8, bits - (i << 3));
+            arr[i] = (byte) (read(increment) << (8 - increment));
+        }
+        return arr;
+    }
+
+    /**
+     * Increment the current position and ignore contained data.
+     *
+     * @param bits the amount by which to increment the position
+     */
+    public void skip(int bits) throws AccessException {
+        if ((mPos + bits) > mEnd) {
+            throw new AccessException(
+                    "illegal skip (pos " + mPos + ", end " + mEnd + ", bits " + bits + ")");
+        }
+        mPos += bits;
+    }
+}
diff --git a/src/com/android/cellbroadcastservice/CbGeoUtils.java b/src/com/android/cellbroadcastservice/CbGeoUtils.java
index 0a8fe48..2a7a1d7 100644
--- a/src/com/android/cellbroadcastservice/CbGeoUtils.java
+++ b/src/com/android/cellbroadcastservice/CbGeoUtils.java
@@ -19,8 +19,8 @@
 import android.annotation.NonNull;
 import android.telephony.CbGeoUtils.Geometry;
 import android.telephony.CbGeoUtils.LatLng;
+import android.telephony.Rlog;
 import android.text.TextUtils;
-import android.util.Log;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -231,7 +231,7 @@
                     geometries.add(new Polygon(vertices));
                     break;
                 default:
-                    Log.e(TAG, "Invalid geometry format " + geometryStr);
+                    Rlog.e(TAG, "Invalid geometry format " + geometryStr);
             }
         }
         return geometries;
@@ -284,7 +284,7 @@
             sb.append("|");
             sb.append(circle.getRadius());
         } else {
-            Log.e(TAG, "Unsupported geometry object " + geometry);
+            Rlog.e(TAG, "Unsupported geometry object " + geometry);
             return null;
         }
         return sb.toString();
diff --git a/src/com/android/cellbroadcastservice/CdmaServiceCategoryProgramHandler.java b/src/com/android/cellbroadcastservice/CdmaServiceCategoryProgramHandler.java
index 4b254b7..212962e 100644
--- a/src/com/android/cellbroadcastservice/CdmaServiceCategoryProgramHandler.java
+++ b/src/com/android/cellbroadcastservice/CdmaServiceCategoryProgramHandler.java
@@ -23,6 +23,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
+import android.os.Looper;
 import android.os.Message;
 import android.provider.Telephony.Sms.Intents;
 import android.telephony.SubscriptionManager;
@@ -44,7 +45,7 @@
      * Create a new CDMA inbound SMS handler.
      */
     CdmaServiceCategoryProgramHandler(Context context) {
-        super("CdmaServiceCategoryProgramHandler", context);
+        super("CdmaServiceCategoryProgramHandler", context, Looper.myLooper());
         mContext = context;
     }
 
diff --git a/src/com/android/cellbroadcastservice/CellBroadcastHandler.java b/src/com/android/cellbroadcastservice/CellBroadcastHandler.java
index a5d1c8a..fd3b8ae 100644
--- a/src/com/android/cellbroadcastservice/CellBroadcastHandler.java
+++ b/src/com/android/cellbroadcastservice/CellBroadcastHandler.java
@@ -16,6 +16,8 @@
 
 package com.android.cellbroadcastservice;
 
+import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
 import static android.provider.Settings.Secure.CMAS_ADDITIONAL_BROADCAST_PKG;
 
 import android.Manifest;
@@ -32,14 +34,17 @@
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.location.Location;
-import android.location.LocationListener;
 import android.location.LocationManager;
+import android.location.LocationRequest;
 import android.net.Uri;
-import android.os.Bundle;
+import android.os.Build;
+import android.os.CancellationSignal;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.Looper;
 import android.os.Message;
 import android.os.Process;
+import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -47,16 +52,14 @@
 import android.provider.Telephony.CellBroadcasts;
 import android.telephony.CbGeoUtils.Geometry;
 import android.telephony.CbGeoUtils.LatLng;
+import android.telephony.Rlog;
 import android.telephony.SmsCbMessage;
 import android.telephony.SubscriptionManager;
 import android.telephony.cdma.CdmaSmsCbProgramData;
 import android.text.TextUtils;
-import android.text.format.DateUtils;
 import android.util.LocalLog;
-import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.telephony.metrics.TelephonyMetrics;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -67,6 +70,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -99,10 +103,10 @@
     private static final boolean IS_DEBUGGABLE = SystemProperties.getInt("ro.debuggable", 0) == 1;
 
     /** Uses to request the location update. */
-    public final LocationRequester mLocationRequester;
+    private final LocationRequester mLocationRequester;
 
     /** Timestamp of last airplane mode on */
-    private long mLastAirplaneModeTime = 0;
+    protected long mLastAirplaneModeTime = 0;
 
     /** Resource cache */
     private final Map<Integer, Resources> mResourcesCache = new HashMap<>();
@@ -117,15 +121,15 @@
     private final Map<Integer, Integer> mServiceCategoryCrossRATMap;
 
     private CellBroadcastHandler(Context context) {
-        this("CellBroadcastHandler", context);
+        this("CellBroadcastHandler", context, Looper.myLooper());
     }
 
-    protected CellBroadcastHandler(String debugTag, Context context) {
-        super(debugTag, context);
+    protected CellBroadcastHandler(String debugTag, Context context, Looper looper) {
+        super(debugTag, context, looper);
         mLocationRequester = new LocationRequester(
                 context,
                 (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE),
-                getHandler().getLooper());
+                getHandler());
 
         // Adding GSM / CDMA service category mapping.
         mServiceCategoryCrossRATMap = Stream.of(new Integer[][] {
@@ -186,7 +190,7 @@
 
         IntentFilter intentFilter = new IntentFilter();
         intentFilter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
-        if (IS_DEBUGGABLE) {
+        if (Build.IS_DEBUGGABLE) {
             intentFilter.addAction(ACTION_DUPLICATE_DETECTION);
         }
         mContext.registerReceiver(
@@ -198,7 +202,7 @@
                                 boolean airplaneModeOn = intent.getBooleanExtra("state", false);
                                 if (airplaneModeOn) {
                                     mLastAirplaneModeTime = System.currentTimeMillis();
-                                    log("Airplane mode on. Reset duplicate detection.");
+                                    log("Airplane mode on.");
                                 }
                                 break;
                             case ACTION_DUPLICATE_DETECTION:
@@ -249,12 +253,6 @@
      */
     protected void handleBroadcastSms(SmsCbMessage message) {
         int slotIndex = message.getSlotIndex();
-        // Log Cellbroadcast msg received event
-        TelephonyMetrics metrics = TelephonyMetrics.getInstance();
-        metrics.writeNewCBSms(slotIndex, message.getMessageFormat(),
-                message.getMessagePriority(), message.isCmasMessage(), message.isEtwsMessage(),
-                message.getServiceCategory(), message.getSerialNumber(),
-                System.currentTimeMillis());
 
         // TODO: Database inserting can be time consuming, therefore this should be changed to
         // asynchronous.
@@ -318,18 +316,17 @@
         long expirationDuration = res.getInteger(R.integer.message_expiration_time);
         long dupCheckTime = System.currentTimeMillis() - expirationDuration;
 
-        // Some carriers require reset duplication detection after airplane mode.
-        if (res.getBoolean(R.bool.reset_duplicate_detection_on_airplane_mode)) {
+        // Some carriers require reset duplication detection after airplane mode or reboot.
+        if (res.getBoolean(R.bool.reset_on_power_cycle_or_airplane_mode)) {
             dupCheckTime = Long.max(dupCheckTime, mLastAirplaneModeTime);
+            dupCheckTime = Long.max(dupCheckTime,
+                    System.currentTimeMillis() - SystemClock.elapsedRealtime());
         }
 
         List<SmsCbMessage> cbMessages = new ArrayList<>();
 
         try (Cursor cursor = mContext.getContentResolver().query(CellBroadcasts.CONTENT_URI,
-                // TODO: QUERY_COLUMNS_FWK is a hidden API, since we are going to move
-                //  CellBroadcastProvider to this module we can define those COLUMNS in side
-                //  CellBroadcastProvider and reference from there.
-                CellBroadcasts.QUERY_COLUMNS_FWK,
+                CellBroadcastProvider.QUERY_COLUMNS,
                 where,
                 new String[] {Long.toString(dupCheckTime)},
                 null)) {
@@ -463,7 +460,7 @@
     protected void broadcastMessage(@NonNull SmsCbMessage message, @Nullable Uri messageUri,
             int slotIndex) {
         String receiverPermission;
-        int appOp;
+        String appOp;
         String msg;
         Intent intent;
         if (message.isEmergencyMessage()) {
@@ -474,7 +471,7 @@
             //Emergency alerts need to be delivered with high priority
             intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
             receiverPermission = Manifest.permission.RECEIVE_EMERGENCY_BROADCAST;
-            appOp = AppOpsManager.OP_RECEIVE_EMERGECY_SMS;
+            appOp = AppOpsManager.OPSTR_RECEIVE_EMERGENCY_BROADCAST;
 
             intent.putExtra(EXTRA_MESSAGE, message);
             int subId = getSubIdForPhone(slotIndex);
@@ -493,9 +490,9 @@
                 if (additionalPackage != null) {
                     Intent additionalIntent = new Intent(intent);
                     additionalIntent.setPackage(additionalPackage);
-                    mContext.sendOrderedBroadcastAsUser(additionalIntent, UserHandle.ALL,
-                            receiverPermission, appOp, null, getHandler(), Activity.RESULT_OK,
-                            null, null);
+                    mContext.createContextAsUser(UserHandle.ALL, 0).sendOrderedBroadcast(
+                            additionalIntent, receiverPermission, appOp, null, getHandler(),
+                            Activity.RESULT_OK, null, null);
                 }
             }
 
@@ -505,8 +502,9 @@
             for (String pkg : pkgs) {
                 // Explicitly send the intent to all the configured cell broadcast receivers.
                 intent.setPackage(pkg);
-                mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, receiverPermission,
-                        appOp, null, mReceiver, getHandler(), Activity.RESULT_OK, null, null);
+                mContext.createContextAsUser(UserHandle.ALL, 0).sendOrderedBroadcast(
+                        intent, receiverPermission, appOp, null, getHandler(),
+                        Activity.RESULT_OK, null, null);
             }
         } else {
             msg = "Dispatching SMS CB, SmsCbMessage is: " + message;
@@ -517,14 +515,15 @@
             // this intent.
             intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
             receiverPermission = Manifest.permission.RECEIVE_SMS;
-            appOp = AppOpsManager.OP_RECEIVE_SMS;
+            appOp = AppOpsManager.OPSTR_RECEIVE_SMS;
 
             intent.putExtra(EXTRA_MESSAGE, message);
             SubscriptionManager.putPhoneIdAndSubIdExtra(intent, slotIndex);
 
             mReceiverCount.incrementAndGet();
-            mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, receiverPermission,
-                    appOp, null, mReceiver, getHandler(), Activity.RESULT_OK, null, null);
+            mContext.createContextAsUser(UserHandle.ALL, 0).sendOrderedBroadcast(
+                    intent, receiverPermission, appOp, mReceiver, getHandler(),
+                    Activity.RESULT_OK, null, null);
         }
 
         if (messageUri != null) {
@@ -580,18 +579,9 @@
 
         /**
          * Use as the default maximum wait time if the cell broadcast doesn't specify the value.
-         * Most of the location request should be responded within 20 seconds.
+         * Most of the location request should be responded within 30 seconds.
          */
-        private static final int DEFAULT_MAXIMUM_WAIT_TIME_SEC = 20;
-
-        /**
-         * Trigger this event when the {@link LocationManager} is not responded within the given
-         * time.
-         */
-        private static final int EVENT_LOCATION_REQUEST_TIMEOUT = 1;
-
-        /** Request a single location update. */
-        private static final int EVENT_REQUEST_LOCATION_UPDATE = 2;
+        private static final int DEFAULT_MAXIMUM_WAIT_TIME_SEC = 30;
 
         /**
          * Request location update from network or gps location provider. Network provider will be
@@ -601,17 +591,20 @@
                 LocationManager.NETWORK_PROVIDER, LocationManager.GPS_PROVIDER);
 
         private final LocationManager mLocationManager;
-        private final Looper mLooper;
         private final List<LocationUpdateCallback> mCallbacks;
         private final Context mContext;
-        private Handler mLocationHandler;
+        private final Handler mLocationHandler;
 
-        LocationRequester(Context context, LocationManager locationManager, Looper looper) {
+        private int mNumLocationUpdatesInProgress;
+
+        private final List<CancellationSignal> mCancellationSignals = new ArrayList<>();
+
+        LocationRequester(Context context, LocationManager locationManager, Handler handler) {
             mLocationManager = locationManager;
-            mLooper = looper;
             mCallbacks = new ArrayList<>();
             mContext = context;
-            mLocationHandler = new LocationHandler(looper);
+            mLocationHandler = handler;
+            mNumLocationUpdatesInProgress = 0;
         }
 
         /**
@@ -619,103 +612,83 @@
          * {@code null} location will be called immediately.
          *
          * @param callback a callback to the response when the location is available
-         * @param maximumWaitTimeSec the maximum wait time of this request. If location is not
+         * @param maximumWaitTimeS the maximum wait time of this request. If location is not
          * updated within the maximum wait time, {@code callback#onLocationUpadte(null)} will be
          * called.
          */
         void requestLocationUpdate(@NonNull LocationUpdateCallback callback,
-                int maximumWaitTimeSec) {
-            mLocationHandler.obtainMessage(EVENT_REQUEST_LOCATION_UPDATE, maximumWaitTimeSec,
-                    0 /* arg2 */, callback).sendToTarget();
+                int maximumWaitTimeS) {
+            mLocationHandler.post(() -> requestLocationUpdateInternal(callback, maximumWaitTimeS));
         }
 
-        private void onLocationUpdate(@Nullable LatLng location) {
+        private void onLocationUpdate(@Nullable Location location) {
+            mNumLocationUpdatesInProgress--;
+
+            LatLng latLng = null;
+            if (location != null) {
+                Rlog.d(TAG, "Got location update");
+                latLng = new LatLng(location.getLatitude(), location.getLongitude());
+            } else if (mNumLocationUpdatesInProgress > 0) {
+                Rlog.d(TAG, "Still waiting for " + mNumLocationUpdatesInProgress
+                        + " more location updates.");
+                return;
+            } else {
+                Rlog.d(TAG, "Location is not available.");
+            }
+
             for (LocationUpdateCallback callback : mCallbacks) {
-                callback.onLocationUpdate(location);
+                callback.onLocationUpdate(latLng);
             }
             mCallbacks.clear();
+
+            mCancellationSignals.forEach(CancellationSignal::cancel);
+            mCancellationSignals.clear();
+
+            mNumLocationUpdatesInProgress = 0;
         }
 
         private void requestLocationUpdateInternal(@NonNull LocationUpdateCallback callback,
-                int maximumWaitTimeSec) {
-            if (DBG) Log.d(TAG, "requestLocationUpdate");
-            if (!isLocationServiceAvailable()) {
+                int maximumWaitTimeS) {
+            if (DBG) Rlog.d(TAG, "requestLocationUpdate");
+            if (!hasPermission(ACCESS_FINE_LOCATION) && !hasPermission(ACCESS_COARSE_LOCATION)) {
                 if (DBG) {
-                    Log.d(TAG, "Can't request location update because of no location permission");
+                    Rlog.d(TAG, "Can't request location update because of no location permission");
                 }
                 callback.onLocationUpdate(null);
                 return;
             }
+            if (mNumLocationUpdatesInProgress == 0) {
+                for (String provider : LOCATION_PROVIDERS) {
+                    if (!mLocationManager.isProviderEnabled(provider)) {
+                        if (DBG) {
+                            Rlog.d(TAG, "provider " + provider + " not available");
+                        }
+                        continue;
+                    }
+                    LocationRequest request = LocationRequest.createFromDeprecatedProvider(provider,
+                            0, 0, true);
+                    if (maximumWaitTimeS == SmsCbMessage.MAXIMUM_WAIT_TIME_NOT_SET) {
+                        maximumWaitTimeS = DEFAULT_MAXIMUM_WAIT_TIME_SEC;
+                    }
+                    request.setExpireIn(TimeUnit.SECONDS.toMillis(maximumWaitTimeS));
 
-            if (!mLocationHandler.hasMessages(EVENT_LOCATION_REQUEST_TIMEOUT)) {
-                if (maximumWaitTimeSec == SmsCbMessage.MAXIMUM_WAIT_TIME_NOT_SET) {
-                    maximumWaitTimeSec = DEFAULT_MAXIMUM_WAIT_TIME_SEC;
-                }
-                mLocationHandler.sendMessageDelayed(
-                        mLocationHandler.obtainMessage(EVENT_LOCATION_REQUEST_TIMEOUT),
-                        maximumWaitTimeSec * DateUtils.SECOND_IN_MILLIS);
-            }
-
-            mCallbacks.add(callback);
-
-            for (String provider : LOCATION_PROVIDERS) {
-                if (mLocationManager.isProviderEnabled(provider)) {
-                    mLocationManager.requestSingleUpdate(provider, mLocationListener, mLooper);
-                    break;
+                    CancellationSignal signal = new CancellationSignal();
+                    mCancellationSignals.add(signal);
+                    mLocationManager.getCurrentLocation(request, signal,
+                            new HandlerExecutor(mLocationHandler), this::onLocationUpdate);
+                    mNumLocationUpdatesInProgress++;
                 }
             }
-        }
-
-        private boolean isLocationServiceAvailable() {
-            if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)
-                    && !hasPermission(Manifest.permission.ACCESS_COARSE_LOCATION)) return false;
-            for (String provider : LOCATION_PROVIDERS) {
-                if (mLocationManager.isProviderEnabled(provider)) return true;
+            if (mNumLocationUpdatesInProgress > 0) {
+                mCallbacks.add(callback);
+            } else {
+                callback.onLocationUpdate(null);
             }
-            return false;
         }
 
         private boolean hasPermission(String permission) {
             return mContext.checkPermission(permission, Process.myPid(), Process.myUid())
                     == PackageManager.PERMISSION_GRANTED;
         }
-
-        private final LocationListener mLocationListener = new LocationListener() {
-            @Override
-            public void onLocationChanged(Location location) {
-                mLocationHandler.removeMessages(EVENT_LOCATION_REQUEST_TIMEOUT);
-                onLocationUpdate(new LatLng(location.getLatitude(), location.getLongitude()));
-            }
-
-            @Override
-            public void onStatusChanged(String provider, int status, Bundle extras) {}
-
-            @Override
-            public void onProviderEnabled(String provider) {}
-
-            @Override
-            public void onProviderDisabled(String provider) {}
-        };
-
-        private final class LocationHandler extends Handler {
-            LocationHandler(Looper looper) {
-                super(looper);
-            }
-
-            @Override
-            public void handleMessage(Message msg) {
-                switch (msg.what) {
-                    case EVENT_LOCATION_REQUEST_TIMEOUT:
-                        if (DBG) Log.d(TAG, "location request timeout");
-                        onLocationUpdate(null);
-                        break;
-                    case EVENT_REQUEST_LOCATION_UPDATE:
-                        requestLocationUpdateInternal((LocationUpdateCallback) msg.obj, msg.arg1);
-                        break;
-                    default:
-                        Log.e(TAG, "Unsupported message type " + msg.what);
-                }
-            }
-        }
     }
 }
diff --git a/src/com/android/cellbroadcastservice/CellBroadcastProvider.java b/src/com/android/cellbroadcastservice/CellBroadcastProvider.java
index 8b1a747..6a2bf05 100644
--- a/src/com/android/cellbroadcastservice/CellBroadcastProvider.java
+++ b/src/com/android/cellbroadcastservice/CellBroadcastProvider.java
@@ -31,6 +31,7 @@
 import android.os.Binder;
 import android.os.Process;
 import android.provider.Telephony.CellBroadcasts;
+import android.telephony.Rlog;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -98,6 +99,43 @@
     /** Content uri of this provider. */
     public static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts");
 
+    /**
+     * Local definition of the subId column name.
+     * The value should match CellBroadcasts.SUB_ID, but we don't use it here because it's hidden
+     * and deprecated, and slot_index should be enough in the future.
+     */
+    private static final String SUB_ID = "sub_id";
+
+    /**
+     * Local definition of the query columns for instantiating
+     * {@link android.telephony.SmsCbMessage} objects.
+     */
+    public static final String[] QUERY_COLUMNS = {
+        CellBroadcasts._ID,
+        CellBroadcasts.SLOT_INDEX,
+        CellBroadcasts.GEOGRAPHICAL_SCOPE,
+        CellBroadcasts.PLMN,
+        CellBroadcasts.LAC,
+        CellBroadcasts.CID,
+        CellBroadcasts.SERIAL_NUMBER,
+        CellBroadcasts.SERVICE_CATEGORY,
+        CellBroadcasts.LANGUAGE_CODE,
+        CellBroadcasts.MESSAGE_BODY,
+        CellBroadcasts.MESSAGE_FORMAT,
+        CellBroadcasts.MESSAGE_PRIORITY,
+        CellBroadcasts.ETWS_WARNING_TYPE,
+        CellBroadcasts.CMAS_MESSAGE_CLASS,
+        CellBroadcasts.CMAS_CATEGORY,
+        CellBroadcasts.CMAS_RESPONSE_TYPE,
+        CellBroadcasts.CMAS_SEVERITY,
+        CellBroadcasts.CMAS_URGENCY,
+        CellBroadcasts.CMAS_CERTAINTY,
+        CellBroadcasts.RECEIVED_TIME,
+        CellBroadcasts.MESSAGE_BROADCASTED,
+        CellBroadcasts.GEOMETRIES,
+        CellBroadcasts.MAXIMUM_WAIT_TIME
+    };
+
     @VisibleForTesting
     public PermissionChecker mPermissionChecker;
 
@@ -148,7 +186,7 @@
         checkReadPermission(uri);
 
         if (DBG) {
-            Log.d(TAG, "query:"
+            Rlog.d(TAG, "query:"
                     + " uri = " + uri
                     + " projection = " + Arrays.toString(projection)
                     + " selection = " + selection
@@ -188,7 +226,7 @@
         checkWritePermission();
 
         if (DBG) {
-            Log.d(TAG, "insert:"
+            Rlog.d(TAG, "insert:"
                     + " uri = " + uri
                     + " contentValue = " + values);
         }
@@ -203,7 +241,7 @@
                             .notifyChange(CONTENT_URI, null /* observer */);
                     return newUri;
                 } else {
-                    Log.e(TAG, "Insert record failed because of unknown reason, uri = " + uri);
+                    Rlog.e(TAG, "Insert record failed because of unknown reason, uri = " + uri);
                     return null;
                 }
             default:
@@ -217,7 +255,7 @@
         checkWritePermission();
 
         if (DBG) {
-            Log.d(TAG, "delete:"
+            Rlog.d(TAG, "delete:"
                     + " uri = " + uri
                     + " selection = " + selection
                     + " selectionArgs = " + Arrays.toString(selectionArgs));
@@ -238,7 +276,7 @@
         checkWritePermission();
 
         if (DBG) {
-            Log.d(TAG, "update:"
+            Rlog.d(TAG, "update:"
                     + " uri = " + uri
                     + " values = {" + values + "}"
                     + " selection = " + selection
@@ -270,7 +308,7 @@
     public static String getStringForCellBroadcastTableCreation(String tableName) {
         return "CREATE TABLE " + tableName + " ("
                 + CellBroadcasts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
-                + CellBroadcasts.SUB_ID + " INTEGER,"
+                + SUB_ID + " INTEGER,"
                 + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0,"
                 + CellBroadcasts.GEOGRAPHICAL_SCOPE + " INTEGER,"
                 + CellBroadcasts.PLMN + " TEXT,"
@@ -345,12 +383,12 @@
         @Override
         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
             if (DBG) {
-                Log.d(TAG, "onUpgrade: oldV=" + oldVersion + " newV=" + newVersion);
+                Rlog.d(TAG, "onUpgrade: oldV=" + oldVersion + " newV=" + newVersion);
             }
             if (newVersion == 2) {
                 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
                         + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0;");
-                Log.d(TAG, "add slotIndex column");
+                Rlog.d(TAG, "add slotIndex column");
             }
         }
     }
diff --git a/src/com/android/cellbroadcastservice/DefaultCellBroadcastService.java b/src/com/android/cellbroadcastservice/DefaultCellBroadcastService.java
index 7b03fef..6e76235 100644
--- a/src/com/android/cellbroadcastservice/DefaultCellBroadcastService.java
+++ b/src/com/android/cellbroadcastservice/DefaultCellBroadcastService.java
@@ -19,12 +19,14 @@
 import android.content.Context;
 import android.os.Bundle;
 import android.telephony.CellBroadcastService;
+import android.telephony.Rlog;
 import android.telephony.SmsCbLocation;
 import android.telephony.SmsCbMessage;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.telephony.cdma.CdmaSmsCbProgramData;
-import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -57,13 +59,13 @@
 
     @Override
     public void onGsmCellBroadcastSms(int slotIndex, byte[] message) {
-        Log.d(TAG, "onGsmCellBroadcastSms received message on slotId=" + slotIndex);
+        Rlog.d(TAG, "onGsmCellBroadcastSms received message on slotId=" + slotIndex);
         mGsmCellBroadcastHandler.onGsmCellBroadcastSms(slotIndex, message);
     }
 
     @Override
     public void onCdmaCellBroadcastSms(int slotIndex, byte[] bearerData, int serviceCategory) {
-        Log.d(TAG, "onCdmaCellBroadcastSms received message on slotId=" + slotIndex);
+        Rlog.d(TAG, "onCdmaCellBroadcastSms received message on slotId=" + slotIndex);
         int[] subIds =
                 ((SubscriptionManager) getSystemService(
                         Context.TELEPHONY_SUBSCRIPTION_SERVICE)).getSubscriptionIds(slotIndex);
@@ -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);
         }
@@ -85,12 +88,11 @@
     @Override
     public void onCdmaScpMessage(int slotIndex, List<CdmaSmsCbProgramData> programData,
             String originatingAddress, Consumer<Bundle> callback) {
-        Log.d(TAG, "onCdmaScpMessage received message on slotId=" + slotIndex);
+        Rlog.d(TAG, "onCdmaScpMessage received message on slotId=" + slotIndex);
         mCdmaScpHandler.onCdmaScpMessage(slotIndex, new ArrayList<>(programData),
                 originatingAddress, callback);
     }
 
-
     /**
      * Parses a CDMA broadcast SMS
      *
@@ -99,14 +101,16 @@
      * @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(bearerData, serviceCategory);
+        BearerData bData = BearerData.decode(context, bearerData, serviceCategory);
         if (bData == null) {
-            Log.w(TAG, "BearerData.decode() returned null");
+            Rlog.w(TAG, "BearerData.decode() returned null");
             return null;
         }
-        Log.d(TAG, "MT raw BearerData = " + toHexString(bearerData, 0, bearerData.length));
+        Rlog.d(TAG, "MT raw BearerData = " + toHexString(bearerData, 0, bearerData.length));
         SmsCbLocation location = new SmsCbLocation(plmn);
 
         return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP2,
diff --git a/src/com/android/cellbroadcastservice/GsmAlphabet.java b/src/com/android/cellbroadcastservice/GsmAlphabet.java
new file mode 100644
index 0000000..c305557
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/GsmAlphabet.java
@@ -0,0 +1,733 @@
+/*
+ * Copyright (C) 2006 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.annotation.UnsupportedAppUsage;
+import android.telephony.Rlog;
+import android.util.SparseIntArray;
+
+/**
+ * This class implements the character set mapping between
+ * the GSM SMS 7-bit alphabet specified in TS 23.038 6.2.1
+ * and UTF-16
+ *
+ * {@hide}
+ */
+public class GsmAlphabet {
+    private static final String TAG = "GSM";
+
+    private GsmAlphabet() {
+    }
+
+    /**
+     * This escapes extended characters, and when present indicates that the
+     * following character should be looked up in the "extended" table.
+     *
+     * gsmToChar(GSM_EXTENDED_ESCAPE) returns 0xffff
+     */
+    public static final byte GSM_EXTENDED_ESCAPE = 0x1B;
+
+    /**
+     * User data header requires one octet for length. Count as one septet, because
+     * all combinations of header elements below will have at least one free bit
+     * when padding to the nearest septet boundary.
+     */
+    public static final int UDH_SEPTET_COST_LENGTH = 1;
+
+    /**
+     * Using a non-default language locking shift table OR single shift table
+     * requires a user data header of 3 octets, or 4 septets, plus UDH length.
+     */
+    public static final int UDH_SEPTET_COST_ONE_SHIFT_TABLE = 4;
+
+    /**
+     * Using a non-default language locking shift table AND single shift table
+     * requires a user data header of 6 octets, or 7 septets, plus UDH length.
+     */
+    public static final int UDH_SEPTET_COST_TWO_SHIFT_TABLES = 7;
+
+    /**
+     * Multi-part messages require a user data header of 5 octets, or 6 septets,
+     * plus UDH length.
+     */
+    public static final int UDH_SEPTET_COST_CONCATENATED_MESSAGE = 6;
+
+    /** Reverse mapping from Unicode characters to indexes into language tables. */
+    @UnsupportedAppUsage
+    private static SparseIntArray[] sCharsToGsmTables;
+
+    /** Reverse mapping from Unicode characters to indexes into language shift tables. */
+    @UnsupportedAppUsage
+    private static SparseIntArray[] sCharsToShiftTables;
+
+    /**
+     * GSM default 7 bit alphabet plus national language locking shift character tables.
+     * Comment lines above strings indicate the lower four bits of the table position.
+     */
+    @UnsupportedAppUsage
+    private static final String[] sLanguageTables = {
+            /* 3GPP TS 23.038 V9.1.1 section 6.2.1 - GSM 7 bit Default Alphabet
+             01.....23.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.E.....F.....0....
+             .1 */
+            "@\u00a3$\u00a5\u00e8\u00e9\u00f9\u00ec\u00f2\u00c7\n\u00d8\u00f8\r\u00c5\u00e5\u0394_"
+                    // 2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.
+                    // ....
+                    + "\u03a6\u0393\u039b\u03a9\u03a0\u03a8\u03a3\u0398\u039e\uffff\u00c6\u00e6"
+                    + "\u00df"
+                    // F.....012.34.....56789ABCDEF0123456789ABCDEF0.....123456789ABCDEF0123456789A
+                    + "\u00c9 !\"#\u00a4%&'()*+,-./0123456789:;<=>?\u00a1ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                    // B.....C.....D.....E.....F.....0.....123456789ABCDEF0123456789AB.....C....
+                    // .D.....
+                    + "\u00c4\u00d6\u00d1\u00dc\u00a7\u00bfabcdefghijklmnopqrstuvwxyz\u00e4\u00f6"
+                    + "\u00f1"
+                    // E.....F.....
+                    + "\u00fc\u00e0",
+
+            /* A.3.1 Turkish National Language Locking Shift Table
+             01.....23.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.E.....F.....0....
+             .1 */
+            "@\u00a3$\u00a5\u20ac\u00e9\u00f9\u0131\u00f2\u00c7\n\u011e\u011f\r\u00c5\u00e5\u0394_"
+                    // 2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.
+                    // ....
+                    + "\u03a6\u0393\u039b\u03a9\u03a0\u03a8\u03a3\u0398\u039e\uffff\u015e\u015f"
+                    + "\u00df"
+                    // F.....012.34.....56789ABCDEF0123456789ABCDEF0.....123456789ABCDEF0123456789A
+                    + "\u00c9 !\"#\u00a4%&'()*+,-./0123456789:;<=>?\u0130ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                    // B.....C.....D.....E.....F.....0.....123456789ABCDEF0123456789AB.....C....
+                    // .D.....
+                    + "\u00c4\u00d6\u00d1\u00dc\u00a7\u00e7abcdefghijklmnopqrstuvwxyz\u00e4\u00f6"
+                    + "\u00f1"
+                    // E.....F.....
+                    + "\u00fc\u00e0",
+
+            /* A.3.2 Void (no locking shift table for Spanish) */
+            "",
+
+            /* A.3.3 Portuguese National Language Locking Shift Table
+             01.....23.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.E.....F.....0....
+             .1 */
+            "@\u00a3$\u00a5\u00ea\u00e9\u00fa\u00ed\u00f3\u00e7\n\u00d4\u00f4\r\u00c1\u00e1\u0394_"
+                    // 2.....3.....4.....5.....67.8.....9.....AB.....C.....D.....E.....F.....012
+                    // .34.....
+                    + "\u00aa\u00c7\u00c0\u221e^\\\u20ac\u00d3|\uffff\u00c2\u00e2\u00ca\u00c9 "
+                    + "!\"#\u00ba"
+                    // 56789ABCDEF0123456789ABCDEF0.....123456789ABCDEF0123456789AB.....C.....D..
+                    // ...E.....
+                    + "%&'()*+,-./0123456789:;<=>?\u00cdABCDEFGHIJKLMNOPQRSTUVWXYZ\u00c3\u00d5"
+                    + "\u00da\u00dc"
+                    // F.....0123456789ABCDEF0123456789AB.....C.....DE.....F.....
+                    + "\u00a7~abcdefghijklmnopqrstuvwxyz\u00e3\u00f5`\u00fc\u00e0",
+
+            /* A.3.4 Bengali National Language Locking Shift Table
+             0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....CD.EF.....0..... */
+            "\u0981\u0982\u0983\u0985\u0986\u0987\u0988\u0989\u098a\u098b\n\u098c \r \u098f\u0990"
+                    // 123.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E....
+                    // .F.....
+                    + "  \u0993\u0994\u0995\u0996\u0997\u0998\u0999\u099a\uffff\u099b\u099c\u099d"
+                    + "\u099e"
+                    // 012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF....
+                    // .0123456789ABC
+                    + " !\u099f\u09a0\u09a1\u09a2\u09a3\u09a4)(\u09a5\u09a6,\u09a7"
+                    + ".\u09a80123456789:; "
+                    // D.....E.....F0.....1.....2.....3.....4.....56.....789A.....B.....C.....D.....
+                    + "\u09aa\u09ab?\u09ac\u09ad\u09ae\u09af\u09b0 \u09b2   "
+                    + "\u09b6\u09b7\u09b8\u09b9"
+                    // E.....F.....0.....1.....2.....3.....4.....5.....6.....789.....A.....BCD...
+                    // ..E.....
+                    + "\u09bc\u09bd\u09be\u09bf\u09c0\u09c1\u09c2\u09c3\u09c4  \u09c7\u09c8  "
+                    + "\u09cb\u09cc"
+                    // F.....0.....123456789ABCDEF0123456789AB.....C.....D.....E.....F.....
+                    + "\u09cd\u09ceabcdefghijklmnopqrstuvwxyz\u09d7\u09dc\u09dd\u09f0\u09f1",
+
+            /* A.3.5 Gujarati National Language Locking Shift Table
+             0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.EF.....0
+             .....*/
+            "\u0a81\u0a82\u0a83\u0a85\u0a86\u0a87\u0a88\u0a89\u0a8a\u0a8b\n\u0a8c\u0a8d\r "
+                    + "\u0a8f\u0a90"
+                    // 1.....23.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E
+                    // .....
+                    + "\u0a91 \u0a93\u0a94\u0a95\u0a96\u0a97\u0a98\u0a99\u0a9a\uffff\u0a9b\u0a9c"
+                    + "\u0a9d"
+                    // F.....012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF....
+                    // .0123456789AB
+                    + "\u0a9e !\u0a9f\u0aa0\u0aa1\u0aa2\u0aa3\u0aa4)(\u0aa5\u0aa6,\u0aa7"
+                    + ".\u0aa80123456789:;"
+                    // CD.....E.....F0.....1.....2.....3.....4.....56.....7.....89.....A.....B...
+                    // ..C.....
+                    + " \u0aaa\u0aab?\u0aac\u0aad\u0aae\u0aaf\u0ab0 \u0ab2\u0ab3 "
+                    + "\u0ab5\u0ab6\u0ab7\u0ab8"
+                    // D.....E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89.....A
+                    // .....
+                    + "\u0ab9\u0abc\u0abd\u0abe\u0abf\u0ac0\u0ac1\u0ac2\u0ac3\u0ac4\u0ac5 "
+                    + "\u0ac7\u0ac8"
+                    // B.....CD.....E.....F.....0.....123456789ABCDEF0123456789AB.....C.....D....
+                    // .E.....
+                    + "\u0ac9 \u0acb\u0acc\u0acd\u0ad0abcdefghijklmnopqrstuvwxyz\u0ae0\u0ae1"
+                    + "\u0ae2\u0ae3"
+                    // F.....
+                    + "\u0af1",
+
+            /* A.3.6 Hindi National Language Locking Shift Table
+             0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.E.....F.
+             ....*/
+            "\u0901\u0902\u0903\u0905\u0906\u0907\u0908\u0909\u090a\u090b\n\u090c\u090d\r\u090e"
+                    + "\u090f"
+                    // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.
+                    // ....D.....
+                    + "\u0910\u0911\u0912\u0913\u0914\u0915\u0916\u0917\u0918\u0919\u091a\uffff"
+                    + "\u091b\u091c"
+                    // E.....F.....012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF.
+                    // ....012345
+                    + "\u091d\u091e !\u091f\u0920\u0921\u0922\u0923\u0924)(\u0925\u0926,\u0927"
+                    + ".\u0928012345"
+                    // 6789ABC.....D.....E.....F0.....1.....2.....3.....4.....5.....6.....7.....8
+                    // .....
+                    + "6789:;\u0929\u092a\u092b?\u092c\u092d\u092e\u092f\u0930\u0931\u0932\u0933"
+                    + "\u0934"
+                    // 9.....A.....B.....C.....D.....E.....F.....0.....1.....2.....3.....4.....5.
+                    // ....6.....
+                    + "\u0935\u0936\u0937\u0938\u0939\u093c\u093d\u093e\u093f\u0940\u0941\u0942"
+                    + "\u0943\u0944"
+                    // 7.....8.....9.....A.....B.....C.....D.....E.....F.....0....
+                    // .123456789ABCDEF012345678
+                    + "\u0945\u0946\u0947\u0948\u0949\u094a\u094b\u094c\u094d"
+                    + "\u0950abcdefghijklmnopqrstuvwx"
+                    // 9AB.....C.....D.....E.....F.....
+                    + "yz\u0972\u097b\u097c\u097e\u097f",
+
+            /* A.3.7 Kannada National Language Locking Shift Table
+               NOTE: TS 23.038 V9.1.1 shows code 0x24 as \u0caa, corrected to \u0ca1 (typo)
+             01.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....CD.E.....F.....0....
+             .1 */
+            " \u0c82\u0c83\u0c85\u0c86\u0c87\u0c88\u0c89\u0c8a\u0c8b\n\u0c8c \r\u0c8e\u0c8f\u0c90 "
+                    // 2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.
+                    // ....F.....
+                    + "\u0c92\u0c93\u0c94\u0c95\u0c96\u0c97\u0c98\u0c99\u0c9a\uffff\u0c9b\u0c9c"
+                    + "\u0c9d\u0c9e"
+                    // 012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF....
+                    // .0123456789ABC
+                    + " !\u0c9f\u0ca0\u0ca1\u0ca2\u0ca3\u0ca4)(\u0ca5\u0ca6,\u0ca7"
+                    + ".\u0ca80123456789:; "
+                    // D.....E.....F0.....1.....2.....3.....4.....5.....6.....7.....89.....A....
+                    // .B.....
+                    + "\u0caa\u0cab?\u0cac\u0cad\u0cae\u0caf\u0cb0\u0cb1\u0cb2\u0cb3 "
+                    + "\u0cb5\u0cb6\u0cb7"
+                    // C.....D.....E.....F.....0.....1.....2.....3.....4.....5.....6.....78.....9
+                    // .....
+                    + "\u0cb8\u0cb9\u0cbc\u0cbd\u0cbe\u0cbf\u0cc0\u0cc1\u0cc2\u0cc3\u0cc4 "
+                    + "\u0cc6\u0cc7"
+                    // A.....BC.....D.....E.....F.....0.....123456789ABCDEF0123456789AB.....C....
+                    // .D.....
+                    + "\u0cc8 \u0cca\u0ccb\u0ccc\u0ccd\u0cd5abcdefghijklmnopqrstuvwxyz\u0cd6"
+                    + "\u0ce0\u0ce1"
+                    // E.....F.....
+                    + "\u0ce2\u0ce3",
+
+            /* A.3.8 Malayalam National Language Locking Shift Table
+             01.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....CD.E.....F.....0....
+             .1 */
+            " \u0d02\u0d03\u0d05\u0d06\u0d07\u0d08\u0d09\u0d0a\u0d0b\n\u0d0c \r\u0d0e\u0d0f\u0d10 "
+                    // 2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.
+                    // ....F.....
+                    + "\u0d12\u0d13\u0d14\u0d15\u0d16\u0d17\u0d18\u0d19\u0d1a\uffff\u0d1b\u0d1c"
+                    + "\u0d1d\u0d1e"
+                    // 012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF....
+                    // .0123456789ABC
+                    + " !\u0d1f\u0d20\u0d21\u0d22\u0d23\u0d24)(\u0d25\u0d26,\u0d27"
+                    + ".\u0d280123456789:; "
+                    // D.....E.....F0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A
+                    // .....
+                    + "\u0d2a\u0d2b?\u0d2c\u0d2d\u0d2e\u0d2f\u0d30\u0d31\u0d32\u0d33\u0d34\u0d35"
+                    + "\u0d36"
+                    // B.....C.....D.....EF.....0.....1.....2.....3.....4.....5.....6.....78....
+                    // .9.....
+                    + "\u0d37\u0d38\u0d39 \u0d3d\u0d3e\u0d3f\u0d40\u0d41\u0d42\u0d43\u0d44 "
+                    + "\u0d46\u0d47"
+                    // A.....BC.....D.....E.....F.....0.....123456789ABCDEF0123456789AB.....C....
+                    // .D.....
+                    + "\u0d48 \u0d4a\u0d4b\u0d4c\u0d4d\u0d57abcdefghijklmnopqrstuvwxyz\u0d60"
+                    + "\u0d61\u0d62"
+                    // E.....F.....
+                    + "\u0d63\u0d79",
+
+            /* A.3.9 Oriya National Language Locking Shift Table
+             0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....CD.EF.....0....
+             .12 */
+            "\u0b01\u0b02\u0b03\u0b05\u0b06\u0b07\u0b08\u0b09\u0b0a\u0b0b\n\u0b0c \r \u0b0f\u0b10  "
+                    // 3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F.
+                    // ....01
+                    + "\u0b13\u0b14\u0b15\u0b16\u0b17\u0b18\u0b19\u0b1a\uffff\u0b1b\u0b1c\u0b1d"
+                    + "\u0b1e !"
+                    // 2.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF....
+                    // .0123456789ABCD.....
+                    + "\u0b1f\u0b20\u0b21\u0b22\u0b23\u0b24)(\u0b25\u0b26,\u0b27"
+                    + ".\u0b280123456789:; \u0b2a"
+                    // E.....F0.....1.....2.....3.....4.....56.....7.....89.....A.....B.....C....
+                    // .D.....
+                    + "\u0b2b?\u0b2c\u0b2d\u0b2e\u0b2f\u0b30 \u0b32\u0b33 "
+                    + "\u0b35\u0b36\u0b37\u0b38\u0b39"
+                    // E.....F.....0.....1.....2.....3.....4.....5.....6.....789.....A.....BCD...
+                    // ..E.....
+                    + "\u0b3c\u0b3d\u0b3e\u0b3f\u0b40\u0b41\u0b42\u0b43\u0b44  \u0b47\u0b48  "
+                    + "\u0b4b\u0b4c"
+                    // F.....0.....123456789ABCDEF0123456789AB.....C.....D.....E.....F.....
+                    + "\u0b4d\u0b56abcdefghijklmnopqrstuvwxyz\u0b57\u0b60\u0b61\u0b62\u0b63",
+
+            /* A.3.10 Punjabi National Language Locking Shift Table
+             0.....1.....2.....3.....4.....5.....6.....7.....8.....9A.BCD.EF.....0.....123.....4.
+             ....*/
+            "\u0a01\u0a02\u0a03\u0a05\u0a06\u0a07\u0a08\u0a09\u0a0a \n  \r \u0a0f\u0a10  "
+                    + "\u0a13\u0a14"
+                    // 5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F.....012....
+                    // .3.....
+                    + "\u0a15\u0a16\u0a17\u0a18\u0a19\u0a1a\uffff\u0a1b\u0a1c\u0a1d\u0a1e "
+                    + "!\u0a1f\u0a20"
+                    // 4.....5.....6.....7.....89A.....B.....CD.....EF.....0123456789ABCD.....E..
+                    // ...F0.....
+                    + "\u0a21\u0a22\u0a23\u0a24)(\u0a25\u0a26,\u0a27.\u0a280123456789:; "
+                    + "\u0a2a\u0a2b?\u0a2c"
+                    // 1.....2.....3.....4.....56.....7.....89.....A.....BC.....D.....E.....F0...
+                    // ..1.....
+                    + "\u0a2d\u0a2e\u0a2f\u0a30 \u0a32\u0a33 \u0a35\u0a36 \u0a38\u0a39\u0a3c "
+                    + "\u0a3e\u0a3f"
+                    // 2.....3.....4.....56789.....A.....BCD.....E.....F.....0....
+                    // .123456789ABCDEF012345678
+                    + "\u0a40\u0a41\u0a42    \u0a47\u0a48  "
+                    + "\u0a4b\u0a4c\u0a4d\u0a51abcdefghijklmnopqrstuvwx"
+                    // 9AB.....C.....D.....E.....F.....
+                    + "yz\u0a70\u0a71\u0a72\u0a73\u0a74",
+
+            /* A.3.11 Tamil National Language Locking Shift Table
+             01.....2.....3.....4.....5.....6.....7.....8.....9A.BCD.E.....F.....0.....12.....3..
+             ... */
+            " \u0b82\u0b83\u0b85\u0b86\u0b87\u0b88\u0b89\u0b8a \n  \r\u0b8e\u0b8f\u0b90 "
+                    + "\u0b92\u0b93"
+                    // 4.....5.....6789.....A.....B.....CD.....EF.....012.....3456.....7....
+                    // .89ABCDEF.....
+                    + "\u0b94\u0b95   \u0b99\u0b9a\uffff \u0b9c \u0b9e !\u0b9f   \u0ba3\u0ba4)(  "
+                    + ", .\u0ba8"
+                    // 0123456789ABC.....D.....EF012.....3.....4.....5.....6.....7.....8.....9...
+                    // ..A.....
+                    + "0123456789:;\u0ba9\u0baa ?  "
+                    + "\u0bae\u0baf\u0bb0\u0bb1\u0bb2\u0bb3\u0bb4\u0bb5\u0bb6"
+                    // B.....C.....D.....EF0.....1.....2.....3.....4.....5678.....9.....A.....BC.
+                    // ....D.....
+                    + "\u0bb7\u0bb8\u0bb9  \u0bbe\u0bbf\u0bc0\u0bc1\u0bc2   \u0bc6\u0bc7\u0bc8 "
+                    + "\u0bca\u0bcb"
+                    // E.....F.....0.....123456789ABCDEF0123456789AB.....C.....D.....E.....F.....
+                    + "\u0bcc\u0bcd\u0bd0abcdefghijklmnopqrstuvwxyz\u0bd7\u0bf0\u0bf1\u0bf2\u0bf9",
+
+            /* A.3.12 Telugu National Language Locking Shift Table
+             0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....CD.E.....F.....0
+             .....*/
+            "\u0c01\u0c02\u0c03\u0c05\u0c06\u0c07\u0c08\u0c09\u0c0a\u0c0b\n\u0c0c "
+                    + "\r\u0c0e\u0c0f\u0c10"
+                    // 12.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E
+                    // .....
+                    + " \u0c12\u0c13\u0c14\u0c15\u0c16\u0c17\u0c18\u0c19\u0c1a\uffff\u0c1b\u0c1c"
+                    + "\u0c1d"
+                    // F.....012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF....
+                    // .0123456789AB
+                    + "\u0c1e !\u0c1f\u0c20\u0c21\u0c22\u0c23\u0c24)(\u0c25\u0c26,\u0c27"
+                    + ".\u0c280123456789:;"
+                    // CD.....E.....F0.....1.....2.....3.....4.....5.....6.....7.....89.....A....
+                    // .B.....
+                    + " \u0c2a\u0c2b?\u0c2c\u0c2d\u0c2e\u0c2f\u0c30\u0c31\u0c32\u0c33 "
+                    + "\u0c35\u0c36\u0c37"
+                    // C.....D.....EF.....0.....1.....2.....3.....4.....5.....6.....78.....9....
+                    // .A.....B
+                    + "\u0c38\u0c39 \u0c3d\u0c3e\u0c3f\u0c40\u0c41\u0c42\u0c43\u0c44 "
+                    + "\u0c46\u0c47\u0c48 "
+                    // C.....D.....E.....F.....0.....123456789ABCDEF0123456789AB.....C.....D....
+                    // .E.....
+                    + "\u0c4a\u0c4b\u0c4c\u0c4d\u0c55abcdefghijklmnopqrstuvwxyz\u0c56\u0c60\u0c61"
+                    + "\u0c62"
+                    // F.....
+                    + "\u0c63",
+
+            /* A.3.13 Urdu National Language Locking Shift Table
+             0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.E.....F.
+             ....*/
+            "\u0627\u0622\u0628\u067b\u0680\u067e\u06a6\u062a\u06c2\u067f\n\u0679\u067d\r\u067a"
+                    + "\u067c"
+                    // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.
+                    // ....D.....
+                    + "\u062b\u062c\u0681\u0684\u0683\u0685\u0686\u0687\u062d\u062e\u062f\uffff"
+                    + "\u068c\u0688"
+                    // E.....F.....012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF.
+                    // ....012345
+                    + "\u0689\u068a !\u068f\u068d\u0630\u0631\u0691\u0693)(\u0699\u0632,\u0696"
+                    + ".\u0698012345"
+                    // 6789ABC.....D.....E.....F0.....1.....2.....3.....4.....5.....6.....7.....8
+                    // .....
+                    + "6789:;\u069a\u0633\u0634?\u0635\u0636\u0637\u0638\u0639\u0641\u0642\u06a9"
+                    + "\u06aa"
+                    // 9.....A.....B.....C.....D.....E.....F.....0.....1.....2.....3.....4.....5.
+                    // ....6.....
+                    + "\u06ab\u06af\u06b3\u06b1\u0644\u0645\u0646\u06ba\u06bb\u06bc\u0648\u06c4"
+                    + "\u06d5\u06c1"
+                    // 7.....8.....9.....A.....B.....C.....D.....E.....F.....0....
+                    // .123456789ABCDEF012345678
+                    + "\u06be\u0621\u06cc\u06d0\u06d2\u064d\u0650\u064f\u0657"
+                    + "\u0654abcdefghijklmnopqrstuvwx"
+                    // 9AB.....C.....D.....E.....F.....
+                    + "yz\u0655\u0651\u0653\u0656\u0670"
+    };
+
+    /**
+     * GSM default extension table plus national language single shift character tables.
+     */
+    @UnsupportedAppUsage
+    private static final String[] sLanguageShiftTables = new String[]{
+            /* 6.2.1.1 GSM 7 bit Default Alphabet Extension Table
+             0123456789A.....BCDEF0123456789ABCDEF0123456789ABCDEF
+             .0123456789ABCDEF0123456789ABCDEF */
+            "          \u000c         ^                   {}     \\            [~] |               "
+                    // 0123456789ABCDEF012345.....6789ABCDEF0123456789ABCDEF
+                    + "                     \u20ac                          ",
+
+            /* A.2.1 Turkish National Language Single Shift Table
+             0123456789A.....BCDEF0123456789ABCDEF0123456789ABCDEF.0123456789ABCDEF01234567.....8 */
+            "          \u000c         ^                   {}     \\            [~] |      \u011e "
+                    // 9.....ABCDEF0123.....456789ABCDEF0123.....45.....67.....89.....ABCDEF0123.
+                    // ....
+                    + "\u0130         \u015e               \u00e7 \u20ac \u011f \u0131         "
+                    + "\u015f"
+                    // 456789ABCDEF
+                    + "            ",
+
+            /* A.2.2 Spanish National Language Single Shift Table
+             0123456789.....A.....BCDEF0123456789ABCDEF0123456789ABCDEF.0123456789ABCDEF01.....23 */
+            "         \u00e7\u000c         ^                   {}     \\            [~] |\u00c1  "
+                    // 456789.....ABCDEF.....012345.....6789ABCDEF01.....2345.....6789.....ABCDEF
+                    // .....012
+                    + "     \u00cd     \u00d3     \u00da           \u00e1   \u20ac   \u00ed     "
+                    + "\u00f3   "
+                    // 345.....6789ABCDEF
+                    + "  \u00fa          ",
+
+            /* A.2.3 Portuguese National Language Single Shift Table
+             012345.....6789.....A.....B.....C.....DE.....F.....012.....3.....45.....6.....7....
+             .8....*/
+            "     \u00ea   \u00e7\u000c\u00d4\u00f4 \u00c1\u00e1  "
+                    + "\u03a6\u0393^\u03a9\u03a0\u03a8\u03a3"
+                    // 9.....ABCDEF.....0123456789ABCDEF.0123456789ABCDEF01.....23456789.....ABCDE
+                    + "\u0398     \u00ca        {}     \\            [~] |\u00c0       \u00cd     "
+                    // F.....012345.....6789AB.....C.....DEF01.....2345.....6789.....ABCDEF....
+                    // .01234
+                    + "\u00d3     \u00da     \u00c3\u00d5    \u00c2   \u20ac   \u00ed     \u00f3 "
+                    + "    "
+                    // 5.....6789AB.....C.....DEF.....
+                    + "\u00fa     \u00e3\u00f5  \u00e2",
+
+            /* A.2.4 Bengali National Language Single Shift Table
+             01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D...
+             .. */
+            "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u09e6\u09e7 \u09e8\u09e9"
+                    // E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89A.....B....
+                    // .C.....
+                    + "\u09ea\u09eb\u09ec\u09ed\u09ee\u09ef\u09df\u09e0\u09e1\u09e2{}\u09e3\u09f2"
+                    + "\u09f3"
+                    // D.....E.....F.0.....1.....2.....3.....4.....56789ABCDEF0123456789ABCDEF
+                    + "\u09f4\u09f5\\\u09f6\u09f7\u09f8\u09f9\u09fa       [~] |ABCDEFGHIJKLMNO"
+                    // 0123456789ABCDEF012345.....6789ABCDEF0123456789ABCDEF
+                    + "PQRSTUVWXYZ          \u20ac                          ",
+
+            /* A.2.5 Gujarati National Language Single Shift Table
+             01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D...
+             .. */
+            "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u0964\u0965 \u0ae6\u0ae7"
+                    // E.....F.....0.....1.....2.....3.....4.....5.....6789ABCDEF.0123456789ABCDEF
+                    + "\u0ae8\u0ae9\u0aea\u0aeb\u0aec\u0aed\u0aee\u0aef  {}     \\            [~] "
+                    // 0123456789ABCDEF0123456789ABCDEF012345.....6789ABCDEF0123456789ABCDEF
+                    + "|ABCDEFGHIJKLMNOPQRSTUVWXYZ          \u20ac                          ",
+
+            /* A.2.6 Hindi National Language Single Shift Table
+             01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D...
+             .. */
+            "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u0964\u0965 \u0966\u0967"
+                    // E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89A.....B....
+                    // .C.....
+                    + "\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f\u0951\u0952{}\u0953\u0954"
+                    + "\u0958"
+                    // D.....E.....F.0.....1.....2.....3.....4.....5.....6.....7.....8.....9....
+                    // .A.....
+                    + "\u0959\u095a\\\u095b\u095c\u095d\u095e\u095f\u0960\u0961\u0962\u0963\u0970"
+                    + "\u0971"
+                    // BCDEF0123456789ABCDEF0123456789ABCDEF012345.....6789ABCDEF0123456789ABCDEF
+                    + " [~] |ABCDEFGHIJKLMNOPQRSTUVWXYZ          \u20ac                          ",
+
+            /* A.2.7 Kannada National Language Single Shift Table
+             01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D...
+             .. */
+            "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u0964\u0965 \u0ce6\u0ce7"
+                    // E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89A.....BCDEF
+                    // .01234567
+                    + "\u0ce8\u0ce9\u0cea\u0ceb\u0cec\u0ced\u0cee\u0cef\u0cde\u0cf1{}\u0cf2    \\"
+                    + "        "
+                    // 89ABCDEF0123456789ABCDEF0123456789ABCDEF012345.....6789ABCDEF0123456789ABCDEF
+                    + "    [~] |ABCDEFGHIJKLMNOPQRSTUVWXYZ          \u20ac                       "
+                    + "   ",
+
+            /* A.2.8 Malayalam National Language Single Shift Table
+             01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D...
+             .. */
+            "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u0964\u0965 \u0d66\u0d67"
+                    // E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89A.....B....
+                    // .C.....
+                    + "\u0d68\u0d69\u0d6a\u0d6b\u0d6c\u0d6d\u0d6e\u0d6f\u0d70\u0d71{}\u0d72\u0d73"
+                    + "\u0d74"
+                    // D.....E.....F.0.....1.....2.....3.....4....
+                    // .56789ABCDEF0123456789ABCDEF0123456789A
+                    + "\u0d75\u0d7a\\\u0d7b\u0d7c\u0d7d\u0d7e\u0d7f       [~] "
+                    + "|ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                    // BCDEF012345.....6789ABCDEF0123456789ABCDEF
+                    + "          \u20ac                          ",
+
+            /* A.2.9 Oriya National Language Single Shift Table
+             01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D...
+             .. */
+            "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u0964\u0965 \u0b66\u0b67"
+                    // E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89A.....B....
+                    // .C.....DE
+                    + "\u0b68\u0b69\u0b6a\u0b6b\u0b6c\u0b6d\u0b6e\u0b6f\u0b5c\u0b5d{}\u0b5f\u0b70"
+                    + "\u0b71  "
+                    // F.0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF012345....
+                    // .6789ABCDEF0123456789A
+                    + "\\            [~] |ABCDEFGHIJKLMNOPQRSTUVWXYZ          \u20ac             "
+                    + "        "
+                    // BCDEF
+                    + "     ",
+
+            /* A.2.10 Punjabi National Language Single Shift Table
+             01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D...
+             .. */
+            "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u0964\u0965 \u0a66\u0a67"
+                    // E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89A.....B....
+                    // .C.....
+                    + "\u0a68\u0a69\u0a6a\u0a6b\u0a6c\u0a6d\u0a6e\u0a6f\u0a59\u0a5a{}\u0a5b\u0a5c"
+                    + "\u0a5e"
+                    // D.....EF.0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF012345....
+                    // .6789ABCDEF01
+                    + "\u0a75 \\            [~] |ABCDEFGHIJKLMNOPQRSTUVWXYZ          \u20ac      "
+                    + "      "
+                    // 23456789ABCDEF
+                    + "              ",
+
+            /* A.2.11 Tamil National Language Single Shift Table
+               NOTE: TS 23.038 V9.1.1 shows code 0x24 as \u0bef, corrected to \u0bee (typo)
+             01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D...
+             .. */
+            "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u0964\u0965 \u0be6\u0be7"
+                    // E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89A.....B....
+                    // .C.....
+                    + "\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef\u0bf3\u0bf4{}\u0bf5\u0bf6"
+                    + "\u0bf7"
+                    // D.....E.....F.0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF012345....
+                    // .6789ABC
+                    + "\u0bf8\u0bfa\\            [~] |ABCDEFGHIJKLMNOPQRSTUVWXYZ          \u20ac "
+                    + "      "
+                    // DEF0123456789ABCDEF
+                    + "                   ",
+
+            /* A.2.12 Telugu National Language Single Shift Table
+               NOTE: TS 23.038 V9.1.1 shows code 0x22-0x23 as \u06cc\u06cd, corrected to
+               \u0c6c\u0c6d
+             01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789ABC.....D.....E.....F.
+             .... */
+            "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*   "
+                    + "\u0c66\u0c67\u0c68\u0c69"
+                    // 0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....D....
+                    // .E.....F.
+                    + "\u0c6a\u0c6b\u0c6c\u0c6d\u0c6e\u0c6f\u0c58\u0c59{}\u0c78\u0c79\u0c7a\u0c7b"
+                    + "\u0c7c\\"
+                    // 0.....1.....2.....3456789ABCDEF0123456789ABCDEF0123456789ABCDEF012345....
+                    // .6789ABCD
+                    + "\u0c7d\u0c7e\u0c7f         [~] |ABCDEFGHIJKLMNOPQRSTUVWXYZ          \u20ac"
+                    + "        "
+                    // EF0123456789ABCDEF
+                    + "                  ",
+
+            /* A.2.13 Urdu National Language Single Shift Table
+             01.....23.....4.....5.6.....789A.....BCDEF0123.....45.....6789.....A.....BC.....D...
+             .. */
+            "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+ -/<=>\u00a1^\u00a1_#*\u0600\u0601 \u06f0\u06f1"
+                    // E.....F.....0.....1.....2.....3.....4.....5.....6.....7.....89A.....B....
+                    // .C.....
+                    + "\u06f2\u06f3\u06f4\u06f5\u06f6\u06f7\u06f8\u06f9\u060c\u060d{}\u060e\u060f"
+                    + "\u0610"
+                    // D.....E.....F.0.....1.....2.....3.....4.....5.....6.....7.....8.....9....
+                    // .A.....
+                    + "\u0611\u0612\\\u0613\u0614\u061b\u061f\u0640\u0652\u0658\u066b\u066c\u0672"
+                    + "\u0673"
+                    // B.....CDEF.....0123456789ABCDEF0123456789ABCDEF012345....
+                    // .6789ABCDEF0123456789ABCDEF
+                    + "\u06cd[~]\u06d4|ABCDEFGHIJKLMNOPQRSTUVWXYZ          \u20ac                "
+                    + "          "
+    };
+
+    static {
+        int numTables = sLanguageTables.length;
+        int numShiftTables = sLanguageShiftTables.length;
+        if (numTables != numShiftTables) {
+            Rlog.e(TAG, "Error: language tables array length " + numTables
+                    + " != shift tables array length " + numShiftTables);
+        }
+
+        sCharsToGsmTables = new SparseIntArray[numTables];
+        for (int i = 0; i < numTables; i++) {
+            String table = sLanguageTables[i];
+
+            int tableLen = table.length();
+            if (tableLen != 0 && tableLen != 128) {
+                Rlog.e(TAG, "Error: language tables index " + i + " length " + tableLen
+                        + " (expected 128 or 0)");
+            }
+
+            SparseIntArray charToGsmTable = new SparseIntArray(tableLen);
+            sCharsToGsmTables[i] = charToGsmTable;
+            for (int j = 0; j < tableLen; j++) {
+                char c = table.charAt(j);
+                charToGsmTable.put(c, j);
+            }
+        }
+
+        sCharsToShiftTables = new SparseIntArray[numShiftTables];
+        for (int i = 0; i < numShiftTables; i++) {
+            String shiftTable = sLanguageShiftTables[i];
+
+            int shiftTableLen = shiftTable.length();
+            if (shiftTableLen != 0 && shiftTableLen != 128) {
+                Rlog.e(TAG, "Error: language shift tables index " + i + " length " + shiftTableLen
+                        + " (expected 128 or 0)");
+            }
+
+            SparseIntArray charToShiftTable = new SparseIntArray(shiftTableLen);
+            sCharsToShiftTables[i] = charToShiftTable;
+            for (int j = 0; j < shiftTableLen; j++) {
+                char c = shiftTable.charAt(j);
+                if (c != ' ') {
+                    charToShiftTable.put(c, j);
+                }
+            }
+        }
+    }
+
+    /**
+     * Convert a GSM alphabet 7 bit packed string (SMS string) into a
+     * {@link java.lang.String}.
+     *
+     * See TS 23.038 6.1.2.1 for SMS Character Packing
+     *
+     * @param pdu           the raw data from the pdu
+     * @param offset        the byte offset of
+     * @param lengthSeptets string length in septets, not bytes
+     * @return String representation or null on decoding exception
+     */
+    @UnsupportedAppUsage
+    public static String gsm7BitPackedToString(byte[] pdu, int offset,
+            int lengthSeptets) {
+        return gsm7BitPackedToString(pdu, offset, lengthSeptets, 0, 0, 0);
+    }
+
+    /**
+     * Convert a GSM alphabet 7 bit packed string (SMS string) into a
+     * {@link java.lang.String}.
+     *
+     * See TS 23.038 6.1.2.1 for SMS Character Packing
+     *
+     * @param pdu            the raw data from the pdu
+     * @param offset         the byte offset of
+     * @param lengthSeptets  string length in septets, not bytes
+     * @param numPaddingBits the number of padding bits before the start of the
+     *                       string in the first byte
+     * @param languageTable  the 7 bit language table, or 0 for the default GSM alphabet
+     * @param shiftTable     the 7 bit single shift language table, or 0 for the default
+     *                       GSM extension table
+     * @return String representation or null on decoding exception
+     */
+    @UnsupportedAppUsage
+    public static String gsm7BitPackedToString(byte[] pdu, int offset,
+            int lengthSeptets, int numPaddingBits, int languageTable, int shiftTable) {
+        StringBuilder ret = new StringBuilder(lengthSeptets);
+
+        if (languageTable < 0 || languageTable > sLanguageTables.length) {
+            Rlog.w(TAG, "unknown language table " + languageTable + ", using default");
+            languageTable = 0;
+        }
+        if (shiftTable < 0 || shiftTable > sLanguageShiftTables.length) {
+            Rlog.w(TAG, "unknown single shift table " + shiftTable + ", using default");
+            shiftTable = 0;
+        }
+
+        try {
+            boolean prevCharWasEscape = false;
+            String languageTableToChar = sLanguageTables[languageTable];
+            String shiftTableToChar = sLanguageShiftTables[shiftTable];
+
+            if (languageTableToChar.isEmpty()) {
+                Rlog.w(TAG, "no language table for code " + languageTable + ", using default");
+                languageTableToChar = sLanguageTables[0];
+            }
+            if (shiftTableToChar.isEmpty()) {
+                Rlog.w(TAG, "no single shift table for code " + shiftTable + ", using default");
+                shiftTableToChar = sLanguageShiftTables[0];
+            }
+
+            for (int i = 0; i < lengthSeptets; i++) {
+                int bitOffset = (7 * i) + numPaddingBits;
+
+                int byteOffset = bitOffset / 8;
+                int shift = bitOffset % 8;
+                int gsmVal;
+
+                gsmVal = (0x7f & (pdu[offset + byteOffset] >> shift));
+
+                // if it crosses a byte boundary
+                if (shift > 1) {
+                    // set msb bits to 0
+                    gsmVal &= 0x7f >> (shift - 1);
+
+                    gsmVal |= 0x7f & (pdu[offset + byteOffset + 1] << (8 - shift));
+                }
+
+                if (prevCharWasEscape) {
+                    if (gsmVal == GSM_EXTENDED_ESCAPE) {
+                        ret.append(' ');    // display ' ' for reserved double escape sequence
+                    } else {
+                        char c = shiftTableToChar.charAt(gsmVal);
+                        if (c == ' ') {
+                            ret.append(languageTableToChar.charAt(gsmVal));
+                        } else {
+                            ret.append(c);
+                        }
+                    }
+                    prevCharWasEscape = false;
+                } else if (gsmVal == GSM_EXTENDED_ESCAPE) {
+                    prevCharWasEscape = true;
+                } else {
+                    ret.append(languageTableToChar.charAt(gsmVal));
+                }
+            }
+        } catch (RuntimeException ex) {
+            Rlog.e(TAG, "Error GSM 7 bit packed: ", ex);
+            return null;
+        }
+
+        return ret.toString();
+    }
+}
diff --git a/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java b/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java
index 6838497..637edd4 100644
--- a/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java
+++ b/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java
@@ -16,14 +16,15 @@
 
 package com.android.cellbroadcastservice;
 
-import static com.android.internal.telephony.gsm.SmsCbConstants.MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER;
-
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.Context;
+import android.content.res.Resources;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Looper;
 import android.os.Message;
+import android.os.SystemClock;
 import android.provider.Telephony.CellBroadcasts;
 import android.telephony.CbGeoUtils.Geometry;
 import android.telephony.CellIdentity;
@@ -31,15 +32,16 @@
 import android.telephony.CellInfo;
 import android.telephony.SmsCbLocation;
 import android.telephony.SmsCbMessage;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.text.format.DateUtils;
 import android.util.Pair;
 
 import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage;
 import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity;
+import com.android.internal.annotations.VisibleForTesting;
 
-import dalvik.annotation.compat.UnsupportedAppUsage;
-
+import java.text.DateFormat;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -55,12 +57,12 @@
     private static final String MESSAGE_NOT_BROADCASTED = "0";
 
     /** This map holds incomplete concatenated messages waiting for assembly. */
-    @UnsupportedAppUsage
     private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
             new HashMap<>(4);
 
-    protected GsmCellBroadcastHandler(Context context) {
-        super("GsmCellBroadcastHandler", context);
+    @VisibleForTesting
+    protected GsmCellBroadcastHandler(Context context, Looper looper) {
+        super("GsmCellBroadcastHandler", context, looper);
     }
 
     @Override
@@ -82,7 +84,7 @@
      * @return the new handler
      */
     public static GsmCellBroadcastHandler makeGsmCellBroadcastHandler(Context context) {
-        GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context);
+        GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context, Looper.myLooper());
         handler.start();
         return handler;
     }
@@ -99,9 +101,26 @@
         final List<SmsCbMessage> cbMessages = new ArrayList<>();
         final List<Uri> cbMessageUris = new ArrayList<>();
 
+        SubscriptionManager subMgr = (SubscriptionManager) mContext.getSystemService(
+                Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+        int[] subIds = subMgr.getSubscriptionIds(slotIndex);
+        Resources res;
+        if (subIds != null) {
+            res = getResources(subIds[0]);
+        } else {
+            res = getResources(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
+        }
+
         // Only consider the cell broadcast received within 24 hours.
         long lastReceivedTime = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS;
 
+        // Some carriers require reset duplication detection after airplane mode or reboot.
+        if (res.getBoolean(R.bool.reset_on_power_cycle_or_airplane_mode)) {
+            lastReceivedTime = Long.max(lastReceivedTime, mLastAirplaneModeTime);
+            lastReceivedTime = Long.max(lastReceivedTime,
+                    System.currentTimeMillis() - SystemClock.elapsedRealtime());
+        }
+
         // Find the cell broadcast message identify by the message identifier and serial number
         // and is not broadcasted.
         String where = CellBroadcasts.SERVICE_CATEGORY + "=? AND "
@@ -112,7 +131,7 @@
         ContentResolver resolver = mContext.getContentResolver();
         for (CellBroadcastIdentity identity : geoFencingTriggerMessage.cbIdentifiers) {
             try (Cursor cursor = resolver.query(CellBroadcasts.CONTENT_URI,
-                    CellBroadcasts.QUERY_COLUMNS_FWK,
+                    CellBroadcastProvider.QUERY_COLUMNS,
                     where,
                     new String[] { Integer.toString(identity.messageIdentifier),
                             Integer.toString(identity.serialNumber), MESSAGE_NOT_BROADCASTED,
@@ -128,6 +147,9 @@
             }
         }
 
+        log("Found " + cbMessages.size() + " not broadcasted messages since "
+                + DateFormat.getDateTimeInstance().format(lastReceivedTime));
+
         List<Geometry> commonBroadcastArea = new ArrayList<>();
         if (geoFencingTriggerMessage.shouldShareBroadcastArea()) {
             for (SmsCbMessage msg : cbMessages) {
@@ -194,7 +216,8 @@
             SmsCbHeader header = createSmsCbHeader(pdu);
             if (header == null) return false;
 
-            if (header.getServiceCategory() == MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER) {
+            log("header=" + header);
+            if (header.getServiceCategory() == SmsCbConstants.MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER) {
                 GeoFencingTriggerMessage triggerMessage =
                         GsmSmsCbMessage.createGeoFencingTriggerMessage(pdu);
                 if (triggerMessage != null) {
@@ -365,7 +388,6 @@
         private final SmsCbHeader mHeader;
         private final SmsCbLocation mLocation;
 
-        @UnsupportedAppUsage
         SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) {
             mHeader = header;
             mLocation = location;
@@ -401,7 +423,6 @@
          * @param cid the current Cell ID
          * @return true if this message is valid for the current location; false otherwise
          */
-        @UnsupportedAppUsage
         public boolean matchesLocation(String plmn, int lac, int cid) {
             return mLocation.isInLocationArea(plmn, lac, cid);
         }
diff --git a/src/com/android/cellbroadcastservice/GsmSmsCbMessage.java b/src/com/android/cellbroadcastservice/GsmSmsCbMessage.java
index c0ded3b..62a7b90 100644
--- a/src/com/android/cellbroadcastservice/GsmSmsCbMessage.java
+++ b/src/com/android/cellbroadcastservice/GsmSmsCbMessage.java
@@ -27,18 +27,17 @@
 import android.content.res.Resources;
 import android.telephony.CbGeoUtils.Geometry;
 import android.telephony.CbGeoUtils.LatLng;
+import android.telephony.Rlog;
 import android.telephony.SmsCbLocation;
 import android.telephony.SmsCbMessage;
+import android.telephony.SmsMessage;
 import android.util.Pair;
-import android.util.Slog;
 
 import com.android.cellbroadcastservice.CbGeoUtils.Circle;
 import com.android.cellbroadcastservice.CbGeoUtils.Polygon;
 import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity;
 import com.android.cellbroadcastservice.SmsCbHeader.DataCodingScheme;
-import com.android.internal.R;
-import com.android.internal.telephony.GsmAlphabet;
-import com.android.internal.telephony.SmsConstants;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
@@ -67,7 +66,8 @@
      * @param category ETWS message category defined in SmsCbConstants
      * @return ETWS text message in string. Return an empty string if no match.
      */
-    private static String getEtwsPrimaryMessage(Context context, int category) {
+    @VisibleForTesting
+    public static String getEtwsPrimaryMessage(Context context, int category) {
         final Resources r = context.getResources();
         switch (category) {
             case ETWS_WARNING_TYPE_EARTHQUAKE:
@@ -103,7 +103,7 @@
                     header.getSerialNumber(), location, header.getServiceCategory(), null,
                     getEtwsPrimaryMessage(context, header.getEtwsInfo().getWarningType()),
                     SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, header.getEtwsInfo(),
-                    header.getCmasInfo(), 0, null /* geometries */, receivedTimeMillis, slotIndex);
+                    header.getCmasInfo(), slotIndex);
         } else if (header.isUmtsFormat()) {
             // UMTS format has only 1 PDU
             byte[] pdu = pdus[0];
@@ -129,7 +129,7 @@
                 } catch (Exception ex) {
                     // Catch the exception here, the message will be considered as having no WAC
                     // information which means the message will be broadcasted directly.
-                    Slog.e(TAG, "Can't parse warning area coordinates, ex = " + ex.toString());
+                    Rlog.e(TAG, "Can't parse warning area coordinates, ex = " + ex.toString());
                 }
             }
 
@@ -152,8 +152,7 @@
             return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
                     header.getGeographicalScope(), header.getSerialNumber(), location,
                     header.getServiceCategory(), language, sb.toString(), priority,
-                    header.getEtwsInfo(), header.getCmasInfo(), 0, null /* geometries */,
-                    receivedTimeMillis, slotIndex);
+                    header.getEtwsInfo(), header.getCmasInfo(), slotIndex);
         }
     }
 
@@ -196,7 +195,7 @@
             }
             return new GeoFencingTriggerMessage(type, cbIdentifiers);
         } catch (Exception ex) {
-            Slog.e(TAG, "create geo-fencing trigger failed, ex = " + ex.toString());
+            Rlog.e(TAG, "create geo-fencing trigger failed, ex = " + ex.toString());
             return null;
         }
     }
@@ -213,7 +212,7 @@
     private static Pair<Integer, List<Geometry>> parseWarningAreaCoordinates(
             byte[] pdu, int wacOffset) {
         // little-endian
-        int wacDataLength = (pdu[wacOffset + 1] << 8) | pdu[wacOffset];
+        int wacDataLength = ((pdu[wacOffset + 1] & 0xff) << 8) | (pdu[wacOffset] & 0xff);
         int offset = wacOffset + 2;
 
         if (offset + wacDataLength > pdu.length) {
@@ -289,7 +288,8 @@
      * @param pdu the PDU to decode
      * @return a pair of string containing the language and body of the message in order
      */
-    private static Pair<String, String> parseUmtsBody(SmsCbHeader header, byte[] pdu) {
+    private static Pair<String, String> parseUmtsBody(SmsCbHeader header,
+            byte[] pdu) {
         // Payload may contain multiple pages
         int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH];
         String language = header.getDataCodingSchemeStructedData().language;
@@ -328,7 +328,8 @@
      * @param pdu the PDU to decode
      * @return a pair of string containing the language and body of the message in order
      */
-    private static Pair<String, String> parseGsmBody(SmsCbHeader header, byte[] pdu) {
+    private static Pair<String, String> parseGsmBody(SmsCbHeader header,
+            byte[] pdu) {
         // Payload is one single page
         int offset = SmsCbHeader.PDU_HEADER_LENGTH;
         int length = pdu.length - offset;
@@ -344,13 +345,13 @@
      * @param dcs data coding scheme
      * @return a Pair of Strings containing the language and body of the message
      */
-    private static Pair<String, String> unpackBody(byte[] pdu, int offset, int length,
-            DataCodingScheme dcs) {
+    private static Pair<String, String> unpackBody(byte[] pdu, int offset,
+            int length, DataCodingScheme dcs) {
         String body = null;
 
         String language = dcs.language;
         switch (dcs.encoding) {
-            case SmsConstants.ENCODING_7BIT:
+            case SmsMessage.ENCODING_7BIT:
                 body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7);
 
                 if (dcs.hasLanguageIndicator && body != null && body.length() > 2) {
@@ -361,7 +362,7 @@
                 }
                 break;
 
-            case SmsConstants.ENCODING_16BIT:
+            case SmsMessage.ENCODING_16BIT:
                 if (dcs.hasLanguageIndicator && pdu.length >= offset + 2) {
                     // Language is two GSM characters.
                     // The actual body text is offset by 2 bytes.
diff --git a/src/com/android/cellbroadcastservice/SmsCbHeader.java b/src/com/android/cellbroadcastservice/SmsCbHeader.java
index 2a859fa..8dd8807 100644
--- a/src/com/android/cellbroadcastservice/SmsCbHeader.java
+++ b/src/com/android/cellbroadcastservice/SmsCbHeader.java
@@ -18,8 +18,7 @@
 
 import android.telephony.SmsCbCmasInfo;
 import android.telephony.SmsCbEtwsInfo;
-
-import com.android.internal.telephony.SmsConstants;
+import android.telephony.SmsMessage;
 
 import dalvik.annotation.compat.UnsupportedAppUsage;
 
@@ -522,42 +521,42 @@
             // section 5.
             switch ((dataCodingScheme & 0xf0) >> 4) {
                 case 0x00:
-                    encoding = SmsConstants.ENCODING_7BIT;
+                    encoding = SmsMessage.ENCODING_7BIT;
                     language = LANGUAGE_CODES_GROUP_0[dataCodingScheme & 0x0f];
                     break;
 
                 case 0x01:
                     hasLanguageIndicator = true;
                     if ((dataCodingScheme & 0x0f) == 0x01) {
-                        encoding = SmsConstants.ENCODING_16BIT;
+                        encoding = SmsMessage.ENCODING_16BIT;
                     } else {
-                        encoding = SmsConstants.ENCODING_7BIT;
+                        encoding = SmsMessage.ENCODING_7BIT;
                     }
                     break;
 
                 case 0x02:
-                    encoding = SmsConstants.ENCODING_7BIT;
+                    encoding = SmsMessage.ENCODING_7BIT;
                     language = LANGUAGE_CODES_GROUP_2[dataCodingScheme & 0x0f];
                     break;
 
                 case 0x03:
-                    encoding = SmsConstants.ENCODING_7BIT;
+                    encoding = SmsMessage.ENCODING_7BIT;
                     break;
 
                 case 0x04:
                 case 0x05:
                     switch ((dataCodingScheme & 0x0c) >> 2) {
                         case 0x01:
-                            encoding = SmsConstants.ENCODING_8BIT;
+                            encoding = SmsMessage.ENCODING_8BIT;
                             break;
 
                         case 0x02:
-                            encoding = SmsConstants.ENCODING_16BIT;
+                            encoding = SmsMessage.ENCODING_16BIT;
                             break;
 
                         case 0x00:
                         default:
-                            encoding = SmsConstants.ENCODING_7BIT;
+                            encoding = SmsMessage.ENCODING_7BIT;
                             break;
                     }
                     break;
@@ -574,15 +573,15 @@
 
                 case 0x0f:
                     if (((dataCodingScheme & 0x04) >> 2) == 0x01) {
-                        encoding = SmsConstants.ENCODING_8BIT;
+                        encoding = SmsMessage.ENCODING_8BIT;
                     } else {
-                        encoding = SmsConstants.ENCODING_7BIT;
+                        encoding = SmsMessage.ENCODING_7BIT;
                     }
                     break;
 
                 default:
                     // Reserved values are to be treated as 7-bit
-                    encoding = SmsConstants.ENCODING_7BIT;
+                    encoding = SmsMessage.ENCODING_7BIT;
                     break;
             }
 
diff --git a/src/com/android/cellbroadcastservice/SmsHeader.java b/src/com/android/cellbroadcastservice/SmsHeader.java
new file mode 100644
index 0000000..2449f98
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/SmsHeader.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2006 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 java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+
+/**
+ * SMS user data header, as specified in TS 23.040 9.2.3.24.
+ */
+public class SmsHeader {
+
+    // TODO(cleanup): this data structure is generally referred to as
+    // the 'user data header' or UDH, and so the class name should
+    // change to reflect this...
+
+    /**
+     * SMS user data header information element identifiers.
+     * (see TS 23.040 9.2.3.24)
+     */
+    public static final int ELT_ID_CONCATENATED_8_BIT_REFERENCE = 0x00;
+    public static final int ELT_ID_SPECIAL_SMS_MESSAGE_INDICATION = 0x01;
+    public static final int ELT_ID_APPLICATION_PORT_ADDRESSING_8_BIT = 0x04;
+    public static final int ELT_ID_APPLICATION_PORT_ADDRESSING_16_BIT = 0x05;
+    public static final int ELT_ID_SMSC_CONTROL_PARAMS = 0x06;
+    public static final int ELT_ID_UDH_SOURCE_INDICATION = 0x07;
+    public static final int ELT_ID_CONCATENATED_16_BIT_REFERENCE = 0x08;
+    public static final int ELT_ID_WIRELESS_CTRL_MSG_PROTOCOL = 0x09;
+    public static final int ELT_ID_TEXT_FORMATTING = 0x0A;
+    public static final int ELT_ID_PREDEFINED_SOUND = 0x0B;
+    public static final int ELT_ID_USER_DEFINED_SOUND = 0x0C;
+    public static final int ELT_ID_PREDEFINED_ANIMATION = 0x0D;
+    public static final int ELT_ID_LARGE_ANIMATION = 0x0E;
+    public static final int ELT_ID_SMALL_ANIMATION = 0x0F;
+    public static final int ELT_ID_LARGE_PICTURE = 0x10;
+    public static final int ELT_ID_SMALL_PICTURE = 0x11;
+    public static final int ELT_ID_VARIABLE_PICTURE = 0x12;
+    public static final int ELT_ID_USER_PROMPT_INDICATOR = 0x13;
+    public static final int ELT_ID_EXTENDED_OBJECT = 0x14;
+    public static final int ELT_ID_REUSED_EXTENDED_OBJECT = 0x15;
+    public static final int ELT_ID_COMPRESSION_CONTROL = 0x16;
+    public static final int ELT_ID_OBJECT_DISTR_INDICATOR = 0x17;
+    public static final int ELT_ID_STANDARD_WVG_OBJECT = 0x18;
+    public static final int ELT_ID_CHARACTER_SIZE_WVG_OBJECT = 0x19;
+    public static final int ELT_ID_EXTENDED_OBJECT_DATA_REQUEST_CMD = 0x1A;
+    public static final int ELT_ID_RFC_822_EMAIL_HEADER = 0x20;
+    public static final int ELT_ID_HYPERLINK_FORMAT_ELEMENT = 0x21;
+    public static final int ELT_ID_REPLY_ADDRESS_ELEMENT = 0x22;
+    public static final int ELT_ID_ENHANCED_VOICE_MAIL_INFORMATION = 0x23;
+    public static final int ELT_ID_NATIONAL_LANGUAGE_SINGLE_SHIFT = 0x24;
+    public static final int ELT_ID_NATIONAL_LANGUAGE_LOCKING_SHIFT = 0x25;
+
+    public static final int PORT_WAP_PUSH = 2948;
+    public static final int PORT_WAP_WSP = 9200;
+
+    /** The maximum number of payload bytes per message */
+    public static final int MAX_USER_DATA_BYTES = 140;
+
+    /**
+     * Port addresses used in creating and parsing SmsHeader.
+     */
+    public static class PortAddrs {
+        public PortAddrs() {
+        }
+
+        public int destPort;
+        public int origPort;
+        public boolean areEightBits;
+    }
+
+    /**
+     * Concatenated reference used in creating and parsing SmsHeader.
+     */
+    public static class ConcatRef {
+        public ConcatRef() {
+        }
+
+        public int refNumber;
+        public int seqNumber;
+        public int msgCount;
+        public boolean isEightBits;
+    }
+
+    /**
+     * Special SMS message indicator, used in creating and parsing SmsHeader.
+     */
+    public static class SpecialSmsMsg {
+        public int msgIndType;
+        public int msgCount;
+    }
+
+    /**
+     * A header element that is not explicitly parsed, meaning not
+     * PortAddrs or ConcatRef or SpecialSmsMsg.
+     */
+    public static class MiscElt {
+        public int id;
+        public byte[] data;
+    }
+
+    public PortAddrs portAddrs;
+    public ConcatRef concatRef;
+    public ArrayList<SpecialSmsMsg> specialSmsMsgList = new ArrayList<SpecialSmsMsg>();
+    public ArrayList<MiscElt> miscEltList = new ArrayList<MiscElt>();
+
+    /** 7 bit national language locking shift table, or 0 for GSM default 7 bit alphabet. */
+    public int languageTable;
+
+    /** 7 bit national language single shift table, or 0 for GSM default 7 bit extension table. */
+    public int languageShiftTable;
+
+    public SmsHeader() {
+    }
+
+    /**
+     * Create structured SmsHeader object from serialized byte array representation.
+     * (see TS 23.040 9.2.3.24)
+     *
+     * @param data is user data header bytes
+     * @return SmsHeader object
+     */
+    public static SmsHeader fromByteArray(byte[] data) {
+        ByteArrayInputStream inStream = new ByteArrayInputStream(data);
+        SmsHeader smsHeader = new SmsHeader();
+        while (inStream.available() > 0) {
+            /**
+             * NOTE: as defined in the spec, ConcatRef and PortAddr
+             * fields should not reoccur, but if they do the last
+             * occurrence is to be used.  Also, for ConcatRef
+             * elements, if the count is zero, sequence is zero, or
+             * sequence is larger than count, the entire element is to
+             * be ignored.
+             */
+            int id = inStream.read();
+            int length = inStream.read();
+            ConcatRef concatRef;
+            PortAddrs portAddrs;
+            switch (id) {
+                case ELT_ID_CONCATENATED_8_BIT_REFERENCE:
+                    concatRef = new ConcatRef();
+                    concatRef.refNumber = inStream.read();
+                    concatRef.msgCount = inStream.read();
+                    concatRef.seqNumber = inStream.read();
+                    concatRef.isEightBits = true;
+                    if (concatRef.msgCount != 0 && concatRef.seqNumber != 0
+                            && concatRef.seqNumber <= concatRef.msgCount) {
+                        smsHeader.concatRef = concatRef;
+                    }
+                    break;
+                case ELT_ID_CONCATENATED_16_BIT_REFERENCE:
+                    concatRef = new ConcatRef();
+                    concatRef.refNumber = (inStream.read() << 8) | inStream.read();
+                    concatRef.msgCount = inStream.read();
+                    concatRef.seqNumber = inStream.read();
+                    concatRef.isEightBits = false;
+                    if (concatRef.msgCount != 0 && concatRef.seqNumber != 0
+                            && concatRef.seqNumber <= concatRef.msgCount) {
+                        smsHeader.concatRef = concatRef;
+                    }
+                    break;
+                case ELT_ID_APPLICATION_PORT_ADDRESSING_8_BIT:
+                    portAddrs = new PortAddrs();
+                    portAddrs.destPort = inStream.read();
+                    portAddrs.origPort = inStream.read();
+                    portAddrs.areEightBits = true;
+                    smsHeader.portAddrs = portAddrs;
+                    break;
+                case ELT_ID_APPLICATION_PORT_ADDRESSING_16_BIT:
+                    portAddrs = new PortAddrs();
+                    portAddrs.destPort = (inStream.read() << 8) | inStream.read();
+                    portAddrs.origPort = (inStream.read() << 8) | inStream.read();
+                    portAddrs.areEightBits = false;
+                    smsHeader.portAddrs = portAddrs;
+                    break;
+                case ELT_ID_NATIONAL_LANGUAGE_SINGLE_SHIFT:
+                    smsHeader.languageShiftTable = inStream.read();
+                    break;
+                case ELT_ID_NATIONAL_LANGUAGE_LOCKING_SHIFT:
+                    smsHeader.languageTable = inStream.read();
+                    break;
+                case ELT_ID_SPECIAL_SMS_MESSAGE_INDICATION:
+                    SpecialSmsMsg specialSmsMsg = new SpecialSmsMsg();
+                    specialSmsMsg.msgIndType = inStream.read();
+                    specialSmsMsg.msgCount = inStream.read();
+                    smsHeader.specialSmsMsgList.add(specialSmsMsg);
+                    break;
+                default:
+                    MiscElt miscElt = new MiscElt();
+                    miscElt.id = id;
+                    miscElt.data = new byte[length];
+                    inStream.read(miscElt.data, 0, length);
+                    smsHeader.miscEltList.add(miscElt);
+            }
+        }
+        return smsHeader;
+    }
+
+    /**
+     * Create serialized byte array representation from structured SmsHeader object.
+     * (see TS 23.040 9.2.3.24)
+     *
+     * @return Byte array representing the SmsHeader
+     */
+    public static byte[] toByteArray(SmsHeader smsHeader) {
+        if ((smsHeader.portAddrs == null) && (smsHeader.concatRef == null)
+                && (smsHeader.specialSmsMsgList.isEmpty()) && (smsHeader.miscEltList.isEmpty()) && (
+                smsHeader.languageShiftTable == 0) && (smsHeader.languageTable == 0)) {
+            return null;
+        }
+
+        ByteArrayOutputStream outStream =
+                new ByteArrayOutputStream(MAX_USER_DATA_BYTES);
+        ConcatRef concatRef = smsHeader.concatRef;
+        if (concatRef != null) {
+            if (concatRef.isEightBits) {
+                outStream.write(ELT_ID_CONCATENATED_8_BIT_REFERENCE);
+                outStream.write(3);
+                outStream.write(concatRef.refNumber);
+            } else {
+                outStream.write(ELT_ID_CONCATENATED_16_BIT_REFERENCE);
+                outStream.write(4);
+                outStream.write(concatRef.refNumber >>> 8);
+                outStream.write(concatRef.refNumber & 0x00FF);
+            }
+            outStream.write(concatRef.msgCount);
+            outStream.write(concatRef.seqNumber);
+        }
+        PortAddrs portAddrs = smsHeader.portAddrs;
+        if (portAddrs != null) {
+            if (portAddrs.areEightBits) {
+                outStream.write(ELT_ID_APPLICATION_PORT_ADDRESSING_8_BIT);
+                outStream.write(2);
+                outStream.write(portAddrs.destPort);
+                outStream.write(portAddrs.origPort);
+            } else {
+                outStream.write(ELT_ID_APPLICATION_PORT_ADDRESSING_16_BIT);
+                outStream.write(4);
+                outStream.write(portAddrs.destPort >>> 8);
+                outStream.write(portAddrs.destPort & 0x00FF);
+                outStream.write(portAddrs.origPort >>> 8);
+                outStream.write(portAddrs.origPort & 0x00FF);
+            }
+        }
+        if (smsHeader.languageShiftTable != 0) {
+            outStream.write(ELT_ID_NATIONAL_LANGUAGE_SINGLE_SHIFT);
+            outStream.write(1);
+            outStream.write(smsHeader.languageShiftTable);
+        }
+        if (smsHeader.languageTable != 0) {
+            outStream.write(ELT_ID_NATIONAL_LANGUAGE_LOCKING_SHIFT);
+            outStream.write(1);
+            outStream.write(smsHeader.languageTable);
+        }
+        for (SpecialSmsMsg specialSmsMsg : smsHeader.specialSmsMsgList) {
+            outStream.write(ELT_ID_SPECIAL_SMS_MESSAGE_INDICATION);
+            outStream.write(2);
+            outStream.write(specialSmsMsg.msgIndType & 0xFF);
+            outStream.write(specialSmsMsg.msgCount & 0xFF);
+        }
+        for (MiscElt miscElt : smsHeader.miscEltList) {
+            outStream.write(miscElt.id);
+            outStream.write(miscElt.data.length);
+            outStream.write(miscElt.data, 0, miscElt.data.length);
+        }
+        return outStream.toByteArray();
+    }
+}
diff --git a/src/com/android/cellbroadcastservice/StateMachine.java b/src/com/android/cellbroadcastservice/StateMachine.java
index 1301ea8..74d16a21 100644
--- a/src/com/android/cellbroadcastservice/StateMachine.java
+++ b/src/com/android/cellbroadcastservice/StateMachine.java
@@ -21,6 +21,7 @@
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Message;
+import android.telephony.Rlog;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -1234,8 +1235,8 @@
         /** @see StateMachine#transitionTo(IState) */
         private final void transitionTo(IState destState) {
             if (mTransitionInProgress) {
-                Log.wtf(mSm.mName, "transitionTo called while transition already in progress to " +
-                        mDestState + ", new target state=" + destState);
+                Log.wtf(mSm.mName, "transitionTo called while transition already in progress to "
+                        + mDestState + ", new target state=" + destState);
             }
             mDestState = (State) destState;
             if (mDbg) mSm.log("transitionTo: destState=" + mDestState.getName());
@@ -2123,7 +2124,7 @@
      * @param s is string log
      */
     protected void log(String s) {
-        Log.d(mName, s);
+        Rlog.d(mName, s);
     }
 
     /**
@@ -2132,7 +2133,7 @@
      * @param s is string log
      */
     protected void logd(String s) {
-        Log.d(mName, s);
+        Rlog.d(mName, s);
     }
 
     /**
@@ -2141,7 +2142,7 @@
      * @param s is string log
      */
     protected void logv(String s) {
-        Log.v(mName, s);
+        Rlog.v(mName, s);
     }
 
     /**
@@ -2150,7 +2151,7 @@
      * @param s is string log
      */
     protected void logi(String s) {
-        Log.i(mName, s);
+        Rlog.i(mName, s);
     }
 
     /**
@@ -2159,7 +2160,7 @@
      * @param s is string log
      */
     protected void logw(String s) {
-        Log.w(mName, s);
+        Rlog.w(mName, s);
     }
 
     /**
@@ -2168,7 +2169,7 @@
      * @param s is string log
      */
     protected void loge(String s) {
-        Log.e(mName, s);
+        Rlog.e(mName, s);
     }
 
     /**
@@ -2178,6 +2179,6 @@
      * @param e is a Throwable which logs additional information.
      */
     protected void loge(String s, Throwable e) {
-        Log.e(mName, s, e);
+        Rlog.e(mName, s, e);
     }
 }
diff --git a/src/com/android/cellbroadcastservice/UserData.java b/src/com/android/cellbroadcastservice/UserData.java
index 8758d8d..7cbf320 100644
--- a/src/com/android/cellbroadcastservice/UserData.java
+++ b/src/com/android/cellbroadcastservice/UserData.java
@@ -18,7 +18,6 @@
 
 import android.util.SparseIntArray;
 
-import com.android.internal.telephony.SmsHeader;
 import com.android.internal.util.HexDump;
 
 import dalvik.annotation.compat.UnsupportedAppUsage;
diff --git a/src/com/android/cellbroadcastservice/WakeLockStateMachine.java b/src/com/android/cellbroadcastservice/WakeLockStateMachine.java
index 789534d..1d08f54 100644
--- a/src/com/android/cellbroadcastservice/WakeLockStateMachine.java
+++ b/src/com/android/cellbroadcastservice/WakeLockStateMachine.java
@@ -16,14 +16,14 @@
 
 package com.android.cellbroadcastservice;
 
-import android.annotation.UnsupportedAppUsage;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.os.Looper;
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.SystemProperties;
-import android.util.Log;
+import android.telephony.Rlog;
 
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
@@ -54,7 +54,6 @@
     /** Broadcast not required due to geo-fencing check */
     static final int EVENT_BROADCAST_NOT_REQUIRED = 4;
 
-    @UnsupportedAppUsage
     protected Context mContext;
 
     protected AtomicInteger mReceiverCount = new AtomicInteger(0);
@@ -63,12 +62,11 @@
     private static final int WAKE_LOCK_TIMEOUT = 3000;
 
     private final DefaultState mDefaultState = new DefaultState();
-    @UnsupportedAppUsage
     private final IdleState mIdleState = new IdleState();
     private final WaitingState mWaitingState = new WaitingState();
 
-    protected WakeLockStateMachine(String debugTag, Context context) {
-        super(debugTag);
+    protected WakeLockStateMachine(String debugTag, Context context, Looper looper) {
+        super(debugTag, looper);
 
         mContext = context;
 
@@ -235,10 +233,9 @@
      * Log with debug level.
      * @param s the string to log
      */
-    @UnsupportedAppUsage
     @Override
     protected void log(String s) {
-        Log.d(getName(), s);
+        Rlog.d(getName(), s);
     }
 
     /**
@@ -247,7 +244,7 @@
      */
     @Override
     protected void loge(String s) {
-        Log.e(getName(), s);
+        Rlog.e(getName(), s);
     }
 
     /**
@@ -257,6 +254,6 @@
      */
     @Override
     protected void loge(String s, Throwable e) {
-        Log.e(getName(), s, e);
+        Rlog.e(getName(), s, e);
     }
 }
diff --git a/tests/Android.bp b/tests/Android.bp
index f59dde6..ef9568e 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -15,7 +15,7 @@
     ],
     srcs: ["src/**/*.java", ":cellbroadcast-shared-srcs"],
     platform_apis: true,
-    test_suites: ["device-tests"],
+    test_suites: ["device-tests", "mts"],
     certificate: "platform",
     instrumentation_for: "CellBroadcastServiceModule",
 }
diff --git a/tests/src/com/android/cellbroadcastservice/CdmaSmsMessageTest.java b/tests/src/com/android/cellbroadcastservice/CdmaSmsMessageTest.java
new file mode 100644
index 0000000..8a5fff2
--- /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.Rlog;
+import android.telephony.SmsCbCmasInfo;
+import android.telephony.SmsCbMessage;
+import android.telephony.cdma.CdmaSmsCbProgramData;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+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) {
+                    Rlog.d(TAG, "success: " + cbMessage);
+                }
+            } catch (Exception e) {
+                Rlog.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) {
+                Rlog.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/CellBroadcastHandlerTest.java b/tests/src/com/android/cellbroadcastservice/CellBroadcastHandlerTest.java
index 0eab2c9..22c76b5 100644
--- a/tests/src/com/android/cellbroadcastservice/CellBroadcastHandlerTest.java
+++ b/tests/src/com/android/cellbroadcastservice/CellBroadcastHandlerTest.java
@@ -49,6 +49,8 @@
 
     private CellBroadcastHandler mCellBroadcastHandler;
 
+    private TestableLooper mTestbleLooper;
+
     @Mock
     private Map<Integer, Resources> mMockedResourcesCache;
 
@@ -58,7 +60,7 @@
                             String sortOrder) {
 
             if (uri.compareTo(Telephony.CellBroadcasts.CONTENT_URI) == 0) {
-                MatrixCursor mc = new MatrixCursor(Telephony.CellBroadcasts.QUERY_COLUMNS_FWK);
+                MatrixCursor mc = new MatrixCursor(CellBroadcastProvider.QUERY_COLUMNS);
 
                 mc.addRow(new Object[]{
                         1,              // _ID
@@ -101,7 +103,11 @@
     @Before
     public void setUp() throws Exception {
         super.setUp();
-        mCellBroadcastHandler = new CellBroadcastHandler("CellBroadcastHandlerUT", mMockedContext);
+
+        mTestbleLooper = TestableLooper.get(CellBroadcastHandlerTest.this);
+
+        mCellBroadcastHandler = new CellBroadcastHandler("CellBroadcastHandlerUT",
+                mMockedContext, mTestbleLooper.getLooper());
         ((MockContentResolver) mMockedContext.getContentResolver()).addProvider(
                 Telephony.CellBroadcasts.CONTENT_URI.getAuthority(),
                 new CellBroadcastContentProvider());
diff --git a/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTest.java b/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTest.java
index dc9837f..529ea77 100644
--- a/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTest.java
+++ b/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTest.java
@@ -16,6 +16,8 @@
 
 package com.android.cellbroadcastservice;
 
+import static com.android.cellbroadcastservice.CellBroadcastProvider.QUERY_COLUMNS;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.doReturn;
@@ -25,9 +27,9 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.provider.Telephony.CellBroadcasts;
+import android.telephony.Rlog;
 import android.test.mock.MockContentResolver;
 import android.test.mock.MockContext;
-import android.util.Log;
 
 import com.android.cellbroadcastservice.CellBroadcastProvider.PermissionChecker;
 
@@ -65,30 +67,6 @@
 
     private static final String SELECT_BY_ID = CellBroadcasts._ID + "=?";
 
-    private static final String[] QUERY_COLUMNS = {
-            CellBroadcasts._ID,
-            CellBroadcasts.GEOGRAPHICAL_SCOPE,
-            CellBroadcasts.PLMN,
-            CellBroadcasts.LAC,
-            CellBroadcasts.CID,
-            CellBroadcasts.SERIAL_NUMBER,
-            CellBroadcasts.SERVICE_CATEGORY,
-            CellBroadcasts.LANGUAGE_CODE,
-            CellBroadcasts.MESSAGE_BODY,
-            CellBroadcasts.MESSAGE_FORMAT,
-            CellBroadcasts.MESSAGE_PRIORITY,
-            CellBroadcasts.ETWS_WARNING_TYPE,
-            CellBroadcasts.CMAS_MESSAGE_CLASS,
-            CellBroadcasts.CMAS_CATEGORY,
-            CellBroadcasts.CMAS_RESPONSE_TYPE,
-            CellBroadcasts.CMAS_SEVERITY,
-            CellBroadcasts.CMAS_URGENCY,
-            CellBroadcasts.CMAS_CERTAINTY,
-            CellBroadcasts.RECEIVED_TIME,
-            CellBroadcasts.MESSAGE_BROADCASTED,
-            CellBroadcasts.GEOMETRIES
-    };
-
     private CellBroadcastProviderTestable mCellBroadcastProviderTestable;
     private MockContextWithProvider mContext;
     private MockContentResolver mContentResolver;
@@ -346,7 +324,7 @@
 
         @Override
         public Object getSystemService(String name) {
-            Log.d(TAG, "getSystemService: returning null");
+            Rlog.d(TAG, "getSystemService: returning null");
             return null;
         }
 
diff --git a/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTestable.java b/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTestable.java
index 2be7512..c3e94e7 100644
--- a/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTestable.java
+++ b/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTestable.java
@@ -20,7 +20,7 @@
 import android.content.pm.ProviderInfo;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
-import android.util.Log;
+import android.telephony.Rlog;
 
 import androidx.test.InstrumentationRegistry;
 
@@ -34,7 +34,7 @@
     @Override
     public boolean onCreate() {
         // DO NOT call super.onCreate(), otherwise the permission checker will be override.
-        Log.d(TAG, "CellBroadcastProviderTestable onCreate");
+        Rlog.d(TAG, "CellBroadcastProviderTestable onCreate");
         mDbHelper = new InMemoryCellBroadcastProviderDbHelper();
         return true;
     }
@@ -53,7 +53,7 @@
 
         @Override
         public void onCreate(SQLiteDatabase db) {
-            Log.d(TAG, "IN MEMORY DB CREATED");
+            Rlog.d(TAG, "IN MEMORY DB CREATED");
             db.execSQL(getStringForCellBroadcastTableCreation(CELL_BROADCASTS_TABLE_NAME));
         }
 
diff --git a/tests/src/com/android/cellbroadcastservice/CellBroadcastServiceTestBase.java b/tests/src/com/android/cellbroadcastservice/CellBroadcastServiceTestBase.java
index 9246f35..a6e8676 100644
--- a/tests/src/com/android/cellbroadcastservice/CellBroadcastServiceTestBase.java
+++ b/tests/src/com/android/cellbroadcastservice/CellBroadcastServiceTestBase.java
@@ -16,20 +16,31 @@
 
 package com.android.cellbroadcastservice;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
 
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.res.Resources;
+import android.location.LocationManager;
 import android.os.Handler;
 import android.os.IPowerManager;
 import android.os.PowerManager;
 import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
 import android.test.mock.MockContentResolver;
 import android.testing.TestableLooper;
 
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+
 import junit.framework.TestCase;
 
 import org.mockito.Mock;
@@ -46,15 +57,24 @@
 public class CellBroadcastServiceTestBase extends TestCase {
 
     @Mock
-    Context mMockedContext;
+    protected Context mMockedContext;
 
     @Mock
-    Resources mMockedResources;
+    protected Resources mMockedResources;
 
     @Mock
-    SubscriptionManager mMockedSubscriptionManager;
+    protected SubscriptionManager mMockedSubscriptionManager;
 
-    final MockContentResolver mMockedContentResolver = new MockContentResolver();
+    @Mock
+    protected TelephonyManager mMockedTelephonyManager;
+
+    @Mock
+    protected LocationManager mMockedLocationManager;
+
+    private final MockContentResolver mMockedContentResolver = new MockContentResolver();
+
+    private final Multimap<String, BroadcastReceiver> mBroadcastReceiversByAction =
+            ArrayListMultimap.create();
 
     private final HashMap<InstanceKey, Object> mOldInstances = new HashMap<>();
 
@@ -97,15 +117,40 @@
         PowerManager powerManager = new PowerManager(mMockedContext, mock(IPowerManager.class),
                 new Handler(TestableLooper.get(CellBroadcastServiceTestBase.this).getLooper()));
         doReturn(powerManager).when(mMockedContext).getSystemService(Context.POWER_SERVICE);
+        doReturn(mMockedTelephonyManager).when(mMockedContext)
+                .getSystemService(Context.TELEPHONY_SERVICE);
         doReturn(mMockedSubscriptionManager).when(mMockedContext)
                 .getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+        doReturn(mMockedLocationManager).when(mMockedContext)
+                .getSystemService(Context.LOCATION_SERVICE);
+        doReturn(mMockedContext).when(mMockedContext).createContextAsUser(any(), anyInt());
         doReturn(new int[]{1}).when(mMockedSubscriptionManager).getSubscriptionIds(anyInt());
+        doReturn(mMockedTelephonyManager).when(mMockedTelephonyManager)
+                .createForSubscriptionId(anyInt());
+        doAnswer(invocation -> {
+            BroadcastReceiver receiver = invocation.getArgument(0);
+            IntentFilter intentFilter = invocation.getArgument(1);
+            for (int i = 0; i < intentFilter.countActions(); i++) {
+                mBroadcastReceiversByAction.put(intentFilter.getAction(i), receiver);
+            }
+            return null;
+        }).when(mMockedContext).registerReceiver(
+                any(BroadcastReceiver.class), any(IntentFilter.class));
+        doReturn(true).when(mMockedLocationManager).isProviderEnabled(anyString());
     }
 
     protected void tearDown() throws Exception {
         restoreInstances();
     }
 
+    void sendBroadcast(Intent intent) {
+        if (mBroadcastReceiversByAction.containsKey(intent.getAction())) {
+            for (BroadcastReceiver receiver : mBroadcastReceiversByAction.get(intent.getAction())) {
+                receiver.onReceive(mMockedContext, intent);
+            }
+        }
+    }
+
     void putResources(int id, Object value) {
         if (value instanceof String[]) {
             doReturn(value).when(mMockedResources).getStringArray(eq(id));
@@ -147,4 +192,50 @@
         mInstanceKeys.clear();
         mOldInstances.clear();
     }
+
+    /**
+     * Converts a hex String to a byte array.
+     *
+     * @param s A string of hexadecimal characters, must be an even number of
+     *          chars long
+     *
+     * @return byte array representation
+     *
+     * @throws RuntimeException on invalid format
+     */
+    public static byte[] hexStringToBytes(String s) {
+        byte[] ret;
+
+        if (s == null) return null;
+
+        int sz = s.length();
+
+        ret = new byte[sz / 2];
+
+        for (int i = 0; i < sz; i += 2) {
+            ret[i / 2] = (byte) ((hexCharToInt(s.charAt(i)) << 4) | hexCharToInt(s.charAt(i + 1)));
+        }
+
+        return ret;
+    }
+
+    /**
+     * Converts a hex char to its integer value
+     *
+     * @param c A single hexadecimal character. Must be in one of these ranges:
+     *          - '0' to '9', or
+     *          - 'a' to 'f', or
+     *          - 'A' to 'F'
+     *
+     * @return the integer representation of {@code c}
+     *
+     * @throws RuntimeException on invalid character
+     */
+    private static int hexCharToInt(char c) {
+        if (c >= '0' && c <= '9') return (c - '0');
+        if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
+        if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
+
+        throw new RuntimeException("invalid hex char '" + c + "'");
+    }
 }
diff --git a/tests/src/com/android/cellbroadcastservice/GsmCellBroadcastHandlerTest.java b/tests/src/com/android/cellbroadcastservice/GsmCellBroadcastHandlerTest.java
new file mode 100644
index 0000000..3f01602
--- /dev/null
+++ b/tests/src/com/android/cellbroadcastservice/GsmCellBroadcastHandlerTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cellbroadcastservice;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.location.Location;
+import android.location.LocationRequest;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.provider.Telephony;
+import android.telephony.SmsCbCmasInfo;
+import android.telephony.SmsCbLocation;
+import android.telephony.SmsCbMessage;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.text.format.DateUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.Map;
+import java.util.function.Consumer;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class GsmCellBroadcastHandlerTest extends CellBroadcastServiceTestBase {
+
+    private GsmCellBroadcastHandler mGsmCellBroadcastHandler;
+
+    private TestableLooper mTestableLooper;
+
+    @Mock
+    private Map<Integer, Resources> mMockedResourcesCache;
+
+    private class CellBroadcastContentProvider extends MockContentProvider {
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                            String sortOrder) {
+
+            // Assume the message was received 2 hours ago.
+            long receivedTime = System.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS * 2;
+
+            if (uri.compareTo(Telephony.CellBroadcasts.CONTENT_URI) == 0
+                    && Long.parseLong(selectionArgs[selectionArgs.length - 1]) <= receivedTime) {
+                MatrixCursor mc = new MatrixCursor(CellBroadcastProvider.QUERY_COLUMNS);
+
+                mc.addRow(new Object[]{
+                        1,              // _ID
+                        0,              // SLOT_INDEX
+                        0,              // GEOGRAPHICAL_SCOPE
+                        "311480",       // PLMN
+                        0,              // LAC
+                        0,              // CID
+                        1234,           // SERIAL_NUMBER
+                        SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL,
+                        "en",           // LANGUAGE_CODE
+                        "Test Message", // MESSAGE_BODY
+                        1,              // MESSAGE_FORMAT
+                        3,              // MESSAGE_PRIORITY
+                        0,              // ETWS_WARNING_TYPE
+                        SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT, // CMAS_MESSAGE_CLASS
+                        0,              // CMAS_CATEGORY
+                        0,              // CMAS_RESPONSE_TYPE
+                        0,              // CMAS_SEVERITY
+                        0,              // CMAS_URGENCY
+                        0,              // CMAS_CERTAINTY
+                        receivedTime,
+                        false,          // MESSAGE_BROADCASTED
+                        "",             // GEOMETRIES
+                        5,              // MAXIMUM_WAIT_TIME
+                });
+
+                return mc;
+            }
+
+            return null;
+        }
+
+        @Override
+        public int update(Uri url, ContentValues values, String where, String[] whereArgs) {
+            return 1;
+        }
+
+        @Override
+        public Uri insert(Uri uri, ContentValues values) {
+            return null;
+        }
+
+    }
+
+    private class SettingsProvider extends MockContentProvider {
+        @Override
+        public Bundle call(String method, String arg, Bundle extras) {
+            return null;
+        }
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                            String sortOrder) {
+            return null;
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mTestableLooper = TestableLooper.get(GsmCellBroadcastHandlerTest.this);
+
+        mGsmCellBroadcastHandler = new GsmCellBroadcastHandler(mMockedContext,
+                mTestableLooper.getLooper());
+        mGsmCellBroadcastHandler.start();
+
+        ((MockContentResolver) mMockedContext.getContentResolver()).addProvider(
+                Telephony.CellBroadcasts.CONTENT_URI.getAuthority(),
+                new CellBroadcastContentProvider());
+        ((MockContentResolver) mMockedContext.getContentResolver()).addProvider(
+                Settings.AUTHORITY, new SettingsProvider());
+        doReturn(true).when(mMockedResourcesCache).containsKey(anyInt());
+        doReturn(mMockedResources).when(mMockedResourcesCache).get(anyInt());
+        replaceInstance(CellBroadcastHandler.class, "mResourcesCache",
+                mGsmCellBroadcastHandler, mMockedResourcesCache);
+        putResources(R.integer.message_expiration_time, 86400000);
+        putResources(com.android.internal.R.array.config_defaultCellBroadcastReceiverPkgs,
+                new String[]{"fake.cellbroadcast.pkg"});
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    private SmsCbMessage createSmsCbMessage(int serialNumber, int serviceCategory,
+                                            String messageBody) {
+        return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
+                0, serialNumber, new SmsCbLocation(),
+                serviceCategory, "en", messageBody, 3,
+                null, null, 0);
+    }
+
+    @Test
+    @SmallTest
+    public void testTriggerMessage() throws Exception {
+        doReturn(false).when(mMockedLocationManager).isProviderEnabled(anyString());
+        final byte[] pdu = hexStringToBytes("0001113001010010C0111204D2");
+        mGsmCellBroadcastHandler.onGsmCellBroadcastSms(0, pdu);
+        mTestableLooper.processAllMessages();
+
+        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+        verify(mMockedContext).sendOrderedBroadcast(captor.capture(), anyString(), anyString(),
+                any(), any(), anyInt(), any(), any());
+        Intent intent = captor.getValue();
+        assertEquals(Telephony.Sms.Intents.ACTION_SMS_EMERGENCY_CB_RECEIVED, intent.getAction());
+        SmsCbMessage msg = intent.getParcelableExtra("message");
+
+        assertEquals(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL,
+                msg.getServiceCategory());
+        assertEquals(1234, msg.getSerialNumber());
+        assertEquals("Test Message", msg.getMessageBody());
+    }
+
+    @Test
+    @SmallTest
+    public void testAirplaneModeReset() {
+        putResources(R.bool.reset_on_power_cycle_or_airplane_mode, true);
+        Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        intent.putExtra("state", true);
+        // Send fake airplane mode on event.
+        sendBroadcast(intent);
+
+        final byte[] pdu = hexStringToBytes("0001113001010010C0111204D2");
+        mGsmCellBroadcastHandler.onGsmCellBroadcastSms(0, pdu);
+        mTestableLooper.processAllMessages();
+
+        verify(mMockedContext, never()).sendOrderedBroadcast(any(), anyString(), anyString(),
+                any(), any(), anyInt(), any(), any());
+    }
+
+    @Test
+    @SmallTest
+    public void testGeofencingAlertOutOfPolygon() {
+        final byte[] pdu = hexStringToBytes("01111D7090010254747A0E4ACF416110B538A582DE6650906AA28"
+                + "2AE6979995D9ECF41C576597E2EBBC77950905D96D3D3EE33689A9FD3CB6D1708CA2E87E76550FAE"
+                + "C7ECBCB203ABA0C6A97E7F3F0B9EC02C15CB5769A5D0652A030FB1ECECF5D5076393C2F83C8E9B9B"
+                + "C7C0ECBC9203A3A3D07B5CBF379F85C06E16030580D660BB662B51A0D57CC3500000000000000000"
+                + "0000000000000000000000000000000000000000000000000003021002078B53B6CA4B84B53988A4"
+                + "B86B53958A4C2DB53B54A4C28B53B6CA4B840100CFF");
+        mGsmCellBroadcastHandler.onGsmCellBroadcastSms(0, pdu);
+        mTestableLooper.processAllMessages();
+
+        ArgumentCaptor<Consumer<Location>> captor = ArgumentCaptor.forClass(Consumer.class);
+        verify(mMockedLocationManager, times(2)).getCurrentLocation(
+                any(LocationRequest.class), any(), any(), captor.capture());
+
+        Consumer<Location> consumer = captor.getValue();
+        consumer.accept(Mockito.mock(Location.class));
+
+        verify(mMockedContext, never()).sendOrderedBroadcast(any(), anyString(), anyString(),
+                any(), any(), anyInt(), any(), any());
+    }
+}
diff --git a/tests/src/com/android/cellbroadcastservice/GsmSmsCbMessageTest.java b/tests/src/com/android/cellbroadcastservice/GsmSmsCbMessageTest.java
new file mode 100644
index 0000000..08d862d
--- /dev/null
+++ b/tests/src/com/android/cellbroadcastservice/GsmSmsCbMessageTest.java
@@ -0,0 +1,1061 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cellbroadcastservice;
+
+import android.telephony.CbGeoUtils;
+import android.telephony.Rlog;
+import android.telephony.SmsCbCmasInfo;
+import android.telephony.SmsCbEtwsInfo;
+import android.telephony.SmsCbLocation;
+import android.telephony.SmsCbMessage;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.cellbroadcastservice.CbGeoUtils.Circle;
+import com.android.cellbroadcastservice.CbGeoUtils.Polygon;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Random;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class GsmSmsCbMessageTest extends CellBroadcastServiceTestBase {
+
+    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();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    public void testGetEtwsPrimaryMessage() {
+        String testMessage1 = "Testmessage1";
+        String testMessage2 = "Testmessage2";
+        String testMessage3 = "Testmessage3";
+        String testMessage4 = "Testmessage4";
+        String testMessage5 = "Testmessage5";
+
+        putResources(R.string.etws_primary_default_message_earthquake, testMessage1);
+        putResources(R.string.etws_primary_default_message_tsunami, testMessage2);
+        putResources(R.string.etws_primary_default_message_earthquake_and_tsunami, testMessage3);
+        putResources(R.string.etws_primary_default_message_test, testMessage4);
+        putResources(R.string.etws_primary_default_message_others, testMessage5);
+
+        String message = GsmSmsCbMessage.getEtwsPrimaryMessage(mMockedContext,
+                SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE);
+        Rlog.d("GsmSmsCbMessageTest", "earthquake message=" + message);
+        assertEquals(testMessage1, message);
+
+        message = GsmSmsCbMessage.getEtwsPrimaryMessage(mMockedContext,
+                SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI);
+        Rlog.d("GsmSmsCbMessageTest", "tsunami message=" + message);
+        assertEquals(testMessage2, message);
+
+        message = GsmSmsCbMessage.getEtwsPrimaryMessage(mMockedContext,
+                SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI);
+        Rlog.d("GsmSmsCbMessageTest", "earthquake and tsunami message=" + message);
+        assertEquals(testMessage3, message);
+
+        message = GsmSmsCbMessage.getEtwsPrimaryMessage(mMockedContext,
+                SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE);
+        Rlog.d("GsmSmsCbMessageTest", "test message=" + message);
+        assertEquals(testMessage4, message);
+
+        message = GsmSmsCbMessage.getEtwsPrimaryMessage(mMockedContext,
+                SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY);
+        Rlog.d("GsmSmsCbMessageTest", "others message=" + message);
+        assertEquals(testMessage5, message);
+    }
+
+    @Test
+    public void testCreateMessageFromBinary() throws Exception {
+        final byte[] pdu = hexStringToBytes("0111130F6A0101C8329BFD06559BD429E8FE96B3C92C101D9D9"
+                + "E83D27350B22E1C7EAFF234BDFCADB962AE9A6BCE06A1DCE57B0AD40241C3E73208147B81622E000"
+                + "0000000000000000000000000000000000000000000000039EA013028B53640A4BF600063204C8FC"
+                + "D063F341AF67167E683CF01215F1E40100C053028B53640A4BF600063204C8FCD063F341AF67167E"
+                + "683CF01215F1E40100C053028B53640A4BF600063204C8FCD063F341AF67167E683CF01215F1E401"
+                + "00C053028B53640A4BF600063204C8FCD063F341AF67167E683CF01215F1E40100C053028B53640A"
+                + "4BF600063204C8FCD063F341AF67167E683CF01215F1E40100C053028B53640A4BF600063204C8FC"
+                + "D063F341AF67167E683CF01215F1E40100C053028B53640A4BF600063204C8FCD063F341AF67167E"
+                + "683CF01215F1E40100C053028B53640A4BF600063204C8FCD063F341AF67167E683CF01215F1E401"
+                + "00C053028B53640A4BF600063204C8FCD063F341AF67167E683CF01215F1E40100C053028B53640A"
+                + "4BF600063204C8FCD063F341AF67167E683CF01215F1E40100C053028B53640A4BF600063204C8FC"
+                + "D063F341AF67167E683CF01215F1E40100C053028B53640A4BF600063204C8FCD063F341AF67167E"
+                + "683CF01215F1E40100C053028B53640A4BF600063204C8FCD063F341AF67167E683CF01215F1E401"
+                + "00C053028B53640A4BF600063204C8FCD063F341AF67167E683CF01215F1E40100C053028B53640A"
+                + "4BF600063204C8FCD063F341AF67167E683CF01215F1E40100C053028B53640A4BF600063");
+        SmsCbHeader header = new SmsCbHeader(pdu);
+
+        byte[][] pdus = new byte[1][];
+        pdus[0] = pdu;
+
+        SmsCbMessage msg = GsmSmsCbMessage.createSmsCbMessage(mMockedContext, header, null, pdus,
+                0);
+
+        Rlog.d(TAG, "msg=" + msg);
+
+        assertEquals(SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE,
+                msg.getGeographicalScope());
+        assertEquals(3946, msg.getSerialNumber());
+        assertEquals(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED,
+                msg.getServiceCategory());
+        assertEquals("en", msg.getLanguageCode());
+        assertEquals("Hello UMTS world, this is IuBC§Write§5.1.5.sl (new) - Page  1/ 1.",
+                msg.getMessageBody());
+        assertEquals(SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, msg.getMessagePriority());
+
+        SmsCbCmasInfo cmasInfo = msg.getCmasWarningInfo();
+        assertEquals(SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT, cmasInfo.getMessageClass());
+        assertEquals(SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN, cmasInfo.getCategory());
+        assertEquals(SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, cmasInfo.getResponseType());
+        assertEquals(SmsCbCmasInfo.CMAS_URGENCY_IMMEDIATE, cmasInfo.getUrgency());
+        assertEquals(SmsCbCmasInfo.CMAS_CERTAINTY_OBSERVED, cmasInfo.getCertainty());
+
+        List<CbGeoUtils.Geometry> geometries = msg.getGeometries();
+        for (int i = 0; i < 15; i++) {
+            assertEquals(1546.875, ((Circle) geometries.get(i * 2)).getRadius());
+            assertEquals(37.41462707519531, ((Circle) geometries.get(i * 2)).getCenter().lat);
+            assertEquals(-122.08093643188477, ((Circle) geometries.get(i * 2)).getCenter().lng);
+            assertEquals(11.109967231750488,
+                    ((Polygon) geometries.get(i * 2 + 1)).getVertices().get(0).lat);
+            assertEquals(22.219934463500977,
+                    ((Polygon) geometries.get(i * 2 + 1)).getVertices().get(0).lng);
+            assertEquals(33.32998752593994, 44,
+                    ((Polygon) geometries.get(i * 2 + 1)).getVertices().get(1).lat);
+            assertEquals(44.43995475769043,
+                    ((Polygon) geometries.get(i * 2 + 1)).getVertices().get(1).lng);
+            assertEquals(55.549964904785156,
+                    ((Polygon) geometries.get(i * 2 + 1)).getVertices().get(2).lat);
+            assertEquals(-56.560020446777344,
+                    ((Polygon) geometries.get(i * 2 + 1)).getVertices().get(2).lng);
+        }
+    }
+
+    @Test
+    public void testCreateTriggerMessage() throws Exception {
+        final byte[] pdu = hexStringToBytes("0001113001010010C0111204D2");
+        GsmSmsCbMessage.GeoFencingTriggerMessage triggerMessage =
+                GsmSmsCbMessage.createGeoFencingTriggerMessage(pdu);
+
+        Rlog.d(TAG, "trigger message=" + triggerMessage);
+
+        assertEquals(1, triggerMessage.type);
+        assertEquals(1, triggerMessage.cbIdentifiers.size());
+        assertEquals(1234, triggerMessage.cbIdentifiers.get(0).serialNumber);
+        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);
+        Rlog.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);
+        Rlog.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);
+        Rlog.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) {
+                Rlog.d(TAG, "exception thrown", e);
+                fail("Exception in decoder at run " + run + " length " + len + ": " + e);
+            }
+        }
+    }
+}