[automerger skipped] Clean up hidden constants am: be4fe78e0a am: 3492f71078
am: 4bd170390a -s ours
am skip reason: change_id I6e0b527d75987825ee70b6f7ac3b42331bc22571 with SHA1 5e17455390 is in history

Change-Id: Ic8a8fc926ca00ab5c49eabb4708a69f91a4168ab
diff --git a/Android.bp b/Android.bp
index c72b5c8..68fd40b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,7 +1,7 @@
 // Copyright 2019 The Android Open Source Project
-
-android_app {
-    name: "CellBroadcastServiceModule",
+java_defaults {
+    name: "CellBroadcastServiceCommon",
+    min_sdk_version: "29",
     srcs: [
       "src/**/*.java",
       ":framework-annotations",
@@ -15,6 +15,25 @@
     optimize: {
         proguard_flags_files: ["proguard.flags"],
     },
+    jarjar_rules: "cellbroadcast-jarjar-rules.txt",
+    plugins: ["java_api_finder"],
+}
+
+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/config.xml b/res/values/config.xml
index 4d640eb..23c8177 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -24,6 +24,10 @@
         <item>com.android.cellbroadcastreceiver</item>
     </string-array>
 
+    <!-- Package names of the test cell broadcast receivers, only used on debug builds -->
+    <string-array name="config_testCellBroadcastReceiverPkgs" translatable="false">
+    </string-array>
+
     <!-- The message expiration time in milliseconds for duplicate detection -->
     <integer name="message_expiration_time">86400000</integer>
 
diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml
new file mode 100644
index 0000000..85ba114
--- /dev/null
+++ b/res/values/overlayable.xml
@@ -0,0 +1,28 @@
+<?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.
+-->
+
+<!-- These values can be used to control CellBroadcastService behavior on individual devices.
+     These can be overridden by OEM's by using an RRO overlay app. -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <overlayable name="CellBroadcastServiceCustomization">
+        <!-- START VENDOR CUSTOMIZATION -->
+        <policy type="product|system|vendor">
+
+          <!-- Params from config.xml that can be overlayed -->
+          <item type="array" name="config_defaultCellBroadcastReceiverPkgs" />
+          <item type="array" name="config_testCellBroadcastReceiverPkgs" />
+          <!-- Params from config.xml that can be overlayed -->
+        </policy>
+        <!-- END VENDOR CUSTOMIZATION -->
+    </overlayable>
+</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..2cfb796 100644
--- a/res/values/symbols.xml
+++ b/res/values/symbols.xml
@@ -22,5 +22,13 @@
       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" />
+	<java-symbol type="array" name="config_testCellBroadcastReceiverPkgs" />
+
+	<!-- 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 b1324d7..d73339c 100644
--- a/src/com/android/cellbroadcastservice/BearerData.java
+++ b/src/com/android/cellbroadcastservice/BearerData.java
@@ -17,11 +17,9 @@
 package com.android.cellbroadcastservice;
 
 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.util.BitwiseInputStream;
 
 /**
  * An object to decode CDMA SMS bearer data.
@@ -68,6 +66,13 @@
     public static final int LANGUAGE_HEBREW = 0x07;
 
     /**
+     * Supported message types for CDMA SMS messages
+     * (See 3GPP2 C.S0015-B, v2.0, table 4.5.1-1)
+     * Used for CdmaSmsCbTest.
+     */
+    public static final int MESSAGE_TYPE_DELIVER        = 0x01;
+
+    /**
      * 16-bit value indicating the message ID, which increments modulo 65536.
      * (Special rules apply for WAP-messages.)
      * (See 3GPP2 C.S0015-B, v2, 4.5.1)
@@ -189,9 +194,9 @@
             inStream.skip(3);
         }
         if ((!decodeSuccess) || (paramBits > 0)) {
-            Log.d(LOG_TAG, "MESSAGE_IDENTIFIER decode " +
-                    (decodeSuccess ? "succeeded" : "failed") +
-                    " (extra bits = " + paramBits + ")");
+            Rlog.d(LOG_TAG, "MESSAGE_IDENTIFIER decode "
+                    + (decodeSuccess ? "succeeded" : "failed")
+                    + " (extra bits = " + paramBits + ")");
         }
         inStream.skip(paramBits);
         return decodeSuccess;
@@ -206,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
@@ -258,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;
@@ -427,9 +432,9 @@
             bData.language = inStream.read(8);
         }
         if ((!decodeSuccess) || (paramBits > 0)) {
-            Log.d(LOG_TAG, "LANGUAGE_INDICATOR decode " +
-                    (decodeSuccess ? "succeeded" : "failed") +
-                    " (extra bits = " + paramBits + ")");
+            Rlog.d(LOG_TAG, "LANGUAGE_INDICATOR decode "
+                    + (decodeSuccess ? "succeeded" : "failed")
+                    + " (extra bits = " + paramBits + ")");
         }
         inStream.skip(paramBits);
         return decodeSuccess;
@@ -447,9 +452,9 @@
             inStream.skip(6);
         }
         if ((!decodeSuccess) || (paramBits > 0)) {
-            Log.d(LOG_TAG, "PRIORITY_INDICATOR decode " +
-                    (decodeSuccess ? "succeeded" : "failed") +
-                    " (extra bits = " + paramBits + ")");
+            Rlog.d(LOG_TAG, "PRIORITY_INDICATOR decode "
+                    + (decodeSuccess ? "succeeded" : "failed")
+                    + " (extra bits = " + paramBits + ")");
         }
         inStream.skip(paramBits);
         return decodeSuccess;
@@ -548,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;
             }
@@ -626,9 +631,9 @@
             }
             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 212962e..4b73258 100644
--- a/src/com/android/cellbroadcastservice/CdmaServiceCategoryProgramHandler.java
+++ b/src/com/android/cellbroadcastservice/CdmaServiceCategoryProgramHandler.java
@@ -139,7 +139,7 @@
 
         // TODO: move this resource and its overlays to the CellBroadcastService directory
         String[] pkgs = mContext.getResources().getStringArray(
-                com.android.internal.R.array.config_defaultCellBroadcastReceiverPkgs);
+                R.array.config_defaultCellBroadcastReceiverPkgs);
         mReceiverCount.addAndGet(pkgs.length);
         for (String pkg : pkgs) {
             intent.setPackage(pkg);
diff --git a/src/com/android/cellbroadcastservice/CellBroadcastHandler.java b/src/com/android/cellbroadcastservice/CellBroadcastHandler.java
index cc0aba4..f22bd08 100644
--- a/src/com/android/cellbroadcastservice/CellBroadcastHandler.java
+++ b/src/com/android/cellbroadcastservice/CellBroadcastHandler.java
@@ -16,7 +16,8 @@
 
 package com.android.cellbroadcastservice;
 
-import static android.provider.Settings.Secure.CMAS_ADDITIONAL_BROADCAST_PKG;
+import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
 
 import android.Manifest;
 import android.annotation.NonNull;
@@ -32,29 +33,29 @@
 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;
 import android.provider.Telephony;
 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;
 
@@ -67,6 +68,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,7 +101,7 @@
     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 */
     protected long mLastAirplaneModeTime = 0;
@@ -120,12 +122,13 @@
         this("CellBroadcastHandler", context, Looper.myLooper());
     }
 
-    protected CellBroadcastHandler(String debugTag, Context context, Looper looper) {
+    @VisibleForTesting
+    public 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 +189,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(
@@ -322,10 +325,7 @@
         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)) {
@@ -484,26 +484,31 @@
             if (IS_DEBUGGABLE) {
                 // Send additional broadcast intent to the specified package. This is only for sl4a
                 // automation tests.
-                final String additionalPackage = Settings.Secure.getString(
-                        mContext.getContentResolver(), CMAS_ADDITIONAL_BROADCAST_PKG);
-                if (additionalPackage != null) {
+                String[] testPkgs = mContext.getResources().getStringArray(
+                        R.array.config_testCellBroadcastReceiverPkgs);
+                if (testPkgs != null) {
+                    mReceiverCount.addAndGet(testPkgs.length);
                     Intent additionalIntent = new Intent(intent);
-                    additionalIntent.setPackage(additionalPackage);
-                    mContext.createContextAsUser(UserHandle.ALL, 0).sendOrderedBroadcast(
-                            additionalIntent, receiverPermission, appOp, null, getHandler(),
-                            Activity.RESULT_OK, null, null);
+                    for (String pkg : testPkgs) {
+                        additionalIntent.setPackage(pkg);
+                        mContext.createContextAsUser(UserHandle.ALL, 0).sendOrderedBroadcast(
+                                additionalIntent, receiverPermission, appOp, mReceiver,
+                                getHandler(), Activity.RESULT_OK, null, null);
+                    }
                 }
             }
 
             String[] pkgs = mContext.getResources().getStringArray(
-                    com.android.internal.R.array.config_defaultCellBroadcastReceiverPkgs);
-            mReceiverCount.addAndGet(pkgs.length);
-            for (String pkg : pkgs) {
-                // Explicitly send the intent to all the configured cell broadcast receivers.
-                intent.setPackage(pkg);
-                mContext.createContextAsUser(UserHandle.ALL, 0).sendOrderedBroadcast(
-                        intent, receiverPermission, appOp, null, getHandler(),
-                        Activity.RESULT_OK, null, null);
+                    R.array.config_defaultCellBroadcastReceiverPkgs);
+            if (pkgs != null) {
+                mReceiverCount.addAndGet(pkgs.length);
+                for (String pkg : pkgs) {
+                    // Explicitly send the intent to all the configured cell broadcast receivers.
+                    intent.setPackage(pkg);
+                    mContext.createContextAsUser(UserHandle.ALL, 0).sendOrderedBroadcast(
+                            intent, receiverPermission, appOp, mReceiver, getHandler(),
+                            Activity.RESULT_OK, null, null);
+                }
             }
         } else {
             msg = "Dispatching SMS CB, SmsCbMessage is: " + message;
@@ -578,18 +583,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
@@ -599,17 +595,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;
         }
 
         /**
@@ -617,103 +616,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..1369b52 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,37 @@
     /** Content uri of this provider. */
     public static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts");
 
+    /**
+     * 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.SUB_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,
+            CellBroadcasts.MAXIMUM_WAIT_TIME
+    };
+
     @VisibleForTesting
     public PermissionChecker mPermissionChecker;
 
@@ -148,7 +180,7 @@
         checkReadPermission(uri);
 
         if (DBG) {
-            Log.d(TAG, "query:"
+            Rlog.d(TAG, "query:"
                     + " uri = " + uri
                     + " projection = " + Arrays.toString(projection)
                     + " selection = " + selection
@@ -188,7 +220,7 @@
         checkWritePermission();
 
         if (DBG) {
-            Log.d(TAG, "insert:"
+            Rlog.d(TAG, "insert:"
                     + " uri = " + uri
                     + " contentValue = " + values);
         }
@@ -203,7 +235,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 +249,7 @@
         checkWritePermission();
 
         if (DBG) {
-            Log.d(TAG, "delete:"
+            Rlog.d(TAG, "delete:"
                     + " uri = " + uri
                     + " selection = " + selection
                     + " selectionArgs = " + Arrays.toString(selectionArgs));
@@ -238,7 +270,7 @@
         checkWritePermission();
 
         if (DBG) {
-            Log.d(TAG, "update:"
+            Rlog.d(TAG, "update:"
                     + " uri = " + uri
                     + " values = {" + values + "}"
                     + " selection = " + selection
@@ -345,12 +377,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) {
+            if (oldVersion < 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 fdf3b4a..3ae61bb 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,20 +101,30 @@
      * @param bearerData the bearerData of the SMS
      * @param serviceCategory the service category of the broadcast
      */
-    private SmsCbMessage parseBroadcastSms(int slotIndex, String plmn, byte[] bearerData,
+    @VisibleForTesting
+    public static SmsCbMessage parseBroadcastSms(Context context, int slotIndex, String plmn,
+            byte[] bearerData,
             int serviceCategory) {
-        BearerData bData = BearerData.decode(getApplicationContext(), bearerData, serviceCategory);
+        BearerData bData = BearerData.decode(context, bearerData, serviceCategory);
         if (bData == null) {
-            Log.w(TAG, "BearerData.decode() returned null");
+            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);
 
+        SubscriptionManager sm = (SubscriptionManager) context.getSystemService(
+                Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+        int subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
+        int[] subIds = sm.getSubscriptionIds(slotIndex);
+        if (subIds != null && subIds.length > 0) {
+            subId = subIds[0];
+        }
+
         return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP2,
                 SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE, bData.messageId, location,
                 serviceCategory, bData.getLanguage(), bData.userData.payloadStr,
-                bData.priority, null, bData.cmasWarningInfo, slotIndex);
+                bData.priority, null, bData.cmasWarningInfo, slotIndex, subId);
     }
 
     private static String toHexString(byte[] array, int offset, int length) {
diff --git a/src/com/android/cellbroadcastservice/GsmAlphabet.java b/src/com/android/cellbroadcastservice/GsmAlphabet.java
index aecaf1b..c305557 100644
--- a/src/com/android/cellbroadcastservice/GsmAlphabet.java
+++ b/src/com/android/cellbroadcastservice/GsmAlphabet.java
@@ -17,7 +17,7 @@
 package com.android.cellbroadcastservice;
 
 import android.annotation.UnsupportedAppUsage;
-import android.util.Log;
+import android.telephony.Rlog;
 import android.util.SparseIntArray;
 
 /**
@@ -584,7 +584,7 @@
         int numTables = sLanguageTables.length;
         int numShiftTables = sLanguageShiftTables.length;
         if (numTables != numShiftTables) {
-            Log.e(TAG, "Error: language tables array length " + numTables
+            Rlog.e(TAG, "Error: language tables array length " + numTables
                     + " != shift tables array length " + numShiftTables);
         }
 
@@ -594,7 +594,7 @@
 
             int tableLen = table.length();
             if (tableLen != 0 && tableLen != 128) {
-                Log.e(TAG, "Error: language tables index " + i + " length " + tableLen
+                Rlog.e(TAG, "Error: language tables index " + i + " length " + tableLen
                         + " (expected 128 or 0)");
             }
 
@@ -612,7 +612,7 @@
 
             int shiftTableLen = shiftTable.length();
             if (shiftTableLen != 0 && shiftTableLen != 128) {
-                Log.e(TAG, "Error: language shift tables index " + i + " length " + shiftTableLen
+                Rlog.e(TAG, "Error: language shift tables index " + i + " length " + shiftTableLen
                         + " (expected 128 or 0)");
             }
 
@@ -666,11 +666,11 @@
         StringBuilder ret = new StringBuilder(lengthSeptets);
 
         if (languageTable < 0 || languageTable > sLanguageTables.length) {
-            Log.w(TAG, "unknown language table " + languageTable + ", using default");
+            Rlog.w(TAG, "unknown language table " + languageTable + ", using default");
             languageTable = 0;
         }
         if (shiftTable < 0 || shiftTable > sLanguageShiftTables.length) {
-            Log.w(TAG, "unknown single shift table " + shiftTable + ", using default");
+            Rlog.w(TAG, "unknown single shift table " + shiftTable + ", using default");
             shiftTable = 0;
         }
 
@@ -680,11 +680,11 @@
             String shiftTableToChar = sLanguageShiftTables[shiftTable];
 
             if (languageTableToChar.isEmpty()) {
-                Log.w(TAG, "no language table for code " + languageTable + ", using default");
+                Rlog.w(TAG, "no language table for code " + languageTable + ", using default");
                 languageTableToChar = sLanguageTables[0];
             }
             if (shiftTableToChar.isEmpty()) {
-                Log.w(TAG, "no single shift table for code " + shiftTable + ", using default");
+                Rlog.w(TAG, "no single shift table for code " + shiftTable + ", using default");
                 shiftTableToChar = sLanguageShiftTables[0];
             }
 
@@ -724,7 +724,7 @@
                 }
             }
         } catch (RuntimeException ex) {
-            Log.e(TAG, "Error GSM 7 bit packed: ", ex);
+            Rlog.e(TAG, "Error GSM 7 bit packed: ", ex);
             return null;
         }
 
diff --git a/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java b/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java
index 2f16555..f2fbec5 100644
--- a/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java
+++ b/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java
@@ -61,7 +61,7 @@
             new HashMap<>(4);
 
     @VisibleForTesting
-    protected GsmCellBroadcastHandler(Context context, Looper looper) {
+    public GsmCellBroadcastHandler(Context context, Looper looper) {
         super("GsmCellBroadcastHandler", context, looper);
     }
 
@@ -131,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,
diff --git a/src/com/android/cellbroadcastservice/GsmSmsCbMessage.java b/src/com/android/cellbroadcastservice/GsmSmsCbMessage.java
index d17c084..8c65c49 100644
--- a/src/com/android/cellbroadcastservice/GsmSmsCbMessage.java
+++ b/src/com/android/cellbroadcastservice/GsmSmsCbMessage.java
@@ -27,17 +27,18 @@
 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.telephony.SubscriptionManager;
 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.annotations.VisibleForTesting;
 
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
@@ -66,7 +67,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:
@@ -92,6 +94,14 @@
     public static SmsCbMessage createSmsCbMessage(Context context, SmsCbHeader header,
             SmsCbLocation location, byte[][] pdus, int slotIndex)
             throws IllegalArgumentException {
+        SubscriptionManager sm = (SubscriptionManager) context.getSystemService(
+                Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+        int subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
+        int[] subIds = sm.getSubscriptionIds(slotIndex);
+        if (subIds != null && subIds.length > 0) {
+            subId = subIds[0];
+        }
+
         long receivedTimeMillis = System.currentTimeMillis();
         if (header.isEtwsPrimaryNotification()) {
             // ETSI TS 23.041 ETWS Primary Notification message
@@ -102,7 +112,7 @@
                     header.getSerialNumber(), location, header.getServiceCategory(), null,
                     getEtwsPrimaryMessage(context, header.getEtwsInfo().getWarningType()),
                     SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, header.getEtwsInfo(),
-                    header.getCmasInfo(), slotIndex);
+                    header.getCmasInfo(), slotIndex, subId);
         } else if (header.isUmtsFormat()) {
             // UMTS format has only 1 PDU
             byte[] pdu = pdus[0];
@@ -128,7 +138,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());
                 }
             }
 
@@ -136,7 +146,7 @@
                     header.getGeographicalScope(), header.getSerialNumber(), location,
                     header.getServiceCategory(), language, body, priority,
                     header.getEtwsInfo(), header.getCmasInfo(), maximumWaitingTimeSec, geometries,
-                    receivedTimeMillis, slotIndex);
+                    receivedTimeMillis, slotIndex, subId);
         } else {
             String language = null;
             StringBuilder sb = new StringBuilder();
@@ -151,7 +161,7 @@
             return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
                     header.getGeographicalScope(), header.getSerialNumber(), location,
                     header.getServiceCategory(), language, sb.toString(), priority,
-                    header.getEtwsInfo(), header.getCmasInfo(), slotIndex);
+                    header.getEtwsInfo(), header.getCmasInfo(), slotIndex, subId);
         }
     }
 
@@ -194,7 +204,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;
         }
     }
@@ -488,7 +498,11 @@
             return type == TYPE_ACTIVE_ALERT_SHARE_WAC;
         }
 
-        static final class CellBroadcastIdentity {
+        /**
+         * The GSM cell broadcast identity
+         */
+        @VisibleForTesting
+        public static final class CellBroadcastIdentity {
             public final int messageIdentifier;
             public final int serialNumber;
             CellBroadcastIdentity(int messageIdentifier, int serialNumber) {
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/WakeLockStateMachine.java b/src/com/android/cellbroadcastservice/WakeLockStateMachine.java
index 1874cef..1d08f54 100644
--- a/src/com/android/cellbroadcastservice/WakeLockStateMachine.java
+++ b/src/com/android/cellbroadcastservice/WakeLockStateMachine.java
@@ -16,7 +16,6 @@
 
 package com.android.cellbroadcastservice;
 
-import android.annotation.UnsupportedAppUsage;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -24,7 +23,7 @@
 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;
@@ -55,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);
@@ -64,7 +62,6 @@
     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();
 
@@ -236,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);
     }
 
     /**
@@ -248,7 +244,7 @@
      */
     @Override
     protected void loge(String s) {
-        Log.e(getName(), s);
+        Rlog.e(getName(), s);
     }
 
     /**
@@ -258,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/GsmSmsCbMessageTest.java b/tests/src/com/android/cellbroadcastservice/GsmSmsCbMessageTest.java
deleted file mode 100644
index 7cacfb4..0000000
--- a/tests/src/com/android/cellbroadcastservice/GsmSmsCbMessageTest.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * 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.SmsCbCmasInfo;
-import android.telephony.SmsCbMessage;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.testing.AndroidTestingRunner;
-import android.util.Log;
-
-import com.android.cellbroadcastservice.CbGeoUtils.Circle;
-import com.android.cellbroadcastservice.CbGeoUtils.Polygon;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.List;
-
-@RunWith(AndroidTestingRunner.class)
-public class GsmSmsCbMessageTest extends CellBroadcastServiceTestBase {
-
-    private static final String TAG = "GsmSmsCbMessageTest";
-
-    @Test
-    @SmallTest
-    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);
-
-        Log.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
-    @SmallTest
-    public void testCreateTriggerMessage() throws Exception {
-        final byte[] pdu = hexStringToBytes("0001113001010010C0111204D2");
-        GsmSmsCbMessage.GeoFencingTriggerMessage triggerMessage =
-                GsmSmsCbMessage.createGeoFencingTriggerMessage(pdu);
-
-        Log.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);
-    }
-}
diff --git a/tests/src/com/android/cellbroadcastservice/tests/CdmaSmsMessageTest.java b/tests/src/com/android/cellbroadcastservice/tests/CdmaSmsMessageTest.java
new file mode 100644
index 0000000..7e34a40
--- /dev/null
+++ b/tests/src/com/android/cellbroadcastservice/tests/CdmaSmsMessageTest.java
@@ -0,0 +1,877 @@
+/*
+ * 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.tests;
+
+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.cellbroadcastservice.BearerData;
+import com.android.cellbroadcastservice.DefaultCellBroadcastService;
+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(com.android.cellbroadcastservice.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/tests/CellBroadcastHandlerTest.java
similarity index 88%
rename from tests/src/com/android/cellbroadcastservice/CellBroadcastHandlerTest.java
rename to tests/src/com/android/cellbroadcastservice/tests/CellBroadcastHandlerTest.java
index 766ea03..3fce3c5 100644
--- a/tests/src/com/android/cellbroadcastservice/CellBroadcastHandlerTest.java
+++ b/tests/src/com/android/cellbroadcastservice/tests/CellBroadcastHandlerTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.cellbroadcastservice;
+package com.android.cellbroadcastservice.tests;
 
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doReturn;
@@ -35,6 +35,10 @@
 import android.testing.TestableLooper;
 import android.text.format.DateUtils;
 
+import com.android.cellbroadcastservice.CellBroadcastHandler;
+import com.android.cellbroadcastservice.CellBroadcastProvider;
+import com.android.cellbroadcastservice.SmsCbConstants;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -57,14 +61,15 @@
     private class CellBroadcastContentProvider extends MockContentProvider {
         @Override
         public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
-                            String sortOrder) {
+                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
                         0,              // SLOT_INDEX
+                        1,              // SUB_ID
                         0,              // GEOGRAPHICAL_SCOPE
                         "311480",       // PLMN
                         0,              // LAC
@@ -115,7 +120,8 @@
         doReturn(mMockedResources).when(mMockedResourcesCache).get(anyInt());
         replaceInstance(CellBroadcastHandler.class, "mResourcesCache", mCellBroadcastHandler,
                 mMockedResourcesCache);
-        putResources(R.integer.message_expiration_time, (int) DateUtils.DAY_IN_MILLIS);
+        putResources(com.android.cellbroadcastservice.R.integer.message_expiration_time,
+                (int) DateUtils.DAY_IN_MILLIS);
     }
 
     @After
@@ -124,11 +130,11 @@
     }
 
     private SmsCbMessage createSmsCbMessage(int serialNumber, int serviceCategory,
-                                            String messageBody) {
+            String messageBody) {
         return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
                 0, serialNumber, new SmsCbLocation(),
                 serviceCategory, "en", messageBody, 3,
-                null, null, 0);
+                null, null, 0, 1);
     }
 
     @Test
@@ -155,7 +161,7 @@
     @Test
     @SmallTest
     public void testNotDuplicateMessageBodyDifferent() throws Exception {
-        putResources(R.bool.duplicate_compare_body, true);
+        putResources(com.android.cellbroadcastservice.R.bool.duplicate_compare_body, true);
         SmsCbMessage msg = createSmsCbMessage(1234, 4370, "msg");
         assertFalse(mCellBroadcastHandler.isDuplicate(msg));
     }
diff --git a/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTest.java b/tests/src/com/android/cellbroadcastservice/tests/CellBroadcastProviderTest.java
similarity index 93%
rename from tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTest.java
rename to tests/src/com/android/cellbroadcastservice/tests/CellBroadcastProviderTest.java
index dc9837f..b19d55f 100644
--- a/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTest.java
+++ b/tests/src/com/android/cellbroadcastservice/tests/CellBroadcastProviderTest.java
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.cellbroadcastservice;
+package com.android.cellbroadcastservice.tests;
+
+import static com.android.cellbroadcastservice.CellBroadcastProvider.QUERY_COLUMNS;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -25,10 +27,11 @@
 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;
 import com.android.cellbroadcastservice.CellBroadcastProvider.PermissionChecker;
 
 import junit.framework.TestCase;
@@ -65,30 +68,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 +325,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/tests/CellBroadcastProviderTestable.java
similarity index 90%
rename from tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTestable.java
rename to tests/src/com/android/cellbroadcastservice/tests/CellBroadcastProviderTestable.java
index 2be7512..06fde6b 100644
--- a/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTestable.java
+++ b/tests/src/com/android/cellbroadcastservice/tests/CellBroadcastProviderTestable.java
@@ -14,16 +14,18 @@
  * limitations under the License.
  */
 
-package com.android.cellbroadcastservice;
+package com.android.cellbroadcastservice.tests;
 
 import android.content.Context;
 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;
 
+import com.android.cellbroadcastservice.CellBroadcastProvider;
+
 public class CellBroadcastProviderTestable extends CellBroadcastProvider {
     private static final String TAG = CellBroadcastProviderTestable.class.getSimpleName();
 
@@ -34,7 +36,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 +55,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/tests/CellBroadcastServiceTestBase.java
similarity index 97%
rename from tests/src/com/android/cellbroadcastservice/CellBroadcastServiceTestBase.java
rename to tests/src/com/android/cellbroadcastservice/tests/CellBroadcastServiceTestBase.java
index 74659dd..c230f2d 100644
--- a/tests/src/com/android/cellbroadcastservice/CellBroadcastServiceTestBase.java
+++ b/tests/src/com/android/cellbroadcastservice/tests/CellBroadcastServiceTestBase.java
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.cellbroadcastservice;
+package com.android.cellbroadcastservice.tests;
 
 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;
@@ -135,6 +136,7 @@
             return null;
         }).when(mMockedContext).registerReceiver(
                 any(BroadcastReceiver.class), any(IntentFilter.class));
+        doReturn(true).when(mMockedLocationManager).isProviderEnabled(anyString());
     }
 
     protected void tearDown() throws Exception {
diff --git a/tests/src/com/android/cellbroadcastservice/GsmCellBroadcastHandlerTest.java b/tests/src/com/android/cellbroadcastservice/tests/GsmCellBroadcastHandlerTest.java
similarity index 76%
rename from tests/src/com/android/cellbroadcastservice/GsmCellBroadcastHandlerTest.java
rename to tests/src/com/android/cellbroadcastservice/tests/GsmCellBroadcastHandlerTest.java
index 1ff4604..1fefbbf 100644
--- a/tests/src/com/android/cellbroadcastservice/GsmCellBroadcastHandlerTest.java
+++ b/tests/src/com/android/cellbroadcastservice/tests/GsmCellBroadcastHandlerTest.java
@@ -14,13 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.cellbroadcastservice;
+package com.android.cellbroadcastservice.tests;
 
 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;
@@ -28,12 +29,13 @@
 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;
@@ -42,14 +44,21 @@
 import android.testing.TestableLooper;
 import android.text.format.DateUtils;
 
+import com.android.cellbroadcastservice.CellBroadcastHandler;
+import com.android.cellbroadcastservice.CellBroadcastProvider;
+import com.android.cellbroadcastservice.GsmCellBroadcastHandler;
+import com.android.cellbroadcastservice.SmsCbConstants;
+
 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
@@ -72,11 +81,12 @@
 
             if (uri.compareTo(Telephony.CellBroadcasts.CONTENT_URI) == 0
                     && Long.parseLong(selectionArgs[selectionArgs.length - 1]) <= receivedTime) {
-                MatrixCursor mc = new MatrixCursor(Telephony.CellBroadcasts.QUERY_COLUMNS_FWK);
+                MatrixCursor mc = new MatrixCursor(CellBroadcastProvider.QUERY_COLUMNS);
 
                 mc.addRow(new Object[]{
                         1,              // _ID
                         0,              // SLOT_INDEX
+                        1,              // SUB_ID
                         0,              // GEOGRAPHICAL_SCOPE
                         "311480",       // PLMN
                         0,              // LAC
@@ -149,8 +159,9 @@
         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,
+        putResources(com.android.cellbroadcastservice.R.integer.message_expiration_time, 86400000);
+        putResources(
+                com.android.cellbroadcastservice.R.array.config_defaultCellBroadcastReceiverPkgs,
                 new String[]{"fake.cellbroadcast.pkg"});
     }
 
@@ -159,17 +170,10 @@
         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();
@@ -190,7 +194,8 @@
     @Test
     @SmallTest
     public void testAirplaneModeReset() {
-        putResources(R.bool.reset_on_power_cycle_or_airplane_mode, true);
+        putResources(com.android.cellbroadcastservice.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.
@@ -203,4 +208,27 @@
         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/tests/GsmSmsCbMessageTest.java b/tests/src/com/android/cellbroadcastservice/tests/GsmSmsCbMessageTest.java
new file mode 100644
index 0000000..1547a56
--- /dev/null
+++ b/tests/src/com/android/cellbroadcastservice/tests/GsmSmsCbMessageTest.java
@@ -0,0 +1,1065 @@
+/*
+ * 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.tests;
+
+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 com.android.cellbroadcastservice.GsmSmsCbMessage;
+import com.android.cellbroadcastservice.R;
+import com.android.cellbroadcastservice.SmsCbConstants;
+import com.android.cellbroadcastservice.SmsCbHeader;
+
+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);
+            }
+        }
+    }
+}