Initial move to CellBroadcastService dir

Bug: 135956699
Test: manually verified that framework still binds to CB service
Change-Id: I34b4e8070d56c69d1ac799159bba085e387d768a
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..cbd991a
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,25 @@
+// Copyright 2019 The Android Open Source Project
+
+android_app {
+    name: "CellBroadcastServiceModule",
+    srcs: [
+      "src/**/*.java",
+      ":framework-cellbroadcast-shared-srcs",
+    ],
+    libs: ["telephony-common"],
+    platform_apis: true,
+    certificate: "platform",
+    privileged: true,
+    resource_dirs: ["res"],
+    static_libs: [
+        "androidx.legacy_legacy-support-v4",
+        "androidx.legacy_legacy-support-v13",
+        "androidx.recyclerview_recyclerview",
+        "androidx.preference_preference",
+        "androidx.appcompat_appcompat",
+        "androidx.legacy_legacy-preference-v14",
+    ],
+    optimize: {
+        proguard_flags_files: ["proguard.flags"],
+    },
+}
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..2dd2140
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,49 @@
+<?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-sdk android:minSdkVersion="29"/>
+
+    <application android:label="Module used to handle cell broadcasts."
+            android:defaultToDeviceProtectedStorage="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/OWNERS b/OWNERS
new file mode 100644
index 0000000..9a61858
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,7 @@
+amitmahajan@google.com
+fionaxu@google.com
+jackyu@google.com
+rgreenwalt@google.com
+refuhoo@google.com
+jminjie@google.com
+shuoq@google.com
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
new file mode 100644
index 0000000..f3db20e
--- /dev/null
+++ b/PREUPLOAD.cfg
@@ -0,0 +1,2 @@
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
diff --git a/cellbroadcast-jarjar-rules.txt b/cellbroadcast-jarjar-rules.txt
new file mode 100644
index 0000000..b50ec50
--- /dev/null
+++ b/cellbroadcast-jarjar-rules.txt
@@ -0,0 +1,4 @@
+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
diff --git a/proguard.flags b/proguard.flags
new file mode 100644
index 0000000..873e7c0
--- /dev/null
+++ b/proguard.flags
@@ -0,0 +1,32 @@
+# This is a configuration file for ProGuard.
+# http://proguard.sourceforge.net/index.html#manual/usage.html
+
+# We want to keep methods in Activity that could be used in the XML attribute onClick.
+-keepclassmembers class * extends android.app.Activity {
+    public void *(android.view.View);
+    public void *(android.view.MenuItem);
+}
+
+# Keep setters in Views so that animations can still work.
+-keep public class * extends android.view.View {
+    public <init>(android.content.Context);
+    public <init>(android.content.Context, android.util.AttributeSet);
+    public <init>(android.content.Context, android.util.AttributeSet, int);
+
+    void set*(***);
+    *** get*();
+}
+
+# Keep classes that may be inflated from XML.
+-keepclasseswithmembers class * {
+    public <init>(android.content.Context, android.util.AttributeSet);
+}
+-keepclasseswithmembers class * {
+    public <init>(android.content.Context, android.util.AttributeSet, int);
+}
+
+# Keep annotated classes or class members.
+-keep @androidx.annotation.Keep class *
+-keepclassmembers class * {
+    @androidx.annotation.Keep *;
+}
\ No newline at end of file
diff --git a/res/values-mcc310-mnc120/config.xml b/res/values-mcc310-mnc120/config.xml
new file mode 100644
index 0000000..3b86e25
--- /dev/null
+++ b/res/values-mcc310-mnc120/config.xml
@@ -0,0 +1,21 @@
+<?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>
+    <!-- If this value is true, SMS encoded as octet is decoded by utf8 decoder.
+      If false, decoded by Latin decoder. -->
+    <bool name="config_sms_utf8_support">false</bool>
+</resources>
diff --git a/res/values/config.xml b/res/values/config.xml
new file mode 100644
index 0000000..3b86e25
--- /dev/null
+++ b/res/values/config.xml
@@ -0,0 +1,21 @@
+<?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>
+    <!-- If this value is true, SMS encoded as octet is decoded by utf8 decoder.
+      If false, decoded by Latin decoder. -->
+    <bool name="config_sms_utf8_support">false</bool>
+</resources>
diff --git a/src/com/android/cellbroadcastservice/BearerData.java b/src/com/android/cellbroadcastservice/BearerData.java
new file mode 100644
index 0000000..584cd15
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/BearerData.java
@@ -0,0 +1,641 @@
+/*
+ * 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.content.res.Resources;
+import android.telephony.SmsCbCmasInfo;
+import android.telephony.cdma.CdmaSmsCbProgramData;
+import android.util.Log;
+
+import com.android.internal.telephony.GsmAlphabet;
+import com.android.internal.telephony.SmsHeader;
+import com.android.internal.util.BitwiseInputStream;
+
+/**
+ * An object to decode CDMA SMS bearer data.
+ */
+public final class BearerData {
+    private final static String LOG_TAG = "BearerData";
+
+    /**
+     * Bearer Data Subparameter Identifiers
+     * (See 3GPP2 C.S0015-B, v2.0, table 4.5-1)
+     * NOTE: Unneeded subparameter types are not included
+     */
+    private final static byte SUBPARAM_MESSAGE_IDENTIFIER               = 0x00;
+    private final static byte SUBPARAM_USER_DATA                        = 0x01;
+    private final static byte SUBPARAM_PRIORITY_INDICATOR               = 0x08;
+    private final static byte SUBPARAM_LANGUAGE_INDICATOR               = 0x0D;
+
+    // All other values after this are reserved.
+    private final static byte SUBPARAM_ID_LAST_DEFINED                  = 0x17;
+
+    /**
+     * Supported priority modes for CDMA SMS messages
+     * (See 3GPP2 C.S0015-B, v2.0, table 4.5.9-1)
+     */
+    public static final int PRIORITY_NORMAL        = 0x0;
+    public static final int PRIORITY_INTERACTIVE   = 0x1;
+    public static final int PRIORITY_URGENT        = 0x2;
+    public static final int PRIORITY_EMERGENCY     = 0x3;
+
+    /**
+     * Language Indicator values.  NOTE: the spec (3GPP2 C.S0015-B,
+     * v2, 4.5.14) is ambiguous as to the meaning of this field, as it
+     * refers to C.R1001-D but that reference has been crossed out.
+     * It would seem reasonable to assume the values from C.R1001-F
+     * (table 9.2-1) are to be used instead.
+     */
+    public static final int LANGUAGE_UNKNOWN  = 0x00;
+    public static final int LANGUAGE_ENGLISH  = 0x01;
+    public static final int LANGUAGE_FRENCH   = 0x02;
+    public static final int LANGUAGE_SPANISH  = 0x03;
+    public static final int LANGUAGE_JAPANESE = 0x04;
+    public static final int LANGUAGE_KOREAN   = 0x05;
+    public static final int LANGUAGE_CHINESE  = 0x06;
+    public static final int LANGUAGE_HEBREW   = 0x07;
+
+    /**
+     * 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)
+     */
+    public int messageId;
+
+    /**
+     * Priority modes for CDMA SMS message (See 3GPP2 C.S0015-B, v2.0, table 4.5.9-1)
+     */
+    public int priority = PRIORITY_NORMAL;
+
+    /**
+     * Language indicator for CDMA SMS message.
+     */
+    public int language = LANGUAGE_UNKNOWN;
+
+    /**
+     * 1-bit value that indicates whether a User Data Header (UDH) is present.
+     * (See 3GPP2 C.S0015-B, v2, 4.5.1)
+     *
+     * NOTE: during encoding, this value will be set based on the
+     * presence of a UDH in the structured data, any existing setting
+     * will be overwritten.
+     */
+    public boolean hasUserDataHeader;
+
+    /**
+     * Information on the user data
+     * (e.g. padding bits, user data, user data header, etc)
+     * (See 3GPP2 C.S.0015-B, v2, 4.5.2)
+     */
+    public UserData userData;
+
+    /**
+     * CMAS warning notification information.
+     * @see #decodeCmasUserData(BearerData, int)
+     */
+    public SmsCbCmasInfo cmasWarningInfo;
+
+    /**
+     * Construct an empty BearerData.
+     */
+    private BearerData() {}
+
+    private static class CodingException extends Exception {
+        public CodingException(String s) {
+            super(s);
+        }
+    }
+
+    /**
+     * Returns the language indicator as a two-character ISO 639 string.
+     * @return a two character ISO 639 language code
+     */
+    public String getLanguage() {
+        return getLanguageCodeForValue(language);
+    }
+
+    /**
+     * Converts a CDMA language indicator value to an ISO 639 two character language code.
+     * @param languageValue the CDMA language value to convert
+     * @return the two character ISO 639 language code for the specified value, or null if unknown
+     */
+    private static String getLanguageCodeForValue(int languageValue) {
+        switch (languageValue) {
+            case LANGUAGE_ENGLISH:
+                return "en";
+
+            case LANGUAGE_FRENCH:
+                return "fr";
+
+            case LANGUAGE_SPANISH:
+                return "es";
+
+            case LANGUAGE_JAPANESE:
+                return "ja";
+
+            case LANGUAGE_KOREAN:
+                return "ko";
+
+            case LANGUAGE_CHINESE:
+                return "zh";
+
+            case LANGUAGE_HEBREW:
+                return "he";
+
+            default:
+                return null;
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("BearerData ");
+        builder.append(", messageId=" + messageId);
+        builder.append(", hasUserDataHeader=" + hasUserDataHeader);
+        builder.append(", userData=" + userData);
+        builder.append(" }");
+        return builder.toString();
+    }
+
+    private static boolean decodeMessageId(BearerData bData, BitwiseInputStream inStream)
+            throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 3 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            inStream.skip(4); // skip messageType
+            bData.messageId = inStream.read(8) << 8;
+            bData.messageId |= inStream.read(8);
+            bData.hasUserDataHeader = (inStream.read(1) == 1);
+            inStream.skip(3);
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Log.d(LOG_TAG, "MESSAGE_IDENTIFIER decode " +
+                    (decodeSuccess ? "succeeded" : "failed") +
+                    " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        return decodeSuccess;
+    }
+
+    private static boolean decodeReserved(BitwiseInputStream inStream, int subparamId)
+            throws BitwiseInputStream.AccessException, CodingException
+    {
+        boolean decodeSuccess = false;
+        int subparamLen = inStream.read(8); // SUBPARAM_LEN
+        int paramBits = subparamLen * 8;
+        if (paramBits <= inStream.available()) {
+            decodeSuccess = true;
+            inStream.skip(paramBits);
+        }
+        Log.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
+                    + " had invalid SUBPARAM_LEN " + subparamLen);
+        }
+
+        return decodeSuccess;
+    }
+
+    private static boolean decodeUserData(BearerData bData, BitwiseInputStream inStream)
+            throws BitwiseInputStream.AccessException
+    {
+        int paramBits = inStream.read(8) * 8;
+        bData.userData = new UserData();
+        bData.userData.msgEncoding = inStream.read(5);
+        bData.userData.msgEncodingSet = true;
+        bData.userData.msgType = 0;
+        int consumedBits = 5;
+        if ((bData.userData.msgEncoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) ||
+                (bData.userData.msgEncoding == UserData.ENCODING_GSM_DCS)) {
+            bData.userData.msgType = inStream.read(8);
+            consumedBits += 8;
+        }
+        bData.userData.numFields = inStream.read(8);
+        consumedBits += 8;
+        int dataBits = paramBits - consumedBits;
+        bData.userData.payload = inStream.readByteArray(dataBits);
+        return true;
+    }
+
+    private static String decodeUtf8(byte[] data, int offset, int numFields)
+            throws CodingException
+    {
+        return decodeCharset(data, offset, numFields, 1, "UTF-8");
+    }
+
+    private static String decodeUtf16(byte[] data, int offset, int numFields)
+            throws CodingException
+    {
+        // Subtract header and possible padding byte (at end) from num fields.
+        int padding = offset % 2;
+        numFields -= (offset + padding) / 2;
+        return decodeCharset(data, offset, numFields, 2, "utf-16be");
+    }
+
+    private static String decodeCharset(byte[] data, int offset, int numFields, int width,
+            String charset) throws CodingException
+    {
+        if (numFields < 0 || (numFields * width + offset) > data.length) {
+            // Try to decode the max number of characters in payload
+            int padding = offset % width;
+            int maxNumFields = (data.length - offset - padding) / width;
+            if (maxNumFields < 0) {
+                throw new CodingException(charset + " decode failed: offset out of range");
+            }
+            Log.e(LOG_TAG, charset + " decode error: offset = " + offset + " numFields = "
+                    + numFields + " data.length = " + data.length + " maxNumFields = "
+                    + maxNumFields);
+            numFields = maxNumFields;
+        }
+        try {
+            return new String(data, offset, numFields * width, charset);
+        } catch (java.io.UnsupportedEncodingException ex) {
+            throw new CodingException(charset + " decode failed: " + ex);
+        }
+    }
+
+    private static String decode7bitAscii(byte[] data, int offset, int numFields)
+            throws CodingException
+    {
+        try {
+            int offsetBits = offset * 8;
+            int offsetSeptets = (offsetBits + 6) / 7;
+            numFields -= offsetSeptets;
+
+            StringBuffer strBuf = new StringBuffer(numFields);
+            BitwiseInputStream inStream = new BitwiseInputStream(data);
+            int wantedBits = (offsetSeptets * 7) + (numFields * 7);
+            if (inStream.available() < wantedBits) {
+                throw new CodingException("insufficient data (wanted " + wantedBits +
+                        " bits, but only have " + inStream.available() + ")");
+            }
+            inStream.skip(offsetSeptets * 7);
+            for (int i = 0; i < numFields; i++) {
+                int charCode = inStream.read(7);
+                if ((charCode >= UserData.ASCII_MAP_BASE_INDEX) &&
+                        (charCode <= UserData.ASCII_MAP_MAX_INDEX)) {
+                    strBuf.append(UserData.ASCII_MAP[charCode - UserData.ASCII_MAP_BASE_INDEX]);
+                } else if (charCode == UserData.ASCII_NL_INDEX) {
+                    strBuf.append('\n');
+                } else if (charCode == UserData.ASCII_CR_INDEX) {
+                    strBuf.append('\r');
+                } else {
+                    /* For other charCodes, they are unprintable, and so simply use SPACE. */
+                    strBuf.append(' ');
+                }
+            }
+            return strBuf.toString();
+        } catch (BitwiseInputStream.AccessException ex) {
+            throw new CodingException("7bit ASCII decode failed: " + ex);
+        }
+    }
+
+    private static String decode7bitGsm(byte[] data, int offset, int numFields)
+            throws CodingException
+    {
+        // Start reading from the next 7-bit aligned boundary after offset.
+        int offsetBits = offset * 8;
+        int offsetSeptets = (offsetBits + 6) / 7;
+        numFields -= offsetSeptets;
+        int paddingBits = (offsetSeptets * 7) - offsetBits;
+        String result = GsmAlphabet.gsm7BitPackedToString(data, offset, numFields, paddingBits,
+                0, 0);
+        if (result == null) {
+            throw new CodingException("7bit GSM decoding failed");
+        }
+        return result;
+    }
+
+    private static String decodeLatin(byte[] data, int offset, int numFields)
+            throws CodingException
+    {
+        return decodeCharset(data, offset, numFields, 1, "ISO-8859-1");
+    }
+
+    private static String decodeShiftJis(byte[] data, int offset, int numFields)
+            throws CodingException
+    {
+        return decodeCharset(data, offset, numFields, 1, "Shift_JIS");
+    }
+
+    private static String decodeGsmDcs(byte[] data, int offset, int numFields, int msgType)
+            throws CodingException
+    {
+        if ((msgType & 0xC0) != 0) {
+            throw new CodingException("unsupported coding group ("
+                    + msgType + ")");
+        }
+
+        switch ((msgType >> 2) & 0x3) {
+            case UserData.ENCODING_GSM_DCS_7BIT:
+                return decode7bitGsm(data, offset, numFields);
+            case UserData.ENCODING_GSM_DCS_8BIT:
+                return decodeUtf8(data, offset, numFields);
+            case UserData.ENCODING_GSM_DCS_16BIT:
+                return decodeUtf16(data, offset, numFields);
+            default:
+                throw new CodingException("unsupported user msgType encoding ("
+                        + msgType + ")");
+        }
+    }
+
+    private static void decodeUserDataPayload(UserData userData, boolean hasUserDataHeader)
+            throws CodingException
+    {
+        int offset = 0;
+        if (hasUserDataHeader) {
+            int udhLen = userData.payload[0] & 0x00FF;
+            offset += udhLen + 1;
+            byte[] headerData = new byte[udhLen];
+            System.arraycopy(userData.payload, 1, headerData, 0, udhLen);
+            userData.userDataHeader = SmsHeader.fromByteArray(headerData);
+        }
+        switch (userData.msgEncoding) {
+            case UserData.ENCODING_OCTET:
+                /*
+                 *  Octet decoding depends on the carrier service.
+                 */
+                boolean decodingtypeUTF8 = Resources.getSystem()
+                        .getBoolean(R.bool.config_sms_utf8_support);
+
+                // Strip off any padding bytes, meaning any differences between the length of the
+                // array and the target length specified by numFields.  This is to avoid any
+                // confusion by code elsewhere that only considers the payload array length.
+                byte[] payload = new byte[userData.numFields];
+                int copyLen = userData.numFields < userData.payload.length
+                        ? userData.numFields : userData.payload.length;
+
+                System.arraycopy(userData.payload, 0, payload, 0, copyLen);
+                userData.payload = payload;
+
+                if (!decodingtypeUTF8) {
+                    // There are many devices in the market that send 8bit text sms (latin encoded) as
+                    // octet encoded.
+                    userData.payloadStr = decodeLatin(userData.payload, offset, userData.numFields);
+                } else {
+                    userData.payloadStr = decodeUtf8(userData.payload, offset, userData.numFields);
+                }
+                break;
+
+            case UserData.ENCODING_IA5:
+            case UserData.ENCODING_7BIT_ASCII:
+                userData.payloadStr = decode7bitAscii(userData.payload, offset, userData.numFields);
+                break;
+            case UserData.ENCODING_UNICODE_16:
+                userData.payloadStr = decodeUtf16(userData.payload, offset, userData.numFields);
+                break;
+            case UserData.ENCODING_GSM_7BIT_ALPHABET:
+                userData.payloadStr = decode7bitGsm(userData.payload, offset, userData.numFields);
+                break;
+            case UserData.ENCODING_LATIN:
+                userData.payloadStr = decodeLatin(userData.payload, offset, userData.numFields);
+                break;
+            case UserData.ENCODING_SHIFT_JIS:
+                userData.payloadStr = decodeShiftJis(userData.payload, offset, userData.numFields);
+                break;
+            case UserData.ENCODING_GSM_DCS:
+                userData.payloadStr = decodeGsmDcs(userData.payload, offset,
+                        userData.numFields, userData.msgType);
+                break;
+            default:
+                throw new CodingException("unsupported user data encoding ("
+                        + userData.msgEncoding + ")");
+        }
+    }
+
+    private static boolean decodeLanguageIndicator(BearerData bData, BitwiseInputStream inStream)
+            throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 1 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.language = inStream.read(8);
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Log.d(LOG_TAG, "LANGUAGE_INDICATOR decode " +
+                    (decodeSuccess ? "succeeded" : "failed") +
+                    " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        return decodeSuccess;
+    }
+
+    private static boolean decodePriorityIndicator(BearerData bData, BitwiseInputStream inStream)
+            throws BitwiseInputStream.AccessException {
+        final int EXPECTED_PARAM_SIZE = 1 * 8;
+        boolean decodeSuccess = false;
+        int paramBits = inStream.read(8) * 8;
+        if (paramBits >= EXPECTED_PARAM_SIZE) {
+            paramBits -= EXPECTED_PARAM_SIZE;
+            decodeSuccess = true;
+            bData.priority = inStream.read(2);
+            inStream.skip(6);
+        }
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Log.d(LOG_TAG, "PRIORITY_INDICATOR decode " +
+                    (decodeSuccess ? "succeeded" : "failed") +
+                    " (extra bits = " + paramBits + ")");
+        }
+        inStream.skip(paramBits);
+        return decodeSuccess;
+    }
+
+    private static int serviceCategoryToCmasMessageClass(int serviceCategory) {
+        switch (serviceCategory) {
+            case CdmaSmsCbProgramData.CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT:
+                return SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT;
+
+            case CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT:
+                return SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT;
+
+            case CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT:
+                return SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT;
+
+            case CdmaSmsCbProgramData.CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY:
+                return SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY;
+
+            case CdmaSmsCbProgramData.CATEGORY_CMAS_TEST_MESSAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST;
+
+            default:
+                return SmsCbCmasInfo.CMAS_CLASS_UNKNOWN;
+        }
+    }
+
+    /**
+     * CMAS message decoding.
+     * (See TIA-1149-0-1, CMAS over CDMA)
+     *
+     * @param serviceCategory is the service category from the SMS envelope
+     */
+    private static void decodeCmasUserData(BearerData bData, int serviceCategory)
+            throws BitwiseInputStream.AccessException, CodingException {
+        BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload);
+        if (inStream.available() < 8) {
+            throw new CodingException("emergency CB with no CMAE_protocol_version");
+        }
+        int protocolVersion = inStream.read(8);
+        if (protocolVersion != 0) {
+            throw new CodingException("unsupported CMAE_protocol_version " + protocolVersion);
+        }
+
+        int messageClass = serviceCategoryToCmasMessageClass(serviceCategory);
+        int category = SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN;
+        int responseType = SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN;
+        int severity = SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN;
+        int urgency = SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN;
+        int certainty = SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN;
+
+        while (inStream.available() >= 16) {
+            int recordType = inStream.read(8);
+            int recordLen = inStream.read(8);
+            switch (recordType) {
+                case 0:     // Type 0 elements (Alert text)
+                    UserData alertUserData = new UserData();
+                    alertUserData.msgEncoding = inStream.read(5);
+                    alertUserData.msgEncodingSet = true;
+                    alertUserData.msgType = 0;
+
+                    int numFields;                          // number of chars to decode
+                    switch (alertUserData.msgEncoding) {
+                        case UserData.ENCODING_OCTET:
+                        case UserData.ENCODING_LATIN:
+                            numFields = recordLen - 1;      // subtract 1 byte for encoding
+                            break;
+
+                        case UserData.ENCODING_IA5:
+                        case UserData.ENCODING_7BIT_ASCII:
+                        case UserData.ENCODING_GSM_7BIT_ALPHABET:
+                            numFields = ((recordLen * 8) - 5) / 7;  // subtract 5 bits for encoding
+                            break;
+
+                        case UserData.ENCODING_UNICODE_16:
+                            numFields = (recordLen - 1) / 2;
+                            break;
+
+                        default:
+                            numFields = 0;      // unsupported encoding
+                    }
+
+                    alertUserData.numFields = numFields;
+                    alertUserData.payload = inStream.readByteArray(recordLen * 8 - 5);
+                    decodeUserDataPayload(alertUserData, false);
+                    bData.userData = alertUserData;
+                    break;
+
+                case 1:     // Type 1 elements
+                    category = inStream.read(8);
+                    responseType = inStream.read(8);
+                    severity = inStream.read(4);
+                    urgency = inStream.read(4);
+                    certainty = inStream.read(4);
+                    inStream.skip(recordLen * 8 - 28);
+                    break;
+
+                default:
+                    Log.w(LOG_TAG, "skipping unsupported CMAS record type " + recordType);
+                    inStream.skip(recordLen * 8);
+                    break;
+            }
+        }
+
+        bData.cmasWarningInfo = new SmsCbCmasInfo(messageClass, category, responseType, severity,
+                urgency, certainty);
+    }
+
+    private static boolean isCmasAlertCategory(int category) {
+        return category >= CdmaSmsCbProgramData.CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT
+                && category <= CdmaSmsCbProgramData.CATEGORY_CMAS_LAST_RESERVED_VALUE;
+    }
+
+    /**
+     * Create BearerData object from serialized representation.
+     * (See 3GPP2 C.R1001-F, v1.0, section 4.5 for layout details)
+     *
+     * @param smsData byte array of raw encoded SMS bearer data.
+     * @param serviceCategory the envelope service category (for CMAS alert handling)
+     * @return an instance of BearerData.
+     */
+    public static BearerData decode(byte[] smsData, int serviceCategory) {
+        try {
+            BitwiseInputStream inStream = new BitwiseInputStream(smsData);
+            BearerData bData = new BearerData();
+            int foundSubparamMask = 0;
+            while (inStream.available() > 0) {
+                int subparamId = inStream.read(8);
+                int subparamIdBit = 1 << subparamId;
+                // int is 4 bytes. This duplicate check has a limit to Id number up to 32 (4*8)
+                // as 32th bit is the max bit in int.
+                // Per 3GPP2 C.S0015-B Table 4.5-1 Bearer Data Subparameter Identifiers:
+                // last defined subparam ID is 23 (00010111 = 0x17 = 23).
+                // Only do duplicate subparam ID check if subparam is within defined value as
+                // reserved subparams are just skipped.
+                if ((foundSubparamMask & subparamIdBit) != 0 &&
+                        (subparamId >= SUBPARAM_MESSAGE_IDENTIFIER &&
+                        subparamId <= SUBPARAM_ID_LAST_DEFINED)) {
+                    throw new CodingException("illegal duplicate subparameter (" +
+                                              subparamId + ")");
+                }
+                boolean decodeSuccess;
+                switch (subparamId) {
+                case SUBPARAM_MESSAGE_IDENTIFIER:
+                    decodeSuccess = decodeMessageId(bData, inStream);
+                    break;
+                case SUBPARAM_USER_DATA:
+                    decodeSuccess = decodeUserData(bData, inStream);
+                    break;
+                case SUBPARAM_LANGUAGE_INDICATOR:
+                    decodeSuccess = decodeLanguageIndicator(bData, inStream);
+                    break;
+                case SUBPARAM_PRIORITY_INDICATOR:
+                    decodeSuccess = decodePriorityIndicator(bData, inStream);
+                    break;
+                default:
+                    decodeSuccess = decodeReserved(inStream, subparamId);
+                }
+                if (decodeSuccess &&
+                        (subparamId >= SUBPARAM_MESSAGE_IDENTIFIER &&
+                        subparamId <= SUBPARAM_ID_LAST_DEFINED)) {
+                    foundSubparamMask |= subparamIdBit;
+                }
+            }
+            if ((foundSubparamMask & (1 << SUBPARAM_MESSAGE_IDENTIFIER)) == 0) {
+                throw new CodingException("missing MESSAGE_IDENTIFIER subparam");
+            }
+            if (bData.userData != null) {
+                if (isCmasAlertCategory(serviceCategory)) {
+                    decodeCmasUserData(bData, serviceCategory);
+                } else {
+                    decodeUserDataPayload(bData.userData, bData.hasUserDataHeader);
+                }
+            }
+            return bData;
+        } catch (BitwiseInputStream.AccessException ex) {
+            Log.e(LOG_TAG, "BearerData decode failed: " + ex);
+        } catch (CodingException ex) {
+            Log.e(LOG_TAG, "BearerData decode failed: " + ex);
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/cellbroadcastservice/CbGeoUtils.java b/src/com/android/cellbroadcastservice/CbGeoUtils.java
new file mode 100644
index 0000000..292f0f6
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/CbGeoUtils.java
@@ -0,0 +1,316 @@
+/*
+ * 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.annotation.NonNull;
+import android.telephony.Rlog;
+import android.text.TextUtils;
+
+import com.android.internal.telephony.CbGeoUtils.Geometry;
+import com.android.internal.telephony.CbGeoUtils.LatLng;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * This utils class is specifically used for geo-targeting of CellBroadcast messages.
+ * The coordinates used by this utils class are latitude and longitude, but some algorithms in this
+ * class only use them as coordinates on plane, so the calculation will be inaccurate. So don't use
+ * this class for anything other then geo-targeting of cellbroadcast messages.
+ */
+public class CbGeoUtils {
+    /**
+     * Tolerance for determining if the value is 0. If the absolute value of a value is less than
+     * this tolerance, it will be treated as 0.
+     */
+    public static final double EPS = 1e-7;
+
+    private static final String TAG = "CbGeoUtils";
+
+    /** The TLV tags of WAC, defined in ATIS-0700041 5.2.3 WAC tag coding. */
+    public static final int GEO_FENCING_MAXIMUM_WAIT_TIME = 0x01;
+    public static final int GEOMETRY_TYPE_POLYGON = 0x02;
+    public static final int GEOMETRY_TYPE_CIRCLE = 0x03;
+
+    /** The identifier of geometry in the encoded string. */
+    private static final String CIRCLE_SYMBOL = "circle";
+    private static final String POLYGON_SYMBOL = "polygon";
+
+    /**
+     * The class represents a simple polygon with at least 3 points.
+     */
+    public static class Polygon implements com.android.internal.telephony.CbGeoUtils.Geometry {
+        /**
+         * In order to reduce the loss of precision in floating point calculations, all vertices
+         * of the polygon are scaled. Set the value of scale to 1000 can take into account the
+         * actual distance accuracy of 1 meter if the EPS is 1e-7 during the calculation.
+         */
+        private static final double SCALE = 1000.0;
+
+        private final List<LatLng> mVertices;
+        private final List<Point> mScaledVertices;
+        private final LatLng mOrigin;
+
+        /**
+         * Constructs a simple polygon from the given vertices. The adjacent two vertices are
+         * connected to form an edge of the polygon. The polygon has at least 3 vertices, and the
+         * last vertices and the first vertices must be adjacent.
+         *
+         * The longitude difference in the vertices should be less than 180 degree.
+         */
+        public Polygon(@NonNull List<LatLng> vertices) {
+            mVertices = vertices;
+
+            // Find the point with smallest longitude as the mOrigin point.
+            int idx = 0;
+            for (int i = 1; i < vertices.size(); i++) {
+                if (vertices.get(i).lng < vertices.get(idx).lng) {
+                    idx = i;
+                }
+            }
+            mOrigin = vertices.get(idx);
+
+            mScaledVertices = vertices.stream()
+                    .map(latLng -> convertAndScaleLatLng(latLng))
+                    .collect(Collectors.toList());
+        }
+
+        public List<LatLng> getVertices() {
+            return mVertices;
+        }
+
+        /**
+         * Check if the given point {@code p} is inside the polygon. This method counts the number
+         * of times the polygon winds around the point P, A.K.A "winding number". The point is
+         * outside only when this "winding number" is 0.
+         *
+         * If a point is on the edge of the polygon, it is also considered to be inside the polygon.
+         */
+        @Override
+        public boolean contains(LatLng latLng) {
+            Point p = convertAndScaleLatLng(latLng);
+
+            int n = mScaledVertices.size();
+            int windingNumber = 0;
+            for (int i = 0; i < n; i++) {
+                Point a = mScaledVertices.get(i);
+                Point b = mScaledVertices.get((i + 1) % n);
+
+                // CCW is counterclockwise
+                // CCW = ab x ap
+                // CCW > 0 -> ap is on the left side of ab
+                // CCW == 0 -> ap is on the same line of ab
+                // CCW < 0 -> ap is on the right side of ab
+                int ccw = sign(crossProduct(b.subtract(a), p.subtract(a)));
+
+                if (ccw == 0) {
+                    if (Math.min(a.x, b.x) <= p.x && p.x <= Math.max(a.x, b.x)
+                            && Math.min(a.y, b.y) <= p.y && p.y <= Math.max(a.y, b.y)) {
+                        return true;
+                    }
+                } else {
+                    if (sign(a.y - p.y) <= 0) {
+                        // upward crossing
+                        if (ccw > 0 && sign(b.y - p.y) > 0) {
+                            ++windingNumber;
+                        }
+                    } else {
+                        // downward crossing
+                        if (ccw < 0 && sign(b.y - p.y) <= 0) {
+                            --windingNumber;
+                        }
+                    }
+                }
+            }
+            return windingNumber != 0;
+        }
+
+        /**
+         * Move the given point {@code latLng} to the coordinate system with {@code mOrigin} as the
+         * origin and scale it. {@code mOrigin} is selected from the vertices of a polygon, it has
+         * the smallest longitude value among all of the polygon vertices.
+         *
+         * @param latLng the point need to be converted and scaled.
+         * @Return a {@link Point} object.
+         */
+        private Point convertAndScaleLatLng(LatLng latLng) {
+            double x = latLng.lat - mOrigin.lat;
+            double y = latLng.lng - mOrigin.lng;
+
+            // If the point is in different hemispheres(western/eastern) than the mOrigin, and the
+            // edge between them cross the 180th meridian, then its relative coordinates will be
+            // extended.
+            // For example, suppose the longitude of the mOrigin is -178, and the longitude of the
+            // point to be converted is 175, then the longitude after the conversion is -8.
+            // calculation: (-178 - 8) - (-178).
+            if (sign(mOrigin.lng) != 0 && sign(mOrigin.lng) != sign(latLng.lng)) {
+                double distCross0thMeridian = Math.abs(mOrigin.lng) + Math.abs(latLng.lng);
+                if (sign(distCross0thMeridian * 2 - 360) > 0) {
+                    y = sign(mOrigin.lng) * (360 - distCross0thMeridian);
+                }
+            }
+            return new Point(x * SCALE, y * SCALE);
+        }
+
+        private static double crossProduct(Point a, Point b) {
+            return a.x * b.y - a.y * b.x;
+        }
+
+        static final class Point {
+            public final double x;
+            public final double y;
+
+            Point(double x, double y) {
+                this.x = x;
+                this.y = y;
+            }
+
+            public Point subtract(Point p) {
+                return new Point(x - p.x, y - p.y);
+            }
+        }
+    }
+
+    /** The class represents a circle. */
+    public static class Circle implements Geometry {
+        private final LatLng mCenter;
+        private final double mRadiusMeter;
+
+        public Circle(LatLng center, double radiusMeter) {
+            this.mCenter = center;
+            this.mRadiusMeter = radiusMeter;
+        }
+
+        public LatLng getCenter() {
+            return mCenter;
+        }
+
+        public double getRadius() {
+            return mRadiusMeter;
+        }
+
+        @Override
+        public boolean contains(LatLng p) {
+            return mCenter.distance(p) <= mRadiusMeter;
+        }
+    }
+
+    /**
+     * Parse the geometries from the encoded string {@code str}. The string must follow the
+     * geometry encoding specified by {@link android.provider.Telephony.CellBroadcasts#GEOMETRIES}.
+     */
+    @NonNull
+    public static List<Geometry> parseGeometriesFromString(@NonNull String str) {
+        List<Geometry> geometries = new ArrayList<>();
+        for (String geometryStr : str.split("\\s*;\\s*")) {
+            String[] geoParameters = geometryStr.split("\\s*\\|\\s*");
+            switch (geoParameters[0]) {
+                case CIRCLE_SYMBOL:
+                    geometries.add(new Circle(parseLatLngFromString(geoParameters[1]),
+                            Double.parseDouble(geoParameters[2])));
+                    break;
+                case POLYGON_SYMBOL:
+                    List<LatLng> vertices = new ArrayList<>(geoParameters.length - 1);
+                    for (int i = 1; i < geoParameters.length; i++) {
+                        vertices.add(parseLatLngFromString(geoParameters[i]));
+                    }
+                    geometries.add(new Polygon(vertices));
+                    break;
+                default:
+                    Rlog.e(TAG, "Invalid geometry format " + geometryStr);
+            }
+        }
+        return geometries;
+    }
+
+    /**
+     * Encode a list of geometry objects to string. The encoding format is specified by
+     * {@link android.provider.Telephony.CellBroadcasts#GEOMETRIES}.
+     *
+     * @param geometries the list of geometry objects need to be encoded.
+     * @return the encoded string.
+     */
+    @NonNull
+    public static String encodeGeometriesToString(@NonNull List<Geometry> geometries) {
+        return geometries.stream()
+                .map(geometry -> encodeGeometryToString(geometry))
+                .filter(encodedStr -> !TextUtils.isEmpty(encodedStr))
+                .collect(Collectors.joining(";"));
+    }
+
+
+    /**
+     * Encode the geometry object to string. The encoding format is specified by
+     * {@link android.provider.Telephony.CellBroadcasts#GEOMETRIES}.
+     * @param geometry the geometry object need to be encoded.
+     * @return the encoded string.
+     */
+    @NonNull
+    private static String encodeGeometryToString(@NonNull Geometry geometry) {
+        StringBuilder sb = new StringBuilder();
+        if (geometry instanceof Polygon) {
+            sb.append(POLYGON_SYMBOL);
+            for (LatLng latLng : ((Polygon) geometry).getVertices()) {
+                sb.append("|");
+                sb.append(latLng.lat);
+                sb.append(",");
+                sb.append(latLng.lng);
+            }
+        } else if (geometry instanceof Circle) {
+            sb.append(CIRCLE_SYMBOL);
+            Circle circle = (Circle) geometry;
+
+            // Center
+            sb.append("|");
+            sb.append(circle.getCenter().lat);
+            sb.append(",");
+            sb.append(circle.getCenter().lng);
+
+            // Radius
+            sb.append("|");
+            sb.append(circle.getRadius());
+        } else {
+            Rlog.e(TAG, "Unsupported geometry object " + geometry);
+            return null;
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Parse {@link LatLng} from {@link String}. Latitude and longitude are separated by ",".
+     * Example: "13.56,-55.447".
+     *
+     * @param str encoded lat/lng string.
+     * @Return {@link LatLng} object.
+     */
+    @NonNull
+    public static LatLng parseLatLngFromString(@NonNull String str) {
+        String[] latLng = str.split("\\s*,\\s*");
+        return new LatLng(Double.parseDouble(latLng[0]), Double.parseDouble(latLng[1]));
+    }
+
+    /**
+     * @Return the sign of the given value {@code value} with the specified tolerance. Return 1
+     * means the sign is positive, -1 means negative, 0 means the value will be treated as 0.
+     */
+    public static int sign(double value) {
+        if (value > EPS) return 1;
+        if (value < -EPS) return -1;
+        return 0;
+    }
+}
diff --git a/src/com/android/cellbroadcastservice/CellBroadcastHandler.java b/src/com/android/cellbroadcastservice/CellBroadcastHandler.java
new file mode 100644
index 0000000..b4f0eb9
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/CellBroadcastHandler.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cellbroadcastservice;
+
+import static android.content.PermissionChecker.PERMISSION_GRANTED;
+import static android.provider.Settings.Secure.CMAS_ADDITIONAL_BROADCAST_PKG;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.AppOpsManager;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.PermissionChecker;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.provider.Telephony;
+import android.provider.Telephony.CellBroadcasts;
+import android.telephony.SmsCbMessage;
+import android.telephony.SubscriptionManager;
+import android.text.format.DateUtils;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.internal.telephony.CbGeoUtils.Geometry;
+import com.android.internal.telephony.CbGeoUtils.LatLng;
+import com.android.internal.telephony.metrics.TelephonyMetrics;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Dispatch new Cell Broadcasts to receivers. Acquires a private wakelock until the broadcast
+ * completes and our result receiver is called.
+ */
+public class CellBroadcastHandler extends WakeLockStateMachine {
+    private static final String EXTRA_MESSAGE = "message";
+
+    private final LocalLog mLocalLog = new LocalLog(100);
+
+    protected static final Uri CELL_BROADCAST_URI = Uri.parse("content://cellbroadcasts_fwk");
+
+    /** Uses to request the location update. */
+    public final LocationRequester mLocationRequester;
+
+    private CellBroadcastHandler(Context context) {
+        this("CellBroadcastHandler", context);
+    }
+
+    protected CellBroadcastHandler(String debugTag, Context context) {
+        super(debugTag, context);
+        mLocationRequester = new LocationRequester(
+                context,
+                (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE),
+                getHandler().getLooper());
+    }
+
+    /**
+     * Create a new CellBroadcastHandler.
+     * @param context the context to use for dispatching Intents
+     * @return the new handler
+     */
+    public static CellBroadcastHandler makeCellBroadcastHandler(Context context) {
+        CellBroadcastHandler handler = new CellBroadcastHandler(context);
+        handler.start();
+        return handler;
+    }
+
+    /**
+     * Handle Cell Broadcast messages from {@code CdmaInboundSmsHandler}.
+     * 3GPP-format Cell Broadcast messages sent from radio are handled in the subclass.
+     *
+     * @param message the message to process
+     * @return true if need to wait for geo-fencing or an ordered broadcast was sent.
+     */
+    @Override
+    protected boolean handleSmsMessage(Message message) {
+        if (message.obj instanceof SmsCbMessage) {
+            handleBroadcastSms((SmsCbMessage) message.obj);
+            return true;
+        } else {
+            loge("handleMessage got object of type: " + message.obj.getClass().getName());
+            return false;
+        }
+    }
+
+    /**
+     * Dispatch a Cell Broadcast message to listeners.
+     * @param message the Cell Broadcast to broadcast
+     */
+    protected void handleBroadcastSms(SmsCbMessage message) {
+        int slotIndex = message.getSlotIndex();
+        // Log Cellbroadcast msg received event
+        TelephonyMetrics metrics = TelephonyMetrics.getInstance();
+        metrics.writeNewCBSms(slotIndex, message.getMessageFormat(),
+                message.getMessagePriority(), message.isCmasMessage(), message.isEtwsMessage(),
+                message.getServiceCategory(), message.getSerialNumber(),
+                System.currentTimeMillis());
+
+        // TODO: Database inserting can be time consuming, therefore this should be changed to
+        // asynchronous.
+        ContentValues cv = message.getContentValues();
+        Uri uri = mContext.getContentResolver().insert(CELL_BROADCAST_URI, cv);
+
+        if (message.needGeoFencingCheck()) {
+            if (DBG) {
+                log("Request location update for geo-fencing. serialNumber = "
+                        + message.getSerialNumber());
+            }
+
+            requestLocationUpdate(location -> {
+                if (location == null) {
+                    // Broadcast the message directly if the location is not available.
+                    broadcastMessage(message, uri, slotIndex);
+                } else {
+                    performGeoFencing(message, uri, message.getGeometries(), location, slotIndex);
+                }
+            }, message.getMaximumWaitingTime());
+        } else {
+            if (DBG) {
+                log("Broadcast the message directly because no geo-fencing required, "
+                        + "serialNumber = " + message.getSerialNumber()
+                        + " needGeoFencing = " + message.needGeoFencingCheck());
+            }
+            broadcastMessage(message, uri, slotIndex);
+        }
+    }
+
+    /**
+     * Perform a geo-fencing check for {@code message}. Broadcast the {@code message} if the
+     * {@code location} is inside the {@code broadcastArea}.
+     * @param message the message need to geo-fencing check
+     * @param uri the message's uri
+     * @param broadcastArea the broadcast area of the message
+     * @param location current location
+     */
+    protected void performGeoFencing(SmsCbMessage message, Uri uri, List<Geometry> broadcastArea,
+            LatLng location, int slotIndex) {
+
+        if (DBG) {
+            logd("Perform geo-fencing check for message identifier = "
+                    + message.getServiceCategory()
+                    + " serialNumber = " + message.getSerialNumber());
+        }
+
+        for (Geometry geo : broadcastArea) {
+            if (geo.contains(location)) {
+                broadcastMessage(message, uri, slotIndex);
+                return;
+            }
+        }
+
+        if (DBG) {
+            logd("Device location is outside the broadcast area "
+                    + CbGeoUtils.encodeGeometriesToString(broadcastArea));
+        }
+    }
+
+    /**
+     * Request a single location update.
+     * @param callback a callback will be called when the location is available.
+     * @param maximumWaitTimeSec the maximum wait time of this request. If location is not updated
+     * within the maximum wait time, {@code callback#onLocationUpadte(null)} will be called.
+     */
+    protected void requestLocationUpdate(LocationUpdateCallback callback, int maximumWaitTimeSec) {
+        mLocationRequester.requestLocationUpdate(callback, maximumWaitTimeSec);
+    }
+
+    /**
+     * Broadcast a list of cell broadcast messages.
+     * @param cbMessages a list of cell broadcast message.
+     * @param cbMessageUris the corresponding {@link Uri} of the cell broadcast messages.
+     */
+    protected void broadcastMessage(List<SmsCbMessage> cbMessages, List<Uri> cbMessageUris,
+            int slotIndex) {
+        for (int i = 0; i < cbMessages.size(); i++) {
+            broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex);
+        }
+    }
+
+    /**
+     * Broadcast the {@code message} to the applications.
+     * @param message a message need to broadcast
+     * @param messageUri message's uri
+     */
+    protected void broadcastMessage(@NonNull SmsCbMessage message, @Nullable Uri messageUri,
+            int slotIndex) {
+        String receiverPermission;
+        int appOp;
+        String msg;
+        Intent intent;
+        if (message.isEmergencyMessage()) {
+            msg = "Dispatching emergency SMS CB, SmsCbMessage is: " + message;
+            log(msg);
+            mLocalLog.log(msg);
+            intent = new Intent(Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION);
+            //Emergency alerts need to be delivered with high priority
+            intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+            receiverPermission = Manifest.permission.RECEIVE_EMERGENCY_BROADCAST;
+            appOp = AppOpsManager.OP_RECEIVE_EMERGECY_SMS;
+
+            intent.putExtra(EXTRA_MESSAGE, message);
+            SubscriptionManager.putPhoneIdAndSubIdExtra(intent, slotIndex);
+
+            if (Build.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) {
+                    Intent additionalIntent = new Intent(intent);
+                    additionalIntent.setPackage(additionalPackage);
+                    mContext.sendOrderedBroadcastAsUser(additionalIntent, UserHandle.ALL,
+                            receiverPermission, appOp, null, 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.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, receiverPermission,
+                        appOp, mReceiver, getHandler(), Activity.RESULT_OK, null, null);
+            }
+        } else {
+            msg = "Dispatching SMS CB, SmsCbMessage is: " + message;
+            log(msg);
+            mLocalLog.log(msg);
+            intent = new Intent(Telephony.Sms.Intents.SMS_CB_RECEIVED_ACTION);
+            // Send implicit intent since there are various 3rd party carrier apps listen to
+            // this intent.
+            intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+            receiverPermission = Manifest.permission.RECEIVE_SMS;
+            appOp = AppOpsManager.OP_RECEIVE_SMS;
+
+            intent.putExtra(EXTRA_MESSAGE, message);
+            SubscriptionManager.putPhoneIdAndSubIdExtra(intent, slotIndex);
+
+            mReceiverCount.incrementAndGet();
+            mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, receiverPermission, appOp,
+                    mReceiver, getHandler(), Activity.RESULT_OK, null, null);
+        }
+
+        if (messageUri != null) {
+            ContentValues cv = new ContentValues();
+            cv.put(CellBroadcasts.MESSAGE_BROADCASTED, 1);
+            mContext.getContentResolver().update(CELL_BROADCAST_URI, cv,
+                    CellBroadcasts._ID + "=?", new String[] {messageUri.getLastPathSegment()});
+        }
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("CellBroadcastHandler:");
+        mLocalLog.dump(fd, pw, args);
+        pw.flush();
+    }
+
+    /** The callback interface of a location request. */
+    public interface LocationUpdateCallback {
+        /**
+         * Call when the location update is available.
+         * @param location a location in (latitude, longitude) format, or {@code null} if the
+         * location service is not available.
+         */
+        void onLocationUpdate(@Nullable LatLng location);
+    }
+
+    private static final class LocationRequester {
+        private static final String TAG = LocationRequester.class.getSimpleName();
+
+        /**
+         * 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.
+         */
+        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;
+
+        /**
+         * Request location update from network or gps location provider. Network provider will be
+         * used if available, otherwise use the gps provider.
+         */
+        private static final List<String> LOCATION_PROVIDERS = Arrays.asList(
+                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;
+
+        LocationRequester(Context context, LocationManager locationManager, Looper looper) {
+            mLocationManager = locationManager;
+            mLooper = looper;
+            mCallbacks = new ArrayList<>();
+            mContext = context;
+            mLocationHandler = new LocationHandler(looper);
+        }
+
+        /**
+         * Request a single location update. If the location is not available, a callback with
+         * {@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
+         * 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();
+        }
+
+        private void onLocationUpdate(@Nullable LatLng location) {
+            for (LocationUpdateCallback callback : mCallbacks) {
+                callback.onLocationUpdate(location);
+            }
+            mCallbacks.clear();
+        }
+
+        private void requestLocationUpdateInternal(@NonNull LocationUpdateCallback callback,
+                int maximumWaitTimeSec) {
+            if (DBG) Log.d(TAG, "requestLocationUpdate");
+            if (!isLocationServiceAvailable()) {
+                if (DBG) {
+                    Log.d(TAG, "Can't request location update because of no location permission");
+                }
+                callback.onLocationUpdate(null);
+                return;
+            }
+
+            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;
+                }
+            }
+        }
+
+        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;
+            }
+            return false;
+        }
+
+        private boolean hasPermission(String permission) {
+            return PermissionChecker.checkCallingOrSelfPermissionForDataDelivery(mContext,
+                    permission, null) == 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/DefaultCellBroadcastService.java b/src/com/android/cellbroadcastservice/DefaultCellBroadcastService.java
new file mode 100644
index 0000000..3b60e51
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/DefaultCellBroadcastService.java
@@ -0,0 +1,110 @@
+/*
+ * 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.content.Context;
+import android.telephony.CellBroadcastService;
+import android.telephony.SmsCbLocation;
+import android.telephony.SmsCbMessage;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+/**
+ * The default implementation of CellBroadcastService, which is used for handling GSM and CDMA
+ * cell broadcast messages.
+ */
+public class DefaultCellBroadcastService extends CellBroadcastService {
+    private GsmCellBroadcastHandler mGsmCellBroadcastHandler;
+    private CellBroadcastHandler mCdmaCellBroadcastHandler;
+
+    private static final String TAG = "DefaultCellBroadcastService";
+
+    private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7',
+            '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mGsmCellBroadcastHandler =
+                GsmCellBroadcastHandler.makeGsmCellBroadcastHandler(getApplicationContext());
+        mCdmaCellBroadcastHandler =
+                CellBroadcastHandler.makeCellBroadcastHandler(getApplicationContext());
+    }
+
+    @Override
+    public void onGsmCellBroadcastSms(int slotIndex, byte[] message) {
+        Log.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);
+        int[] subIds =
+                ((SubscriptionManager) getSystemService(
+                        Context.TELEPHONY_SUBSCRIPTION_SERVICE)).getSubscriptionIds(slotIndex);
+        String plmn;
+        if (subIds != null && subIds.length > 0) {
+            int subId = subIds[0];
+            plmn = ((TelephonyManager) getSystemService(
+                            Context.TELEPHONY_SERVICE)).createForSubscriptionId(
+                            subId).getNetworkOperator();
+        } else {
+            plmn = "";
+        }
+        SmsCbMessage message = parseBroadcastSms(slotIndex, plmn, bearerData, serviceCategory);
+        if (message != null) {
+            mCdmaCellBroadcastHandler.onCdmaCellBroadcastSms(message);
+        }
+    }
+
+    /**
+     * Parses a CDMA broadcast SMS
+     *
+     * @param slotIndex the slotIndex the SMS was received on
+     * @param plmn the PLMN for a broadcast SMS or "" if unknown
+     * @param bearerData the bearerData of the SMS
+     * @param serviceCategory the service category of the broadcast
+     */
+    private SmsCbMessage parseBroadcastSms(int slotIndex, String plmn, byte[] bearerData,
+            int serviceCategory) {
+        BearerData bData = BearerData.decode(bearerData, serviceCategory);
+        if (bData == null) {
+            Log.w(TAG, "BearerData.decode() returned null");
+            return null;
+        }
+        Log.d(TAG, "MT raw BearerData = " + toHexString(bearerData, 0, bearerData.length));
+        SmsCbLocation location = new SmsCbLocation(plmn);
+
+        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);
+    }
+
+    private static String toHexString(byte[] array, int offset, int length) {
+        char[] buf = new char[length * 2];
+        int bufIndex = 0;
+        for (int i = offset; i < offset + length; i++) {
+            byte b = array[i];
+            buf[bufIndex++] = HEX_DIGITS[(b >>> 4) & 0x0F];
+            buf[bufIndex++] = HEX_DIGITS[b & 0x0F];
+        }
+        return new String(buf);
+    }
+}
diff --git a/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java b/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java
new file mode 100644
index 0000000..abcbc20
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cellbroadcastservice;
+
+import static com.android.internal.telephony.gsm.SmsCbConstants.MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Message;
+import android.provider.Telephony.CellBroadcasts;
+import android.telephony.CellInfo;
+import android.telephony.CellLocation;
+import android.telephony.SmsCbLocation;
+import android.telephony.SmsCbMessage;
+import android.telephony.TelephonyManager;
+import android.telephony.gsm.GsmCellLocation;
+import android.text.format.DateUtils;
+
+import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage;
+import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity;
+import com.android.internal.telephony.CbGeoUtils.Geometry;
+
+import dalvik.annotation.compat.UnsupportedAppUsage;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Handler for 3GPP format Cell Broadcasts. Parent class can also handle CDMA Cell Broadcasts.
+ */
+public class GsmCellBroadcastHandler extends CellBroadcastHandler {
+    private static final boolean VDBG = false;  // log CB PDU data
+
+    /** Indicates that a message is not being broadcasted. */
+    private static final String MESSAGE_NOT_BROADCASTED = "0";
+
+    /** This map holds incomplete concatenated messages waiting for assembly. */
+    @UnsupportedAppUsage
+    private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
+            new HashMap<>(4);
+
+    protected GsmCellBroadcastHandler(Context context) {
+        super("GsmCellBroadcastHandler", context);
+    }
+
+    @Override
+    protected void onQuitting() {
+        super.onQuitting();     // release wakelock
+    }
+
+    /**
+     * Handle a GSM cell broadcast message passed from the telephony framework.
+     * @param message
+     */
+    public void onGsmCellBroadcastSms(int slotIndex, byte[] message) {
+        sendMessage(EVENT_NEW_SMS_MESSAGE, slotIndex, -1, message);
+    }
+
+    /**
+     * Create a new CellBroadcastHandler.
+     * @param context the context to use for dispatching Intents
+     * @return the new handler
+     */
+    public static GsmCellBroadcastHandler makeGsmCellBroadcastHandler(Context context) {
+        GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context);
+        handler.start();
+        return handler;
+    }
+
+    /**
+     * Find the cell broadcast messages specify by the geo-fencing trigger message and perform a
+     * geo-fencing check for these messages.
+     * @param geoFencingTriggerMessage the trigger message
+     *
+     * @return {@code True} if geo-fencing is need for some cell broadcast message.
+     */
+    private boolean handleGeoFencingTriggerMessage(
+            GeoFencingTriggerMessage geoFencingTriggerMessage, int slotIndex) {
+        final List<SmsCbMessage> cbMessages = new ArrayList<>();
+        final List<Uri> cbMessageUris = new ArrayList<>();
+
+        // Only consider the cell broadcast received within 24 hours.
+        long lastReceivedTime = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS;
+
+        // Find the cell broadcast message identify by the message identifier and serial number
+        // and is not broadcasted.
+        String where = CellBroadcasts.SERVICE_CATEGORY + "=? AND "
+                + CellBroadcasts.SERIAL_NUMBER + "=? AND "
+                + CellBroadcasts.MESSAGE_BROADCASTED + "=? AND "
+                + CellBroadcasts.RECEIVED_TIME + ">?";
+
+        ContentResolver resolver = mContext.getContentResolver();
+        for (CellBroadcastIdentity identity : geoFencingTriggerMessage.cbIdentifiers) {
+            try (Cursor cursor = resolver.query(CELL_BROADCAST_URI,
+                    CellBroadcasts.QUERY_COLUMNS_FWK,
+                    where,
+                    new String[] { Integer.toString(identity.messageIdentifier),
+                            Integer.toString(identity.serialNumber), MESSAGE_NOT_BROADCASTED,
+                            Long.toString(lastReceivedTime) },
+                    null /* sortOrder */)) {
+                if (cursor != null) {
+                    while (cursor.moveToNext()) {
+                        cbMessages.add(SmsCbMessage.createFromCursor(cursor));
+                        cbMessageUris.add(ContentUris.withAppendedId(CELL_BROADCAST_URI,
+                                cursor.getInt(cursor.getColumnIndex(CellBroadcasts._ID))));
+                    }
+                }
+            }
+        }
+
+        List<Geometry> commonBroadcastArea = new ArrayList<>();
+        if (geoFencingTriggerMessage.shouldShareBroadcastArea()) {
+            for (SmsCbMessage msg : cbMessages) {
+                if (msg.getGeometries() != null) {
+                    commonBroadcastArea.addAll(msg.getGeometries());
+                }
+            }
+        }
+
+        // ATIS doesn't specify the geo fencing maximum wait time for the cell broadcasts specified
+        // in geo fencing trigger message. We will pick the largest maximum wait time among these
+        // cell broadcasts.
+        int maximumWaitTimeSec = 0;
+        for (SmsCbMessage msg : cbMessages) {
+            maximumWaitTimeSec = Math.max(maximumWaitTimeSec, msg.getMaximumWaitingTime());
+        }
+
+        if (DBG) {
+            logd("Geo-fencing trigger message = " + geoFencingTriggerMessage);
+            for (SmsCbMessage msg : cbMessages) {
+                logd(msg.toString());
+            }
+        }
+
+        if (cbMessages.isEmpty()) {
+            if (DBG) logd("No CellBroadcast message need to be broadcasted");
+            return false;
+        }
+
+        requestLocationUpdate(location -> {
+            if (location == null) {
+                // If the location is not available, broadcast the messages directly.
+                broadcastMessage(cbMessages, cbMessageUris, slotIndex);
+            } else {
+                for (int i = 0; i < cbMessages.size(); i++) {
+                    List<Geometry> broadcastArea = !commonBroadcastArea.isEmpty()
+                            ? commonBroadcastArea : cbMessages.get(i).getGeometries();
+                    if (broadcastArea == null || broadcastArea.isEmpty()) {
+                        broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex);
+                    } else {
+                        performGeoFencing(cbMessages.get(i), cbMessageUris.get(i), broadcastArea,
+                                location, slotIndex);
+                    }
+                }
+            }
+        }, maximumWaitTimeSec);
+        return true;
+    }
+
+    /**
+     * Handle 3GPP-format Cell Broadcast messages sent from radio.
+     *
+     * @param message the message to process
+     * @return true if need to wait for geo-fencing or an ordered broadcast was sent.
+     */
+    @Override
+    protected boolean handleSmsMessage(Message message) {
+        // For GSM, message.obj should be a byte[]
+        int slotIndex = message.arg1;
+        if (message.obj instanceof byte[]) {
+            byte[] pdu = (byte[]) message.obj;
+            SmsCbHeader header = createSmsCbHeader(pdu);
+            if (header == null) return false;
+
+            if (header.getServiceCategory() == MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER) {
+                GeoFencingTriggerMessage triggerMessage =
+                        GsmSmsCbMessage.createGeoFencingTriggerMessage(pdu);
+                if (triggerMessage != null) {
+                    return handleGeoFencingTriggerMessage(triggerMessage, slotIndex);
+                }
+            } else {
+                SmsCbMessage cbMessage = handleGsmBroadcastSms(header, pdu, slotIndex);
+                if (cbMessage != null) {
+                    handleBroadcastSms(cbMessage);
+                    return true;
+                }
+                if (VDBG) log("Not handled GSM broadcasts.");
+            }
+        }
+        return super.handleSmsMessage(message);
+    }
+
+    // return the cell location from the first returned cell info, prioritizing GSM
+    private CellLocation getCellLocation() {
+        TelephonyManager tm =
+                (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+        List<CellInfo> infos = tm.getAllCellInfo();
+        for (CellInfo info : infos) {
+            CellLocation cl = info.getCellIdentity().asCellLocation();
+            if (cl instanceof GsmCellLocation) {
+                return cl;
+            }
+        }
+        // If no GSM, return first in list
+        if (infos != null && !infos.isEmpty() && infos.get(0) != null) {
+            return infos.get(0).getCellIdentity().asCellLocation();
+        }
+        return CellLocation.getEmpty();
+    }
+
+
+    /**
+     * Handle 3GPP format SMS-CB message.
+     * @param header the cellbroadcast header.
+     * @param receivedPdu the received PDUs as a byte[]
+     */
+    private SmsCbMessage handleGsmBroadcastSms(SmsCbHeader header, byte[] receivedPdu,
+            int slotIndex) {
+        try {
+            if (VDBG) {
+                int pduLength = receivedPdu.length;
+                for (int i = 0; i < pduLength; i += 8) {
+                    StringBuilder sb = new StringBuilder("SMS CB pdu data: ");
+                    for (int j = i; j < i + 8 && j < pduLength; j++) {
+                        int b = receivedPdu[j] & 0xff;
+                        if (b < 0x10) {
+                            sb.append('0');
+                        }
+                        sb.append(Integer.toHexString(b)).append(' ');
+                    }
+                    log(sb.toString());
+                }
+            }
+
+            if (VDBG) log("header=" + header);
+            TelephonyManager tm =
+                    (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+            // TODO make a systemAPI for getNetworkOperatorForSlotIndex
+            String plmn = tm.getNetworkOperatorForPhone(slotIndex);
+            int lac = -1;
+            int cid = -1;
+            CellLocation cl = getCellLocation();
+            // Check if cell location is GsmCellLocation.  This is required to support
+            // dual-mode devices such as CDMA/LTE devices that require support for
+            // both 3GPP and 3GPP2 format messages
+            if (cl instanceof GsmCellLocation) {
+                GsmCellLocation cellLocation = (GsmCellLocation) cl;
+                lac = cellLocation.getLac();
+                cid = cellLocation.getCid();
+            }
+
+            SmsCbLocation location;
+            switch (header.getGeographicalScope()) {
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_LOCATION_AREA_WIDE:
+                    location = new SmsCbLocation(plmn, lac, -1);
+                    break;
+
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE:
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE:
+                    location = new SmsCbLocation(plmn, lac, cid);
+                    break;
+
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE:
+                default:
+                    location = new SmsCbLocation(plmn);
+                    break;
+            }
+
+            byte[][] pdus;
+            int pageCount = header.getNumberOfPages();
+            if (pageCount > 1) {
+                // Multi-page message
+                SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, location);
+
+                // Try to find other pages of the same message
+                pdus = mSmsCbPageMap.get(concatInfo);
+
+                if (pdus == null) {
+                    // This is the first page of this message, make room for all
+                    // pages and keep until complete
+                    pdus = new byte[pageCount][];
+
+                    mSmsCbPageMap.put(concatInfo, pdus);
+                }
+
+                if (VDBG) log("pdus size=" + pdus.length);
+                // Page parameter is one-based
+                pdus[header.getPageIndex() - 1] = receivedPdu;
+
+                for (byte[] pdu : pdus) {
+                    if (pdu == null) {
+                        // Still missing pages, exit
+                        log("still missing pdu");
+                        return null;
+                    }
+                }
+
+                // Message complete, remove and dispatch
+                mSmsCbPageMap.remove(concatInfo);
+            } else {
+                // Single page message
+                pdus = new byte[1][];
+                pdus[0] = receivedPdu;
+            }
+
+            // Remove messages that are out of scope to prevent the map from
+            // growing indefinitely, containing incomplete messages that were
+            // never assembled
+            Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator();
+
+            while (iter.hasNext()) {
+                SmsCbConcatInfo info = iter.next();
+
+                if (!info.matchesLocation(plmn, lac, cid)) {
+                    iter.remove();
+                }
+            }
+
+            return GsmSmsCbMessage.createSmsCbMessage(mContext, header, location, pdus, slotIndex);
+
+        } catch (RuntimeException e) {
+            loge("Error in decoding SMS CB pdu", e);
+            return null;
+        }
+    }
+
+    private SmsCbHeader createSmsCbHeader(byte[] bytes) {
+        try {
+            return new SmsCbHeader(bytes);
+        } catch (Exception ex) {
+            loge("Can't create SmsCbHeader, ex = " + ex.toString());
+            return null;
+        }
+    }
+
+    /**
+     * Holds all info about a message page needed to assemble a complete concatenated message.
+     */
+    private static final class SmsCbConcatInfo {
+
+        private final SmsCbHeader mHeader;
+        private final SmsCbLocation mLocation;
+
+        @UnsupportedAppUsage
+        SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) {
+            mHeader = header;
+            mLocation = location;
+        }
+
+        @Override
+        public int hashCode() {
+            return (mHeader.getSerialNumber() * 31) + mLocation.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof SmsCbConcatInfo) {
+                SmsCbConcatInfo other = (SmsCbConcatInfo) obj;
+
+                // Two pages match if they have the same serial number (which includes the
+                // geographical scope and update number), and both pages belong to the same
+                // location (PLMN, plus LAC and CID if these are part of the geographical scope).
+                return mHeader.getSerialNumber() == other.mHeader.getSerialNumber()
+                        && mLocation.equals(other.mLocation);
+            }
+
+            return false;
+        }
+
+        /**
+         * Compare the location code for this message to the current location code. The match is
+         * relative to the geographical scope of the message, which determines whether the LAC
+         * and Cell ID are saved in mLocation or set to -1 to match all values.
+         *
+         * @param plmn the current PLMN
+         * @param lac the current Location Area (GSM) or Service Area (UMTS)
+         * @param cid the current Cell ID
+         * @return true if this message is valid for the current location; false otherwise
+         */
+        @UnsupportedAppUsage
+        public boolean matchesLocation(String plmn, int lac, int cid) {
+            return mLocation.isInLocationArea(plmn, lac, cid);
+        }
+    }
+}
diff --git a/src/com/android/cellbroadcastservice/GsmSmsCbMessage.java b/src/com/android/cellbroadcastservice/GsmSmsCbMessage.java
new file mode 100644
index 0000000..cd2d964
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/GsmSmsCbMessage.java
@@ -0,0 +1,509 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cellbroadcastservice;
+
+import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE;
+import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI;
+import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY;
+import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE;
+import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.Resources;
+import android.telephony.SmsCbLocation;
+import android.telephony.SmsCbMessage;
+import android.util.Pair;
+import android.util.Slog;
+
+import com.android.cellbroadcastservice.CbGeoUtils.Circle;
+import com.android.cellbroadcastservice.CbGeoUtils.Polygon;
+import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity;
+import com.android.cellbroadcastservice.SmsCbHeader.DataCodingScheme;
+import com.android.internal.R;
+import com.android.internal.telephony.CbGeoUtils.Geometry;
+import com.android.internal.telephony.CbGeoUtils.LatLng;
+import com.android.internal.telephony.GsmAlphabet;
+import com.android.internal.telephony.SmsConstants;
+
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Parses a GSM or UMTS format SMS-CB message into an {@link SmsCbMessage} object. The class is
+ * public because {@link #createSmsCbMessage(SmsCbLocation, byte[][])} is used by some test cases.
+ */
+public class GsmSmsCbMessage {
+    private static final String TAG = GsmSmsCbMessage.class.getSimpleName();
+
+    private static final char CARRIAGE_RETURN = 0x0d;
+
+    private static final int PDU_BODY_PAGE_LENGTH = 82;
+
+    /** Utility class with only static methods. */
+    private GsmSmsCbMessage() { }
+
+    /**
+     * Get built-in ETWS primary messages by category. ETWS primary message does not contain text,
+     * so we have to show the pre-built messages to the user.
+     *
+     * @param context Device context
+     * @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) {
+        final Resources r = context.getResources();
+        switch (category) {
+            case ETWS_WARNING_TYPE_EARTHQUAKE:
+                return r.getString(R.string.etws_primary_default_message_earthquake);
+            case ETWS_WARNING_TYPE_TSUNAMI:
+                return r.getString(R.string.etws_primary_default_message_tsunami);
+            case ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI:
+                return r.getString(R.string.etws_primary_default_message_earthquake_and_tsunami);
+            case ETWS_WARNING_TYPE_TEST_MESSAGE:
+                return r.getString(R.string.etws_primary_default_message_test);
+            case ETWS_WARNING_TYPE_OTHER_EMERGENCY:
+                return r.getString(R.string.etws_primary_default_message_others);
+            default:
+                return "";
+        }
+    }
+
+    /**
+     * Create a new SmsCbMessage object from a header object plus one or more received PDUs.
+     *
+     * @param pdus PDU bytes
+     */
+    public static SmsCbMessage createSmsCbMessage(Context context, SmsCbHeader header,
+            SmsCbLocation location, byte[][] pdus, int slotIndex)
+            throws IllegalArgumentException {
+        long receivedTimeMillis = System.currentTimeMillis();
+        if (header.isEtwsPrimaryNotification()) {
+            // ETSI TS 23.041 ETWS Primary Notification message
+            // ETWS primary message only contains 4 fields including serial number,
+            // message identifier, warning type, and warning security information.
+            // There is no field for the content/text so we get the text from the resources.
+            return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, header.getGeographicalScope(),
+                    header.getSerialNumber(), location, header.getServiceCategory(), null,
+                    getEtwsPrimaryMessage(context, header.getEtwsInfo().getWarningType()),
+                    SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, header.getEtwsInfo(),
+                    header.getCmasInfo(), 0, null /* geometries */, receivedTimeMillis, slotIndex);
+        } else if (header.isUmtsFormat()) {
+            // UMTS format has only 1 PDU
+            byte[] pdu = pdus[0];
+            Pair<String, String> cbData = parseUmtsBody(header, pdu);
+            String language = cbData.first;
+            String body = cbData.second;
+            int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY
+                    : SmsCbMessage.MESSAGE_PRIORITY_NORMAL;
+            int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH];
+            int wacDataOffset = SmsCbHeader.PDU_HEADER_LENGTH
+                    + 1 // number of pages
+                    + (PDU_BODY_PAGE_LENGTH + 1) * nrPages; // cb data
+
+            // Has Warning Area Coordinates information
+            List<Geometry> geometries = null;
+            int maximumWaitingTimeSec = 255;
+            if (pdu.length > wacDataOffset) {
+                try {
+                    Pair<Integer, List<Geometry>> wac = parseWarningAreaCoordinates(pdu,
+                            wacDataOffset);
+                    maximumWaitingTimeSec = wac.first;
+                    geometries = wac.second;
+                } 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());
+                }
+            }
+
+            return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
+                    header.getGeographicalScope(), header.getSerialNumber(), location,
+                    header.getServiceCategory(), language, body, priority,
+                    header.getEtwsInfo(), header.getCmasInfo(), maximumWaitingTimeSec, geometries,
+                    receivedTimeMillis, slotIndex);
+        } else {
+            String language = null;
+            StringBuilder sb = new StringBuilder();
+            for (byte[] pdu : pdus) {
+                Pair<String, String> p = parseGsmBody(header, pdu);
+                language = p.first;
+                sb.append(p.second);
+            }
+            int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY
+                    : SmsCbMessage.MESSAGE_PRIORITY_NORMAL;
+
+            return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
+                    header.getGeographicalScope(), header.getSerialNumber(), location,
+                    header.getServiceCategory(), language, sb.toString(), priority,
+                    header.getEtwsInfo(), header.getCmasInfo(), 0, null /* geometries */,
+                    receivedTimeMillis, slotIndex);
+        }
+    }
+
+    /**
+     * Parse WEA Handset Action Message(WHAM) a.k.a geo-fencing trigger message.
+     *
+     * WEA Handset Action Message(WHAM) is a cell broadcast service message broadcast by the network
+     * to direct devices to perform a geo-fencing check on selected alerts.
+     *
+     * WEA Handset Action Message(WHAM) requirements from ATIS-0700041 section 4
+     * 1. The Warning Message contents of a WHAM shall be in Cell Broadcast(CB) data format as
+     * defined in TS 23.041.
+     * 2. The Warning Message Contents of WHAM shall be limited to one CB page(max 20 referenced
+     * WEA messages).
+     * 3. The broadcast area for a WHAM shall be the union of the broadcast areas of the referenced
+     * WEA message.
+     * @param pdu cell broadcast pdu, including the header
+     * @return {@link GeoFencingTriggerMessage} instance
+     */
+    public static GeoFencingTriggerMessage createGeoFencingTriggerMessage(byte[] pdu) {
+        try {
+            // Header length + 1(number of page). ATIS-0700041 define the number of page of
+            // geo-fencing trigger message is 1.
+            int whamOffset = SmsCbHeader.PDU_HEADER_LENGTH + 1;
+
+            BitStreamReader bitReader = new BitStreamReader(pdu, whamOffset);
+            int type = bitReader.read(4);
+            int length = bitReader.read(7);
+            // Skip the remained 5 bits
+            bitReader.skip();
+
+            int messageIdentifierCount = (length - 2) * 8 / 32;
+            List<CellBroadcastIdentity> cbIdentifiers = new ArrayList<>();
+            for (int i = 0; i < messageIdentifierCount; i++) {
+                // Both messageIdentifier and serialNumber are 16 bits integers.
+                // ATIS-0700041 Section 5.1.6
+                int messageIdentifier = bitReader.read(16);
+                int serialNumber = bitReader.read(16);
+                cbIdentifiers.add(new CellBroadcastIdentity(messageIdentifier, serialNumber));
+            }
+            return new GeoFencingTriggerMessage(type, cbIdentifiers);
+        } catch (Exception ex) {
+            Slog.e(TAG, "create geo-fencing trigger failed, ex = " + ex.toString());
+            return null;
+        }
+    }
+
+    /**
+     * Parse the broadcast area and maximum wait time from the Warning Area Coordinates TLV.
+     *
+     * @param pdu Warning Area Coordinates TLV.
+     * @param wacOffset the offset of Warning Area Coordinates TLV.
+     * @return a pair with the first element is maximum wait time and the second is the broadcast
+     * area. The default value of the maximum wait time is 255 which means use the device default
+     * value.
+     */
+    private static Pair<Integer, List<Geometry>> parseWarningAreaCoordinates(
+            byte[] pdu, int wacOffset) {
+        // little-endian
+        int wacDataLength = (pdu[wacOffset + 1] << 8) | pdu[wacOffset];
+        int offset = wacOffset + 2;
+
+        if (offset + wacDataLength > pdu.length) {
+            throw new IllegalArgumentException("Invalid wac data, expected the length of pdu at"
+                    + "least " + offset + wacDataLength + ", actual is " + pdu.length);
+        }
+
+        BitStreamReader bitReader = new BitStreamReader(pdu, offset);
+
+        int maximumWaitTimeSec = SmsCbMessage.MAXIMUM_WAIT_TIME_NOT_SET;
+
+        List<Geometry> geo = new ArrayList<>();
+        int remainedBytes = wacDataLength;
+        while (remainedBytes > 0) {
+            int type = bitReader.read(4);
+            int length = bitReader.read(10);
+            remainedBytes -= length;
+            // Skip the 2 remained bits
+            bitReader.skip();
+
+            switch (type) {
+                case CbGeoUtils.GEO_FENCING_MAXIMUM_WAIT_TIME:
+                    maximumWaitTimeSec = bitReader.read(8);
+                    break;
+                case CbGeoUtils.GEOMETRY_TYPE_POLYGON:
+                    List<LatLng> latLngs = new ArrayList<>();
+                    // Each coordinate is represented by 44 bits integer.
+                    // ATIS-0700041 5.2.4 Coordinate coding
+                    int n = (length - 2) * 8 / 44;
+                    for (int i = 0; i < n; i++) {
+                        latLngs.add(getLatLng(bitReader));
+                    }
+                    // Skip the padding bits
+                    bitReader.skip();
+                    geo.add(new Polygon(latLngs));
+                    break;
+                case CbGeoUtils.GEOMETRY_TYPE_CIRCLE:
+                    LatLng center = getLatLng(bitReader);
+                    // radius = (wacRadius / 2^6). The unit of wacRadius is km, we use meter as the
+                    // distance unit during geo-fencing.
+                    // ATIS-0700041 5.2.5 radius coding
+                    double radius = (bitReader.read(20) * 1.0 / (1 << 6)) * 1000.0;
+                    geo.add(new Circle(center, radius));
+                    break;
+                default:
+                    throw new IllegalArgumentException("Unsupported geoType = " + type);
+            }
+        }
+        return new Pair(maximumWaitTimeSec, geo);
+    }
+
+    /**
+     * The coordinate is (latitude, longitude), represented by a 44 bits integer.
+     * The coding is defined in ATIS-0700041 5.2.4
+     * @param bitReader
+     * @return coordinate (latitude, longitude)
+     */
+    private static LatLng getLatLng(BitStreamReader bitReader) {
+        // wacLatitude = floor(((latitude + 90) / 180) * 2^22)
+        // wacLongitude = floor(((longitude + 180) / 360) * 2^22)
+        int wacLat = bitReader.read(22);
+        int wacLng = bitReader.read(22);
+
+        // latitude = wacLatitude * 180 / 2^22 - 90
+        // longitude = wacLongitude * 360 / 2^22 -180
+        return new LatLng((wacLat * 180.0 / (1 << 22)) - 90, (wacLng * 360.0 / (1 << 22) - 180));
+    }
+
+    /**
+     * Parse and unpack the UMTS body text according to the encoding in the data coding scheme.
+     *
+     * @param header the message header to use
+     * @param pdu the PDU to decode
+     * @return a pair of string containing the language and body of the message in order
+     */
+    private static Pair<String, String> parseUmtsBody(SmsCbHeader header, byte[] pdu) {
+        // Payload may contain multiple pages
+        int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH];
+        String language = header.getDataCodingSchemeStructedData().language;
+
+        if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1)
+                * nrPages) {
+            throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match "
+                    + nrPages + " pages");
+        }
+
+        StringBuilder sb = new StringBuilder();
+
+        for (int i = 0; i < nrPages; i++) {
+            // Each page is 82 bytes followed by a length octet indicating
+            // the number of useful octets within those 82
+            int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i;
+            int length = pdu[offset + PDU_BODY_PAGE_LENGTH];
+
+            if (length > PDU_BODY_PAGE_LENGTH) {
+                throw new IllegalArgumentException("Page length " + length
+                        + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH);
+            }
+
+            Pair<String, String> p = unpackBody(pdu, offset, length,
+                    header.getDataCodingSchemeStructedData());
+            language = p.first;
+            sb.append(p.second);
+        }
+        return new Pair(language, sb.toString());
+
+    }
+
+    /**
+     * Parse and unpack the GSM body text according to the encoding in the data coding scheme.
+     * @param header the message header to use
+     * @param pdu the PDU to decode
+     * @return a pair of string containing the language and body of the message in order
+     */
+    private static Pair<String, String> parseGsmBody(SmsCbHeader header, byte[] pdu) {
+        // Payload is one single page
+        int offset = SmsCbHeader.PDU_HEADER_LENGTH;
+        int length = pdu.length - offset;
+        return unpackBody(pdu, offset, length, header.getDataCodingSchemeStructedData());
+    }
+
+    /**
+     * Unpack body text from the pdu using the given encoding, position and length within the pdu.
+     *
+     * @param pdu The pdu
+     * @param offset Position of the first byte to unpack
+     * @param length Number of bytes to unpack
+     * @param dcs data coding scheme
+     * @return a Pair of Strings containing the language and body of the message
+     */
+    private static Pair<String, String> unpackBody(byte[] pdu, int offset, int length,
+            DataCodingScheme dcs) {
+        String body = null;
+
+        String language = dcs.language;
+        switch (dcs.encoding) {
+            case SmsConstants.ENCODING_7BIT:
+                body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7);
+
+                if (dcs.hasLanguageIndicator && body != null && body.length() > 2) {
+                    // Language is two GSM characters followed by a CR.
+                    // The actual body text is offset by 3 characters.
+                    language = body.substring(0, 2);
+                    body = body.substring(3);
+                }
+                break;
+
+            case SmsConstants.ENCODING_16BIT:
+                if (dcs.hasLanguageIndicator && pdu.length >= offset + 2) {
+                    // Language is two GSM characters.
+                    // The actual body text is offset by 2 bytes.
+                    language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2);
+                    offset += 2;
+                    length -= 2;
+                }
+
+                try {
+                    body = new String(pdu, offset, (length & 0xfffe), "utf-16");
+                } catch (UnsupportedEncodingException e) {
+                    // Apparently it wasn't valid UTF-16.
+                    throw new IllegalArgumentException("Error decoding UTF-16 message", e);
+                }
+                break;
+
+            default:
+                break;
+        }
+
+        if (body != null) {
+            // Remove trailing carriage return
+            for (int i = body.length() - 1; i >= 0; i--) {
+                if (body.charAt(i) != CARRIAGE_RETURN) {
+                    body = body.substring(0, i + 1);
+                    break;
+                }
+            }
+        } else {
+            body = "";
+        }
+
+        return new Pair<String, String>(language, body);
+    }
+
+    /** A class use to facilitate the processing of bits stream data. */
+    private static final class BitStreamReader {
+        /** The bits stream represent by a bytes array. */
+        private final byte[] mData;
+
+        /** The offset of the current byte. */
+        private int mCurrentOffset;
+
+        /**
+         * The remained bits of the current byte which have not been read. The most significant
+         * will be read first, so the remained bits are always the least significant bits.
+         */
+        private int mRemainedBit;
+
+        /**
+         * Constructor
+         * @param data bit stream data represent by byte array.
+         * @param offset the offset of the first byte.
+         */
+        BitStreamReader(byte[] data, int offset) {
+            mData = data;
+            mCurrentOffset = offset;
+            mRemainedBit = 8;
+        }
+
+        /**
+         * Read the first {@code count} bits.
+         * @param count the number of bits need to read
+         * @return {@code bits} represent by an 32-bits integer, therefore {@code count} must be no
+         * greater than 32.
+         */
+        public int read(int count) throws IndexOutOfBoundsException {
+            int val = 0;
+            while (count > 0) {
+                if (count >= mRemainedBit) {
+                    val <<= mRemainedBit;
+                    val |= mData[mCurrentOffset] & ((1 << mRemainedBit) - 1);
+                    count -= mRemainedBit;
+                    mRemainedBit = 8;
+                    ++mCurrentOffset;
+                } else {
+                    val <<= count;
+                    val |= (mData[mCurrentOffset] & ((1 << mRemainedBit) - 1))
+                            >> (mRemainedBit - count);
+                    mRemainedBit -= count;
+                    count = 0;
+                }
+            }
+            return val;
+        }
+
+        /**
+         * Skip the current bytes if the remained bits is less than 8. This is useful when
+         * processing the padding or reserved bits.
+         */
+        public void skip() {
+            if (mRemainedBit < 8) {
+                mRemainedBit = 8;
+                ++mCurrentOffset;
+            }
+        }
+    }
+
+    /**
+     * Part of a GSM SMS cell broadcast message which may trigger geo-fencing logic.
+     * @hide
+     */
+    public static final class GeoFencingTriggerMessage {
+        /**
+         * Indicate the list of active alerts share their warning area coordinates which means the
+         * broadcast area is the union of the broadcast areas of the active alerts in this list.
+         */
+        public static final int TYPE_ACTIVE_ALERT_SHARE_WAC = 2;
+
+        public final int type;
+        public final List<CellBroadcastIdentity> cbIdentifiers;
+
+        GeoFencingTriggerMessage(int type, @NonNull List<CellBroadcastIdentity> cbIdentifiers) {
+            this.type = type;
+            this.cbIdentifiers = cbIdentifiers;
+        }
+
+        /**
+         * Whether the trigger message indicates that the broadcast areas are shared between all
+         * active alerts.
+         * @return true if broadcast areas are to be shared
+         */
+        boolean shouldShareBroadcastArea() {
+            return type == TYPE_ACTIVE_ALERT_SHARE_WAC;
+        }
+
+        static final class CellBroadcastIdentity {
+            public final int messageIdentifier;
+            public final int serialNumber;
+            CellBroadcastIdentity(int messageIdentifier, int serialNumber) {
+                this.messageIdentifier = messageIdentifier;
+                this.serialNumber = serialNumber;
+            }
+        }
+
+        @Override
+        public String toString() {
+            String identifiers = cbIdentifiers.stream()
+                    .map(cbIdentifier ->String.format("(msgId = %d, serial = %d)",
+                            cbIdentifier.messageIdentifier, cbIdentifier.serialNumber))
+                    .collect(Collectors.joining(","));
+            return "triggerType=" + type + " identifiers=" + identifiers;
+        }
+    }
+}
diff --git a/src/com/android/cellbroadcastservice/IState.java b/src/com/android/cellbroadcastservice/IState.java
new file mode 100644
index 0000000..a29c040
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/IState.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cellbroadcastservice;
+
+import android.annotation.UnsupportedAppUsage;
+import android.os.Message;
+
+/**
+ * {@hide}
+ *
+ * The interface for implementing states in a {@link StateMachine}
+ */
+public interface IState {
+
+    /**
+     * Returned by processMessage to indicate the message was processed.
+     */
+    boolean HANDLED = true;
+
+    /**
+     * Returned by processMessage to indicate the message was NOT processed.
+     */
+    boolean NOT_HANDLED = false;
+
+    /**
+     * Called when a state is entered.
+     */
+    void enter();
+
+    /**
+     * Called when a state is exited.
+     */
+    void exit();
+
+    /**
+     * Called when a message is to be processed by the
+     * state machine.
+     *
+     * This routine is never reentered thus no synchronization
+     * is needed as only one processMessage method will ever be
+     * executing within a state machine at any given time. This
+     * does mean that processing by this routine must be completed
+     * as expeditiously as possible as no subsequent messages will
+     * be processed until this routine returns.
+     *
+     * @param msg to process
+     * @return HANDLED if processing has completed and NOT_HANDLED
+     *         if the message wasn't processed.
+     */
+    boolean processMessage(Message msg);
+
+    /**
+     * Name of State for debugging purposes.
+     *
+     * @return name of state.
+     */
+    @UnsupportedAppUsage
+    String getName();
+}
diff --git a/src/com/android/cellbroadcastservice/SmsCbHeader.java b/src/com/android/cellbroadcastservice/SmsCbHeader.java
new file mode 100644
index 0000000..4cdd1cb
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/SmsCbHeader.java
@@ -0,0 +1,596 @@
+/*
+ * Copyright (C) 2010 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.SmsCbCmasInfo;
+import android.telephony.SmsCbEtwsInfo;
+
+import com.android.internal.telephony.SmsConstants;
+import com.android.internal.telephony.gsm.SmsCbConstants;
+
+import dalvik.annotation.compat.UnsupportedAppUsage;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * Parses a 3GPP TS 23.041 cell broadcast message header. This class is public for use by
+ * CellBroadcastReceiver test cases, but should not be used by applications.
+ *
+ * All relevant header information is now sent as a Parcelable
+ * {@link android.telephony.SmsCbMessage} object in the "message" extra of the
+ * {@link android.provider.Telephony.Sms.Intents#SMS_CB_RECEIVED_ACTION} or
+ * {@link android.provider.Telephony.Sms.Intents#SMS_EMERGENCY_CB_RECEIVED_ACTION} intent.
+ * The raw PDU is no longer sent to SMS CB applications.
+ */
+public class SmsCbHeader {
+    /**
+     * Languages in the 0000xxxx DCS group as defined in 3GPP TS 23.038, section 5.
+     */
+    private static final String[] LANGUAGE_CODES_GROUP_0 = {
+            Locale.GERMAN.getLanguage(),        // German
+            Locale.ENGLISH.getLanguage(),       // English
+            Locale.ITALIAN.getLanguage(),       // Italian
+            Locale.FRENCH.getLanguage(),        // French
+            new Locale("es").getLanguage(),     // Spanish
+            new Locale("nl").getLanguage(),     // Dutch
+            new Locale("sv").getLanguage(),     // Swedish
+            new Locale("da").getLanguage(),     // Danish
+            new Locale("pt").getLanguage(),     // Portuguese
+            new Locale("fi").getLanguage(),     // Finnish
+            new Locale("nb").getLanguage(),     // Norwegian
+            new Locale("el").getLanguage(),     // Greek
+            new Locale("tr").getLanguage(),     // Turkish
+            new Locale("hu").getLanguage(),     // Hungarian
+            new Locale("pl").getLanguage(),     // Polish
+            null
+    };
+
+    /**
+     * Languages in the 0010xxxx DCS group as defined in 3GPP TS 23.038, section 5.
+     */
+    private static final String[] LANGUAGE_CODES_GROUP_2 = {
+            new Locale("cs").getLanguage(),     // Czech
+            new Locale("he").getLanguage(),     // Hebrew
+            new Locale("ar").getLanguage(),     // Arabic
+            new Locale("ru").getLanguage(),     // Russian
+            new Locale("is").getLanguage(),     // Icelandic
+            null, null, null, null, null, null, null, null, null, null, null
+    };
+
+    /**
+     * Length of SMS-CB header
+     */
+    static final int PDU_HEADER_LENGTH = 6;
+
+    /**
+     * GSM pdu format, as defined in 3gpp TS 23.041, section 9.4.1
+     */
+    static final int FORMAT_GSM = 1;
+
+    /**
+     * UMTS pdu format, as defined in 3gpp TS 23.041, section 9.4.2
+     */
+    static final int FORMAT_UMTS = 2;
+
+    /**
+     * ETWS pdu format, as defined in 3gpp TS 23.041, section 9.4.1.3
+     */
+    static final int FORMAT_ETWS_PRIMARY = 3;
+
+    /**
+     * Message type value as defined in 3gpp TS 25.324, section 11.1.
+     */
+    private static final int MESSAGE_TYPE_CBS_MESSAGE = 1;
+
+    /**
+     * Length of GSM pdus
+     */
+    private static final int PDU_LENGTH_GSM = 88;
+
+    /**
+     * Maximum length of ETWS primary message GSM pdus
+     */
+    private static final int PDU_LENGTH_ETWS = 56;
+
+    private final int mGeographicalScope;
+
+    /** The serial number combines geographical scope, message code, and update number. */
+    private final int mSerialNumber;
+
+    /** The Message Identifier in 3GPP is the same as the Service Category in CDMA. */
+    @UnsupportedAppUsage
+    private final int mMessageIdentifier;
+
+    private final int mDataCodingScheme;
+
+    private final int mPageIndex;
+
+    private final int mNrOfPages;
+
+    private final int mFormat;
+
+    private DataCodingScheme mDataCodingSchemeStructedData;
+
+    /** ETWS warning notification info. */
+    private final SmsCbEtwsInfo mEtwsInfo;
+
+    /** CMAS warning notification info. */
+    private final SmsCbCmasInfo mCmasInfo;
+
+    @UnsupportedAppUsage
+    public SmsCbHeader(byte[] pdu) throws IllegalArgumentException {
+        if (pdu == null || pdu.length < PDU_HEADER_LENGTH) {
+            throw new IllegalArgumentException("Illegal PDU");
+        }
+
+        if (pdu.length <= PDU_LENGTH_GSM) {
+            // can be ETWS or GSM format.
+            // Per TS23.041 9.4.1.2 and 9.4.1.3.2, GSM and ETWS format both
+            // contain serial number which contains GS, Message Code, and Update Number
+            // per 9.4.1.2.1, and message identifier in same octets
+            mGeographicalScope = (pdu[0] & 0xc0) >>> 6;
+            mSerialNumber = ((pdu[0] & 0xff) << 8) | (pdu[1] & 0xff);
+            mMessageIdentifier = ((pdu[2] & 0xff) << 8) | (pdu[3] & 0xff);
+            if (isEtwsMessage() && pdu.length <= PDU_LENGTH_ETWS) {
+                mFormat = FORMAT_ETWS_PRIMARY;
+                mDataCodingScheme = -1;
+                mPageIndex = -1;
+                mNrOfPages = -1;
+                boolean emergencyUserAlert = (pdu[4] & 0x1) != 0;
+                boolean activatePopup = (pdu[5] & 0x80) != 0;
+                int warningType = (pdu[4] & 0xfe) >>> 1;
+                byte[] warningSecurityInfo;
+                // copy the Warning-Security-Information, if present
+                if (pdu.length > PDU_HEADER_LENGTH) {
+                    warningSecurityInfo = Arrays.copyOfRange(pdu, 6, pdu.length);
+                } else {
+                    warningSecurityInfo = null;
+                }
+                mEtwsInfo = new SmsCbEtwsInfo(warningType, emergencyUserAlert, activatePopup,
+                        true, warningSecurityInfo);
+                mCmasInfo = null;
+                return;     // skip the ETWS/CMAS initialization code for regular notifications
+            } else {
+                // GSM pdus are no more than 88 bytes
+                mFormat = FORMAT_GSM;
+                mDataCodingScheme = pdu[4] & 0xff;
+
+                // Check for invalid page parameter
+                int pageIndex = (pdu[5] & 0xf0) >>> 4;
+                int nrOfPages = pdu[5] & 0x0f;
+
+                if (pageIndex == 0 || nrOfPages == 0 || pageIndex > nrOfPages) {
+                    pageIndex = 1;
+                    nrOfPages = 1;
+                }
+
+                mPageIndex = pageIndex;
+                mNrOfPages = nrOfPages;
+            }
+        } else {
+            // UMTS pdus are always at least 90 bytes since the payload includes
+            // a number-of-pages octet and also one length octet per page
+            mFormat = FORMAT_UMTS;
+
+            int messageType = pdu[0];
+
+            if (messageType != MESSAGE_TYPE_CBS_MESSAGE) {
+                throw new IllegalArgumentException("Unsupported message type " + messageType);
+            }
+
+            mMessageIdentifier = ((pdu[1] & 0xff) << 8) | pdu[2] & 0xff;
+            mGeographicalScope = (pdu[3] & 0xc0) >>> 6;
+            mSerialNumber = ((pdu[3] & 0xff) << 8) | (pdu[4] & 0xff);
+            mDataCodingScheme = pdu[5] & 0xff;
+
+            // We will always consider a UMTS message as having one single page
+            // since there's only one instance of the header, even though the
+            // actual payload may contain several pages.
+            mPageIndex = 1;
+            mNrOfPages = 1;
+        }
+
+        if (mDataCodingScheme != -1) {
+            mDataCodingSchemeStructedData = new DataCodingScheme(mDataCodingScheme);
+        }
+
+        if (isEtwsMessage()) {
+            boolean emergencyUserAlert = isEtwsEmergencyUserAlert();
+            boolean activatePopup = isEtwsPopupAlert();
+            int warningType = getEtwsWarningType();
+            mEtwsInfo = new SmsCbEtwsInfo(warningType, emergencyUserAlert, activatePopup,
+                    false, null);
+            mCmasInfo = null;
+        } else if (isCmasMessage()) {
+            int messageClass = getCmasMessageClass();
+            int severity = getCmasSeverity();
+            int urgency = getCmasUrgency();
+            int certainty = getCmasCertainty();
+            mEtwsInfo = null;
+            mCmasInfo = new SmsCbCmasInfo(messageClass, SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN,
+                    SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, severity, urgency, certainty);
+        } else {
+            mEtwsInfo = null;
+            mCmasInfo = null;
+        }
+    }
+
+    @UnsupportedAppUsage
+    int getGeographicalScope() {
+        return mGeographicalScope;
+    }
+
+    @UnsupportedAppUsage
+    int getSerialNumber() {
+        return mSerialNumber;
+    }
+
+    @UnsupportedAppUsage
+    int getServiceCategory() {
+        return mMessageIdentifier;
+    }
+
+    int getDataCodingScheme() {
+        return mDataCodingScheme;
+    }
+
+    DataCodingScheme getDataCodingSchemeStructedData() {
+        return mDataCodingSchemeStructedData;
+    }
+
+    @UnsupportedAppUsage
+    int getPageIndex() {
+        return mPageIndex;
+    }
+
+    @UnsupportedAppUsage
+    int getNumberOfPages() {
+        return mNrOfPages;
+    }
+
+    SmsCbEtwsInfo getEtwsInfo() {
+        return mEtwsInfo;
+    }
+
+    SmsCbCmasInfo getCmasInfo() {
+        return mCmasInfo;
+    }
+
+    /**
+     * Return whether this broadcast is an emergency (PWS) message type.
+     * @return true if this message is emergency type; false otherwise
+     */
+    boolean isEmergencyMessage() {
+        return mMessageIdentifier >= SmsCbConstants.MESSAGE_ID_PWS_FIRST_IDENTIFIER
+                && mMessageIdentifier <= SmsCbConstants.MESSAGE_ID_PWS_LAST_IDENTIFIER;
+    }
+
+    /**
+     * Return whether this broadcast is an ETWS emergency message type.
+     * @return true if this message is ETWS emergency type; false otherwise
+     */
+    private boolean isEtwsMessage() {
+        return (mMessageIdentifier & SmsCbConstants.MESSAGE_ID_ETWS_TYPE_MASK)
+                == SmsCbConstants.MESSAGE_ID_ETWS_TYPE;
+    }
+
+    /**
+     * Return whether this broadcast is an ETWS primary notification.
+     * @return true if this message is an ETWS primary notification; false otherwise
+     */
+    boolean isEtwsPrimaryNotification() {
+        return mFormat == FORMAT_ETWS_PRIMARY;
+    }
+
+    /**
+     * Return whether this broadcast is in UMTS format.
+     * @return true if this message is in UMTS format; false otherwise
+     */
+    boolean isUmtsFormat() {
+        return mFormat == FORMAT_UMTS;
+    }
+
+    /**
+     * Return whether this message is a CMAS emergency message type.
+     * @return true if this message is CMAS emergency type; false otherwise
+     */
+    private boolean isCmasMessage() {
+        return mMessageIdentifier >= SmsCbConstants.MESSAGE_ID_CMAS_FIRST_IDENTIFIER
+                && mMessageIdentifier <= SmsCbConstants.MESSAGE_ID_CMAS_LAST_IDENTIFIER;
+    }
+
+    /**
+     * Return whether the popup alert flag is set for an ETWS warning notification.
+     * This method assumes that the message ID has already been checked for ETWS type.
+     *
+     * @return true if the message code indicates a popup alert should be displayed
+     */
+    private boolean isEtwsPopupAlert() {
+        return (mSerialNumber & SmsCbConstants.SERIAL_NUMBER_ETWS_ACTIVATE_POPUP) != 0;
+    }
+
+    /**
+     * Return whether the emergency user alert flag is set for an ETWS warning notification.
+     * This method assumes that the message ID has already been checked for ETWS type.
+     *
+     * @return true if the message code indicates an emergency user alert
+     */
+    private boolean isEtwsEmergencyUserAlert() {
+        return (mSerialNumber & SmsCbConstants.SERIAL_NUMBER_ETWS_EMERGENCY_USER_ALERT) != 0;
+    }
+
+    /**
+     * Returns the warning type for an ETWS warning notification.
+     * This method assumes that the message ID has already been checked for ETWS type.
+     *
+     * @return the ETWS warning type defined in 3GPP TS 23.041 section 9.3.24
+     */
+    private int getEtwsWarningType() {
+        return mMessageIdentifier - SmsCbConstants.MESSAGE_ID_ETWS_EARTHQUAKE_WARNING;
+    }
+
+    /**
+     * Returns the message class for a CMAS warning notification.
+     * This method assumes that the message ID has already been checked for CMAS type.
+     * @return the CMAS message class as defined in {@link SmsCbCmasInfo}
+     */
+    private int getCmasMessageClass() {
+        switch (mMessageIdentifier) {
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXERCISE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXERCISE_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_OPERATOR_DEFINED_USE;
+
+            default:
+                return SmsCbCmasInfo.CMAS_CLASS_UNKNOWN;
+        }
+    }
+
+    /**
+     * Returns the severity for a CMAS warning notification. This is only available for extreme
+     * and severe alerts, not for other types such as Presidential Level and AMBER alerts.
+     * This method assumes that the message ID has already been checked for CMAS type.
+     * @return the CMAS severity as defined in {@link SmsCbCmasInfo}
+     */
+    private int getCmasSeverity() {
+        switch (mMessageIdentifier) {
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_SEVERITY_EXTREME;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_SEVERITY_SEVERE;
+
+            default:
+                return SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN;
+        }
+    }
+
+    /**
+     * Returns the urgency for a CMAS warning notification. This is only available for extreme
+     * and severe alerts, not for other types such as Presidential Level and AMBER alerts.
+     * This method assumes that the message ID has already been checked for CMAS type.
+     * @return the CMAS urgency as defined in {@link SmsCbCmasInfo}
+     */
+    private int getCmasUrgency() {
+        switch (mMessageIdentifier) {
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_URGENCY_IMMEDIATE;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_URGENCY_EXPECTED;
+
+            default:
+                return SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN;
+        }
+    }
+
+    /**
+     * Returns the certainty for a CMAS warning notification. This is only available for extreme
+     * and severe alerts, not for other types such as Presidential Level and AMBER alerts.
+     * This method assumes that the message ID has already been checked for CMAS type.
+     * @return the CMAS certainty as defined in {@link SmsCbCmasInfo}
+     */
+    private int getCmasCertainty() {
+        switch (mMessageIdentifier) {
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CERTAINTY_OBSERVED;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE:
+                return SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY;
+
+            default:
+                return SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "SmsCbHeader{GS=" + mGeographicalScope + ", serialNumber=0x"
+                + Integer.toHexString(mSerialNumber)
+                + ", messageIdentifier=0x" + Integer.toHexString(mMessageIdentifier)
+                + ", format=" + mFormat
+                + ", DCS=0x" + Integer.toHexString(mDataCodingScheme)
+                + ", page " + mPageIndex + " of " + mNrOfPages + '}';
+    }
+
+    /**
+     * CBS Data Coding Scheme.
+     * Reference: 3GPP TS 23.038 version 15.0.0 section #5, CBS Data Coding Scheme
+     */
+    public static final class DataCodingScheme {
+        public final int encoding;
+        public final String language;
+        public final boolean hasLanguageIndicator;
+
+        public DataCodingScheme(int dataCodingScheme) {
+            int encoding = 0;
+            String language = null;
+            boolean hasLanguageIndicator = false;
+
+            // Extract encoding and language from DCS, as defined in 3gpp TS 23.038,
+            // section 5.
+            switch ((dataCodingScheme & 0xf0) >> 4) {
+                case 0x00:
+                    encoding = SmsConstants.ENCODING_7BIT;
+                    language = LANGUAGE_CODES_GROUP_0[dataCodingScheme & 0x0f];
+                    break;
+
+                case 0x01:
+                    hasLanguageIndicator = true;
+                    if ((dataCodingScheme & 0x0f) == 0x01) {
+                        encoding = SmsConstants.ENCODING_16BIT;
+                    } else {
+                        encoding = SmsConstants.ENCODING_7BIT;
+                    }
+                    break;
+
+                case 0x02:
+                    encoding = SmsConstants.ENCODING_7BIT;
+                    language = LANGUAGE_CODES_GROUP_2[dataCodingScheme & 0x0f];
+                    break;
+
+                case 0x03:
+                    encoding = SmsConstants.ENCODING_7BIT;
+                    break;
+
+                case 0x04:
+                case 0x05:
+                    switch ((dataCodingScheme & 0x0c) >> 2) {
+                        case 0x01:
+                            encoding = SmsConstants.ENCODING_8BIT;
+                            break;
+
+                        case 0x02:
+                            encoding = SmsConstants.ENCODING_16BIT;
+                            break;
+
+                        case 0x00:
+                        default:
+                            encoding = SmsConstants.ENCODING_7BIT;
+                            break;
+                    }
+                    break;
+
+                case 0x06:
+                case 0x07:
+                    // Compression not supported
+                case 0x09:
+                    // UDH structure not supported
+                case 0x0e:
+                    // Defined by the WAP forum not supported
+                    throw new IllegalArgumentException("Unsupported GSM dataCodingScheme "
+                            + dataCodingScheme);
+
+                case 0x0f:
+                    if (((dataCodingScheme & 0x04) >> 2) == 0x01) {
+                        encoding = SmsConstants.ENCODING_8BIT;
+                    } else {
+                        encoding = SmsConstants.ENCODING_7BIT;
+                    }
+                    break;
+
+                default:
+                    // Reserved values are to be treated as 7-bit
+                    encoding = SmsConstants.ENCODING_7BIT;
+                    break;
+            }
+
+
+            this.encoding = encoding;
+            this.language = language;
+            this.hasLanguageIndicator = hasLanguageIndicator;
+        }
+    }
+}
diff --git a/src/com/android/cellbroadcastservice/State.java b/src/com/android/cellbroadcastservice/State.java
new file mode 100644
index 0000000..8d7cbc5
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/State.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cellbroadcastservice;
+
+import android.annotation.UnsupportedAppUsage;
+import android.os.Message;
+
+/**
+ * {@hide}
+ *
+ * The class for implementing states in a StateMachine
+ */
+public class State implements IState {
+
+    /**
+     * Constructor
+     */
+    @UnsupportedAppUsage
+    protected State() {
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.internal.util.IState#enter()
+     */
+    @UnsupportedAppUsage
+    @Override
+    public void enter() {
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.internal.util.IState#exit()
+     */
+    @UnsupportedAppUsage
+    @Override
+    public void exit() {
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.internal.util.IState#processMessage(android.os.Message)
+     */
+    @UnsupportedAppUsage
+    @Override
+    public boolean processMessage(Message msg) {
+        return false;
+    }
+
+    /**
+     * Name of State for debugging purposes.
+     *
+     * This default implementation returns the class name, returning
+     * the instance name would better in cases where a State class
+     * is used for multiple states. But normally there is one class per
+     * state and the class name is sufficient and easy to get. You may
+     * want to provide a setName or some other mechanism for setting
+     * another name if the class name is not appropriate.
+     *
+     * @see com.android.internal.util.IState#processMessage(android.os.Message)
+     */
+    @UnsupportedAppUsage
+    @Override
+    public String getName() {
+        String name = getClass().getName();
+        int lastDollar = name.lastIndexOf('$');
+        return name.substring(lastDollar + 1);
+    }
+}
diff --git a/src/com/android/cellbroadcastservice/StateMachine.java b/src/com/android/cellbroadcastservice/StateMachine.java
new file mode 100644
index 0000000..1301ea8
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/StateMachine.java
@@ -0,0 +1,2183 @@
+/**
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cellbroadcastservice;
+
+import android.annotation.UnsupportedAppUsage;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Vector;
+
+/**
+ * {@hide}
+ *
+ * <p>The state machine defined here is a hierarchical state machine which processes messages
+ * and can have states arranged hierarchically.</p>
+ *
+ * <p>A state is a <code>State</code> object and must implement
+ * <code>processMessage</code> and optionally <code>enter/exit/getName</code>.
+ * The enter/exit methods are equivalent to the construction and destruction
+ * in Object Oriented programming and are used to perform initialization and
+ * cleanup of the state respectively. The <code>getName</code> method returns the
+ * name of the state; the default implementation returns the class name. It may be
+ * desirable to have <code>getName</code> return the state instance name instead,
+ * in particular if a particular state class has multiple instances.</p>
+ *
+ * <p>When a state machine is created, <code>addState</code> is used to build the
+ * hierarchy and <code>setInitialState</code> is used to identify which of these
+ * is the initial state. After construction the programmer calls <code>start</code>
+ * which initializes and starts the state machine. The first action the StateMachine
+ * is to the invoke <code>enter</code> for all of the initial state's hierarchy,
+ * starting at its eldest parent. The calls to enter will be done in the context
+ * of the StateMachine's Handler, not in the context of the call to start, and they
+ * will be invoked before any messages are processed. For example, given the simple
+ * state machine below, mP1.enter will be invoked and then mS1.enter. Finally,
+ * messages sent to the state machine will be processed by the current state;
+ * in our simple state machine below that would initially be mS1.processMessage.</p>
+<pre>
+        mP1
+       /   \
+      mS2   mS1 ----&gt; initial state
+</pre>
+ * <p>After the state machine is created and started, messages are sent to a state
+ * machine using <code>sendMessage</code> and the messages are created using
+ * <code>obtainMessage</code>. When the state machine receives a message the
+ * current state's <code>processMessage</code> is invoked. In the above example
+ * mS1.processMessage will be invoked first. The state may use <code>transitionTo</code>
+ * to change the current state to a new state.</p>
+ *
+ * <p>Each state in the state machine may have a zero or one parent states. If
+ * a child state is unable to handle a message it may have the message processed
+ * by its parent by returning false or NOT_HANDLED. If a message is not handled by
+ * a child state or any of its ancestors, <code>unhandledMessage</code> will be invoked
+ * to give one last chance for the state machine to process the message.</p>
+ *
+ * <p>When all processing is completed a state machine may choose to call
+ * <code>transitionToHaltingState</code>. When the current <code>processingMessage</code>
+ * returns the state machine will transfer to an internal <code>HaltingState</code>
+ * and invoke <code>halting</code>. Any message subsequently received by the state
+ * machine will cause <code>haltedProcessMessage</code> to be invoked.</p>
+ *
+ * <p>If it is desirable to completely stop the state machine call <code>quit</code> or
+ * <code>quitNow</code>. These will call <code>exit</code> of the current state and its parents,
+ * call <code>onQuitting</code> and then exit Thread/Loopers.</p>
+ *
+ * <p>In addition to <code>processMessage</code> each <code>State</code> has
+ * an <code>enter</code> method and <code>exit</code> method which may be overridden.</p>
+ *
+ * <p>Since the states are arranged in a hierarchy transitioning to a new state
+ * causes current states to be exited and new states to be entered. To determine
+ * the list of states to be entered/exited the common parent closest to
+ * the current state is found. We then exit from the current state and its
+ * parent's up to but not including the common parent state and then enter all
+ * of the new states below the common parent down to the destination state.
+ * If there is no common parent all states are exited and then the new states
+ * are entered.</p>
+ *
+ * <p>Two other methods that states can use are <code>deferMessage</code> and
+ * <code>sendMessageAtFrontOfQueue</code>. The <code>sendMessageAtFrontOfQueue</code> sends
+ * a message but places it on the front of the queue rather than the back. The
+ * <code>deferMessage</code> causes the message to be saved on a list until a
+ * transition is made to a new state. At which time all of the deferred messages
+ * will be put on the front of the state machine queue with the oldest message
+ * at the front. These will then be processed by the new current state before
+ * any other messages that are on the queue or might be added later. Both of
+ * these are protected and may only be invoked from within a state machine.</p>
+ *
+ * <p>To illustrate some of these properties we'll use state machine with an 8
+ * state hierarchy:</p>
+<pre>
+          mP0
+         /   \
+        mP1   mS0
+       /   \
+      mS2   mS1
+     /  \    \
+    mS3  mS4  mS5  ---&gt; initial state
+</pre>
+ * <p>After starting mS5 the list of active states is mP0, mP1, mS1 and mS5.
+ * So the order of calling processMessage when a message is received is mS5,
+ * mS1, mP1, mP0 assuming each processMessage indicates it can't handle this
+ * message by returning false or NOT_HANDLED.</p>
+ *
+ * <p>Now assume mS5.processMessage receives a message it can handle, and during
+ * the handling determines the machine should change states. It could call
+ * transitionTo(mS4) and return true or HANDLED. Immediately after returning from
+ * processMessage the state machine runtime will find the common parent,
+ * which is mP1. It will then call mS5.exit, mS1.exit, mS2.enter and then
+ * mS4.enter. The new list of active states is mP0, mP1, mS2 and mS4. So
+ * when the next message is received mS4.processMessage will be invoked.</p>
+ *
+ * <p>Now for some concrete examples, here is the canonical HelloWorld as a state machine.
+ * It responds with "Hello World" being printed to the log for every message.</p>
+<pre>
+class HelloWorld extends StateMachine {
+    HelloWorld(String name) {
+        super(name);
+        addState(mState1);
+        setInitialState(mState1);
+    }
+
+    public static HelloWorld makeHelloWorld() {
+        HelloWorld hw = new HelloWorld("hw");
+        hw.start();
+        return hw;
+    }
+
+    class State1 extends State {
+        &#64;Override public boolean processMessage(Message message) {
+            log("Hello World");
+            return HANDLED;
+        }
+    }
+    State1 mState1 = new State1();
+}
+
+void testHelloWorld() {
+    HelloWorld hw = makeHelloWorld();
+    hw.sendMessage(hw.obtainMessage());
+}
+</pre>
+ * <p>A more interesting state machine is one with four states
+ * with two independent parent states.</p>
+<pre>
+        mP1      mP2
+       /   \
+      mS2   mS1
+</pre>
+ * <p>Here is a description of this state machine using pseudo code.</p>
+ <pre>
+state mP1 {
+     enter { log("mP1.enter"); }
+     exit { log("mP1.exit");  }
+     on msg {
+         CMD_2 {
+             send(CMD_3);
+             defer(msg);
+             transitionTo(mS2);
+             return HANDLED;
+         }
+         return NOT_HANDLED;
+     }
+}
+
+INITIAL
+state mS1 parent mP1 {
+     enter { log("mS1.enter"); }
+     exit  { log("mS1.exit");  }
+     on msg {
+         CMD_1 {
+             transitionTo(mS1);
+             return HANDLED;
+         }
+         return NOT_HANDLED;
+     }
+}
+
+state mS2 parent mP1 {
+     enter { log("mS2.enter"); }
+     exit  { log("mS2.exit");  }
+     on msg {
+         CMD_2 {
+             send(CMD_4);
+             return HANDLED;
+         }
+         CMD_3 {
+             defer(msg);
+             transitionTo(mP2);
+             return HANDLED;
+         }
+         return NOT_HANDLED;
+     }
+}
+
+state mP2 {
+     enter {
+         log("mP2.enter");
+         send(CMD_5);
+     }
+     exit { log("mP2.exit"); }
+     on msg {
+         CMD_3, CMD_4 { return HANDLED; }
+         CMD_5 {
+             transitionTo(HaltingState);
+             return HANDLED;
+         }
+         return NOT_HANDLED;
+     }
+}
+</pre>
+ * <p>The implementation is below and also in StateMachineTest:</p>
+<pre>
+class Hsm1 extends StateMachine {
+    public static final int CMD_1 = 1;
+    public static final int CMD_2 = 2;
+    public static final int CMD_3 = 3;
+    public static final int CMD_4 = 4;
+    public static final int CMD_5 = 5;
+
+    public static Hsm1 makeHsm1() {
+        log("makeHsm1 E");
+        Hsm1 sm = new Hsm1("hsm1");
+        sm.start();
+        log("makeHsm1 X");
+        return sm;
+    }
+
+    Hsm1(String name) {
+        super(name);
+        log("ctor E");
+
+        // Add states, use indentation to show hierarchy
+        addState(mP1);
+            addState(mS1, mP1);
+            addState(mS2, mP1);
+        addState(mP2);
+
+        // Set the initial state
+        setInitialState(mS1);
+        log("ctor X");
+    }
+
+    class P1 extends State {
+        &#64;Override public void enter() {
+            log("mP1.enter");
+        }
+        &#64;Override public boolean processMessage(Message message) {
+            boolean retVal;
+            log("mP1.processMessage what=" + message.what);
+            switch(message.what) {
+            case CMD_2:
+                // CMD_2 will arrive in mS2 before CMD_3
+                sendMessage(obtainMessage(CMD_3));
+                deferMessage(message);
+                transitionTo(mS2);
+                retVal = HANDLED;
+                break;
+            default:
+                // Any message we don't understand in this state invokes unhandledMessage
+                retVal = NOT_HANDLED;
+                break;
+            }
+            return retVal;
+        }
+        &#64;Override public void exit() {
+            log("mP1.exit");
+        }
+    }
+
+    class S1 extends State {
+        &#64;Override public void enter() {
+            log("mS1.enter");
+        }
+        &#64;Override public boolean processMessage(Message message) {
+            log("S1.processMessage what=" + message.what);
+            if (message.what == CMD_1) {
+                // Transition to ourself to show that enter/exit is called
+                transitionTo(mS1);
+                return HANDLED;
+            } else {
+                // Let parent process all other messages
+                return NOT_HANDLED;
+            }
+        }
+        &#64;Override public void exit() {
+            log("mS1.exit");
+        }
+    }
+
+    class S2 extends State {
+        &#64;Override public void enter() {
+            log("mS2.enter");
+        }
+        &#64;Override public boolean processMessage(Message message) {
+            boolean retVal;
+            log("mS2.processMessage what=" + message.what);
+            switch(message.what) {
+            case(CMD_2):
+                sendMessage(obtainMessage(CMD_4));
+                retVal = HANDLED;
+                break;
+            case(CMD_3):
+                deferMessage(message);
+                transitionTo(mP2);
+                retVal = HANDLED;
+                break;
+            default:
+                retVal = NOT_HANDLED;
+                break;
+            }
+            return retVal;
+        }
+        &#64;Override public void exit() {
+            log("mS2.exit");
+        }
+    }
+
+    class P2 extends State {
+        &#64;Override public void enter() {
+            log("mP2.enter");
+            sendMessage(obtainMessage(CMD_5));
+        }
+        &#64;Override public boolean processMessage(Message message) {
+            log("P2.processMessage what=" + message.what);
+            switch(message.what) {
+            case(CMD_3):
+                break;
+            case(CMD_4):
+                break;
+            case(CMD_5):
+                transitionToHaltingState();
+                break;
+            }
+            return HANDLED;
+        }
+        &#64;Override public void exit() {
+            log("mP2.exit");
+        }
+    }
+
+    &#64;Override
+    void onHalting() {
+        log("halting");
+        synchronized (this) {
+            this.notifyAll();
+        }
+    }
+
+    P1 mP1 = new P1();
+    S1 mS1 = new S1();
+    S2 mS2 = new S2();
+    P2 mP2 = new P2();
+}
+</pre>
+ * <p>If this is executed by sending two messages CMD_1 and CMD_2
+ * (Note the synchronize is only needed because we use hsm.wait())</p>
+<pre>
+Hsm1 hsm = makeHsm1();
+synchronize(hsm) {
+     hsm.sendMessage(obtainMessage(hsm.CMD_1));
+     hsm.sendMessage(obtainMessage(hsm.CMD_2));
+     try {
+          // wait for the messages to be handled
+          hsm.wait();
+     } catch (InterruptedException e) {
+          loge("exception while waiting " + e.getMessage());
+     }
+}
+</pre>
+ * <p>The output is:</p>
+<pre>
+D/hsm1    ( 1999): makeHsm1 E
+D/hsm1    ( 1999): ctor E
+D/hsm1    ( 1999): ctor X
+D/hsm1    ( 1999): mP1.enter
+D/hsm1    ( 1999): mS1.enter
+D/hsm1    ( 1999): makeHsm1 X
+D/hsm1    ( 1999): mS1.processMessage what=1
+D/hsm1    ( 1999): mS1.exit
+D/hsm1    ( 1999): mS1.enter
+D/hsm1    ( 1999): mS1.processMessage what=2
+D/hsm1    ( 1999): mP1.processMessage what=2
+D/hsm1    ( 1999): mS1.exit
+D/hsm1    ( 1999): mS2.enter
+D/hsm1    ( 1999): mS2.processMessage what=2
+D/hsm1    ( 1999): mS2.processMessage what=3
+D/hsm1    ( 1999): mS2.exit
+D/hsm1    ( 1999): mP1.exit
+D/hsm1    ( 1999): mP2.enter
+D/hsm1    ( 1999): mP2.processMessage what=3
+D/hsm1    ( 1999): mP2.processMessage what=4
+D/hsm1    ( 1999): mP2.processMessage what=5
+D/hsm1    ( 1999): mP2.exit
+D/hsm1    ( 1999): halting
+</pre>
+ */
+public class StateMachine {
+    // Name of the state machine and used as logging tag
+    private String mName;
+
+    /** Message.what value when quitting */
+    private static final int SM_QUIT_CMD = -1;
+
+    /** Message.what value when initializing */
+    private static final int SM_INIT_CMD = -2;
+
+    /**
+     * Convenience constant that maybe returned by processMessage
+     * to indicate the message was processed and is not to be
+     * processed by parent states
+     */
+    public static final boolean HANDLED = true;
+
+    /**
+     * Convenience constant that maybe returned by processMessage
+     * to indicate the message was NOT processed and is to be
+     * processed by parent states
+     */
+    public static final boolean NOT_HANDLED = false;
+
+    /**
+     * StateMachine logging record.
+     * {@hide}
+     */
+    public static class LogRec {
+        private StateMachine mSm;
+        private long mTime;
+        private int mWhat;
+        private String mInfo;
+        private IState mState;
+        private IState mOrgState;
+        private IState mDstState;
+
+        /**
+         * Constructor
+         *
+         * @param msg
+         * @param state the state which handled the message
+         * @param orgState is the first state the received the message but
+         * did not processes the message.
+         * @param transToState is the state that was transitioned to after the message was
+         * processed.
+         */
+        LogRec(StateMachine sm, Message msg, String info, IState state, IState orgState,
+                IState transToState) {
+            update(sm, msg, info, state, orgState, transToState);
+        }
+
+        /**
+         * Update the information in the record.
+         * @param state that handled the message
+         * @param orgState is the first state the received the message
+         * @param dstState is the state that was the transition target when logging
+         */
+        public void update(StateMachine sm, Message msg, String info, IState state, IState orgState,
+                IState dstState) {
+            mSm = sm;
+            mTime = System.currentTimeMillis();
+            mWhat = (msg != null) ? msg.what : 0;
+            mInfo = info;
+            mState = state;
+            mOrgState = orgState;
+            mDstState = dstState;
+        }
+
+        /**
+         * @return time stamp
+         */
+        public long getTime() {
+            return mTime;
+        }
+
+        /**
+         * @return msg.what
+         */
+        public long getWhat() {
+            return mWhat;
+        }
+
+        /**
+         * @return the command that was executing
+         */
+        public String getInfo() {
+            return mInfo;
+        }
+
+        /**
+         * @return the state that handled this message
+         */
+        public IState getState() {
+            return mState;
+        }
+
+        /**
+         * @return the state destination state if a transition is occurring or null if none.
+         */
+        public IState getDestState() {
+            return mDstState;
+        }
+
+        /**
+         * @return the original state that received the message.
+         */
+        public IState getOriginalState() {
+            return mOrgState;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("time=");
+            Calendar c = Calendar.getInstance();
+            c.setTimeInMillis(mTime);
+            sb.append(String.format("%tm-%td %tH:%tM:%tS.%tL", c, c, c, c, c, c));
+            sb.append(" processed=");
+            sb.append(mState == null ? "<null>" : mState.getName());
+            sb.append(" org=");
+            sb.append(mOrgState == null ? "<null>" : mOrgState.getName());
+            sb.append(" dest=");
+            sb.append(mDstState == null ? "<null>" : mDstState.getName());
+            sb.append(" what=");
+            String what = mSm != null ? mSm.getWhatToString(mWhat) : "";
+            if (TextUtils.isEmpty(what)) {
+                sb.append(mWhat);
+                sb.append("(0x");
+                sb.append(Integer.toHexString(mWhat));
+                sb.append(")");
+            } else {
+                sb.append(what);
+            }
+            if (!TextUtils.isEmpty(mInfo)) {
+                sb.append(" ");
+                sb.append(mInfo);
+            }
+            return sb.toString();
+        }
+    }
+
+    /**
+     * A list of log records including messages recently processed by the state machine.
+     *
+     * The class maintains a list of log records including messages
+     * recently processed. The list is finite and may be set in the
+     * constructor or by calling setSize. The public interface also
+     * includes size which returns the number of recent records,
+     * count which is the number of records processed since the
+     * the last setSize, get which returns a record and
+     * add which adds a record.
+     */
+    private static class LogRecords {
+
+        private static final int DEFAULT_SIZE = 20;
+
+        private Vector<LogRec> mLogRecVector = new Vector<LogRec>();
+        private int mMaxSize = DEFAULT_SIZE;
+        private int mOldestIndex = 0;
+        private int mCount = 0;
+        private boolean mLogOnlyTransitions = false;
+
+        /**
+         * private constructor use add
+         */
+        private LogRecords() {
+        }
+
+        /**
+         * Set size of messages to maintain and clears all current records.
+         *
+         * @param maxSize number of records to maintain at anyone time.
+        */
+        synchronized void setSize(int maxSize) {
+            // TODO: once b/28217358 is fixed, add unit tests  to verify that these variables are
+            // cleared after calling this method, and that subsequent calls to get() function as
+            // expected.
+            mMaxSize = maxSize;
+            mOldestIndex = 0;
+            mCount = 0;
+            mLogRecVector.clear();
+        }
+
+        synchronized void setLogOnlyTransitions(boolean enable) {
+            mLogOnlyTransitions = enable;
+        }
+
+        synchronized boolean logOnlyTransitions() {
+            return mLogOnlyTransitions;
+        }
+
+        /**
+         * @return the number of recent records.
+         */
+        synchronized int size() {
+            return mLogRecVector.size();
+        }
+
+        /**
+         * @return the total number of records processed since size was set.
+         */
+        synchronized int count() {
+            return mCount;
+        }
+
+        /**
+         * Clear the list of records.
+         */
+        synchronized void cleanup() {
+            mLogRecVector.clear();
+        }
+
+        /**
+         * @return the information on a particular record. 0 is the oldest
+         * record and size()-1 is the newest record. If the index is to
+         * large null is returned.
+         */
+        synchronized LogRec get(int index) {
+            int nextIndex = mOldestIndex + index;
+            if (nextIndex >= mMaxSize) {
+                nextIndex -= mMaxSize;
+            }
+            if (nextIndex >= size()) {
+                return null;
+            } else {
+                return mLogRecVector.get(nextIndex);
+            }
+        }
+
+        /**
+         * Add a processed message.
+         *
+         * @param msg
+         * @param messageInfo to be stored
+         * @param state that handled the message
+         * @param orgState is the first state the received the message but
+         * did not processes the message.
+         * @param transToState is the state that was transitioned to after the message was
+         * processed.
+         *
+         */
+        synchronized void add(StateMachine sm, Message msg, String messageInfo, IState state,
+                IState orgState, IState transToState) {
+            mCount += 1;
+            if (mLogRecVector.size() < mMaxSize) {
+                mLogRecVector.add(new LogRec(sm, msg, messageInfo, state, orgState, transToState));
+            } else {
+                LogRec pmi = mLogRecVector.get(mOldestIndex);
+                mOldestIndex += 1;
+                if (mOldestIndex >= mMaxSize) {
+                    mOldestIndex = 0;
+                }
+                pmi.update(sm, msg, messageInfo, state, orgState, transToState);
+            }
+        }
+    }
+
+    private static class SmHandler extends Handler {
+
+        /** true if StateMachine has quit */
+        private boolean mHasQuit = false;
+
+        /** The debug flag */
+        private boolean mDbg = false;
+
+        /** The SmHandler object, identifies that message is internal */
+        private static final Object sSmHandlerObj = new Object();
+
+        /** The current message */
+        private Message mMsg;
+
+        /** A list of log records including messages this state machine has processed */
+        private LogRecords mLogRecords = new LogRecords();
+
+        /** true if construction of the state machine has not been completed */
+        private boolean mIsConstructionCompleted;
+
+        /** Stack used to manage the current hierarchy of states */
+        private StateInfo[] mStateStack;
+
+        /** Top of mStateStack */
+        private int mStateStackTopIndex = -1;
+
+        /** A temporary stack used to manage the state stack */
+        private StateInfo[] mTempStateStack;
+
+        /** The top of the mTempStateStack */
+        private int mTempStateStackCount;
+
+        /** State used when state machine is halted */
+        private HaltingState mHaltingState = new HaltingState();
+
+        /** State used when state machine is quitting */
+        private QuittingState mQuittingState = new QuittingState();
+
+        /** Reference to the StateMachine */
+        private StateMachine mSm;
+
+        /**
+         * Information about a state.
+         * Used to maintain the hierarchy.
+         */
+        private class StateInfo {
+            /** The state */
+            State state;
+
+            /** The parent of this state, null if there is no parent */
+            StateInfo parentStateInfo;
+
+            /** True when the state has been entered and on the stack */
+            boolean active;
+
+            /**
+             * Convert StateInfo to string
+             */
+            @Override
+            public String toString() {
+                return "state=" + state.getName() + ",active=" + active + ",parent="
+                        + ((parentStateInfo == null) ? "null" : parentStateInfo.state.getName());
+            }
+        }
+
+        /** The map of all of the states in the state machine */
+        private HashMap<State, StateInfo> mStateInfo = new HashMap<State, StateInfo>();
+
+        /** The initial state that will process the first message */
+        private State mInitialState;
+
+        /** The destination state when transitionTo has been invoked */
+        private State mDestState;
+
+        /**
+         * Indicates if a transition is in progress
+         *
+         * This will be true for all calls of State.exit and all calls of State.enter except for the
+         * last enter call for the current destination state.
+         */
+        private boolean mTransitionInProgress = false;
+
+        /** The list of deferred messages */
+        private ArrayList<Message> mDeferredMessages = new ArrayList<Message>();
+
+        /**
+         * State entered when transitionToHaltingState is called.
+         */
+        private class HaltingState extends State {
+            @Override
+            public boolean processMessage(Message msg) {
+                mSm.haltedProcessMessage(msg);
+                return true;
+            }
+        }
+
+        /**
+         * State entered when a valid quit message is handled.
+         */
+        private class QuittingState extends State {
+            @Override
+            public boolean processMessage(Message msg) {
+                return NOT_HANDLED;
+            }
+        }
+
+        /**
+         * Handle messages sent to the state machine by calling
+         * the current state's processMessage. It also handles
+         * the enter/exit calls and placing any deferred messages
+         * back onto the queue when transitioning to a new state.
+         */
+        @Override
+        public final void handleMessage(Message msg) {
+            if (!mHasQuit) {
+                if (mSm != null && msg.what != SM_INIT_CMD && msg.what != SM_QUIT_CMD) {
+                    mSm.onPreHandleMessage(msg);
+                }
+
+                if (mDbg) mSm.log("handleMessage: E msg.what=" + msg.what);
+
+                /** Save the current message */
+                mMsg = msg;
+
+                /** State that processed the message */
+                State msgProcessedState = null;
+                if (mIsConstructionCompleted || (mMsg.what == SM_QUIT_CMD)) {
+                    /** Normal path */
+                    msgProcessedState = processMsg(msg);
+                } else if (!mIsConstructionCompleted && (mMsg.what == SM_INIT_CMD)
+                        && (mMsg.obj == sSmHandlerObj)) {
+                    /** Initial one time path. */
+                    mIsConstructionCompleted = true;
+                    invokeEnterMethods(0);
+                } else {
+                    throw new RuntimeException("StateMachine.handleMessage: "
+                            + "The start method not called, received msg: " + msg);
+                }
+                performTransitions(msgProcessedState, msg);
+
+                // We need to check if mSm == null here as we could be quitting.
+                if (mDbg && mSm != null) mSm.log("handleMessage: X");
+
+                if (mSm != null && msg.what != SM_INIT_CMD && msg.what != SM_QUIT_CMD) {
+                    mSm.onPostHandleMessage(msg);
+                }
+            }
+        }
+
+        /**
+         * Do any transitions
+         * @param msgProcessedState is the state that processed the message
+         */
+        private void performTransitions(State msgProcessedState, Message msg) {
+            /**
+             * If transitionTo has been called, exit and then enter
+             * the appropriate states. We loop on this to allow
+             * enter and exit methods to use transitionTo.
+             */
+            State orgState = mStateStack[mStateStackTopIndex].state;
+
+            /**
+             * Record whether message needs to be logged before we transition and
+             * and we won't log special messages SM_INIT_CMD or SM_QUIT_CMD which
+             * always set msg.obj to the handler.
+             */
+            boolean recordLogMsg = mSm.recordLogRec(mMsg) && (msg.obj != sSmHandlerObj);
+
+            if (mLogRecords.logOnlyTransitions()) {
+                /** Record only if there is a transition */
+                if (mDestState != null) {
+                    mLogRecords.add(mSm, mMsg, mSm.getLogRecString(mMsg), msgProcessedState,
+                            orgState, mDestState);
+                }
+            } else if (recordLogMsg) {
+                /** Record message */
+                mLogRecords.add(mSm, mMsg, mSm.getLogRecString(mMsg), msgProcessedState, orgState,
+                        mDestState);
+            }
+
+            State destState = mDestState;
+            if (destState != null) {
+                /**
+                 * Process the transitions including transitions in the enter/exit methods
+                 */
+                while (true) {
+                    if (mDbg) mSm.log("handleMessage: new destination call exit/enter");
+
+                    /**
+                     * Determine the states to exit and enter and return the
+                     * common ancestor state of the enter/exit states. Then
+                     * invoke the exit methods then the enter methods.
+                     */
+                    StateInfo commonStateInfo = setupTempStateStackWithStatesToEnter(destState);
+                    // flag is cleared in invokeEnterMethods before entering the target state
+                    mTransitionInProgress = true;
+                    invokeExitMethods(commonStateInfo);
+                    int stateStackEnteringIndex = moveTempStateStackToStateStack();
+                    invokeEnterMethods(stateStackEnteringIndex);
+
+                    /**
+                     * Since we have transitioned to a new state we need to have
+                     * any deferred messages moved to the front of the message queue
+                     * so they will be processed before any other messages in the
+                     * message queue.
+                     */
+                    moveDeferredMessageAtFrontOfQueue();
+
+                    if (destState != mDestState) {
+                        // A new mDestState so continue looping
+                        destState = mDestState;
+                    } else {
+                        // No change in mDestState so we're done
+                        break;
+                    }
+                }
+                mDestState = null;
+            }
+
+            /**
+             * After processing all transitions check and
+             * see if the last transition was to quit or halt.
+             */
+            if (destState != null) {
+                if (destState == mQuittingState) {
+                    /**
+                     * Call onQuitting to let subclasses cleanup.
+                     */
+                    mSm.onQuitting();
+                    cleanupAfterQuitting();
+                } else if (destState == mHaltingState) {
+                    /**
+                     * Call onHalting() if we've transitioned to the halting
+                     * state. All subsequent messages will be processed in
+                     * in the halting state which invokes haltedProcessMessage(msg);
+                     */
+                    mSm.onHalting();
+                }
+            }
+        }
+
+        /**
+         * Cleanup all the static variables and the looper after the SM has been quit.
+         */
+        private final void cleanupAfterQuitting() {
+            if (mSm.mSmThread != null) {
+                // If we made the thread then quit looper which stops the thread.
+                getLooper().quit();
+                mSm.mSmThread = null;
+            }
+
+            mSm.mSmHandler = null;
+            mSm = null;
+            mMsg = null;
+            mLogRecords.cleanup();
+            mStateStack = null;
+            mTempStateStack = null;
+            mStateInfo.clear();
+            mInitialState = null;
+            mDestState = null;
+            mDeferredMessages.clear();
+            mHasQuit = true;
+        }
+
+        /**
+         * Complete the construction of the state machine.
+         */
+        private final void completeConstruction() {
+            if (mDbg) mSm.log("completeConstruction: E");
+
+            /**
+             * Determine the maximum depth of the state hierarchy
+             * so we can allocate the state stacks.
+             */
+            int maxDepth = 0;
+            for (StateInfo si : mStateInfo.values()) {
+                int depth = 0;
+                for (StateInfo i = si; i != null; depth++) {
+                    i = i.parentStateInfo;
+                }
+                if (maxDepth < depth) {
+                    maxDepth = depth;
+                }
+            }
+            if (mDbg) mSm.log("completeConstruction: maxDepth=" + maxDepth);
+
+            mStateStack = new StateInfo[maxDepth];
+            mTempStateStack = new StateInfo[maxDepth];
+            setupInitialStateStack();
+
+            /** Sending SM_INIT_CMD message to invoke enter methods asynchronously */
+            sendMessageAtFrontOfQueue(obtainMessage(SM_INIT_CMD, sSmHandlerObj));
+
+            if (mDbg) mSm.log("completeConstruction: X");
+        }
+
+        /**
+         * Process the message. If the current state doesn't handle
+         * it, call the states parent and so on. If it is never handled then
+         * call the state machines unhandledMessage method.
+         * @return the state that processed the message
+         */
+        private final State processMsg(Message msg) {
+            StateInfo curStateInfo = mStateStack[mStateStackTopIndex];
+            if (mDbg) {
+                mSm.log("processMsg: " + curStateInfo.state.getName());
+            }
+
+            if (isQuit(msg)) {
+                transitionTo(mQuittingState);
+            } else {
+                while (!curStateInfo.state.processMessage(msg)) {
+                    /**
+                     * Not processed
+                     */
+                    curStateInfo = curStateInfo.parentStateInfo;
+                    if (curStateInfo == null) {
+                        /**
+                         * No parents left so it's not handled
+                         */
+                        mSm.unhandledMessage(msg);
+                        break;
+                    }
+                    if (mDbg) {
+                        mSm.log("processMsg: " + curStateInfo.state.getName());
+                    }
+                }
+            }
+            return (curStateInfo != null) ? curStateInfo.state : null;
+        }
+
+        /**
+         * Call the exit method for each state from the top of stack
+         * up to the common ancestor state.
+         */
+        private final void invokeExitMethods(StateInfo commonStateInfo) {
+            while ((mStateStackTopIndex >= 0)
+                    && (mStateStack[mStateStackTopIndex] != commonStateInfo)) {
+                State curState = mStateStack[mStateStackTopIndex].state;
+                if (mDbg) mSm.log("invokeExitMethods: " + curState.getName());
+                curState.exit();
+                mStateStack[mStateStackTopIndex].active = false;
+                mStateStackTopIndex -= 1;
+            }
+        }
+
+        /**
+         * Invoke the enter method starting at the entering index to top of state stack
+         */
+        private final void invokeEnterMethods(int stateStackEnteringIndex) {
+            for (int i = stateStackEnteringIndex; i <= mStateStackTopIndex; i++) {
+                if (stateStackEnteringIndex == mStateStackTopIndex) {
+                    // Last enter state for transition
+                    mTransitionInProgress = false;
+                }
+                if (mDbg) mSm.log("invokeEnterMethods: " + mStateStack[i].state.getName());
+                mStateStack[i].state.enter();
+                mStateStack[i].active = true;
+            }
+            mTransitionInProgress = false; // ensure flag set to false if no methods called
+        }
+
+        /**
+         * Move the deferred message to the front of the message queue.
+         */
+        private final void moveDeferredMessageAtFrontOfQueue() {
+            /**
+             * The oldest messages on the deferred list must be at
+             * the front of the queue so start at the back, which
+             * as the most resent message and end with the oldest
+             * messages at the front of the queue.
+             */
+            for (int i = mDeferredMessages.size() - 1; i >= 0; i--) {
+                Message curMsg = mDeferredMessages.get(i);
+                if (mDbg) mSm.log("moveDeferredMessageAtFrontOfQueue; what=" + curMsg.what);
+                sendMessageAtFrontOfQueue(curMsg);
+            }
+            mDeferredMessages.clear();
+        }
+
+        /**
+         * Move the contents of the temporary stack to the state stack
+         * reversing the order of the items on the temporary stack as
+         * they are moved.
+         *
+         * @return index into mStateStack where entering needs to start
+         */
+        private final int moveTempStateStackToStateStack() {
+            int startingIndex = mStateStackTopIndex + 1;
+            int i = mTempStateStackCount - 1;
+            int j = startingIndex;
+            while (i >= 0) {
+                if (mDbg) mSm.log("moveTempStackToStateStack: i=" + i + ",j=" + j);
+                mStateStack[j] = mTempStateStack[i];
+                j += 1;
+                i -= 1;
+            }
+
+            mStateStackTopIndex = j - 1;
+            if (mDbg) {
+                mSm.log("moveTempStackToStateStack: X mStateStackTop=" + mStateStackTopIndex
+                        + ",startingIndex=" + startingIndex + ",Top="
+                        + mStateStack[mStateStackTopIndex].state.getName());
+            }
+            return startingIndex;
+        }
+
+        /**
+         * Setup the mTempStateStack with the states we are going to enter.
+         *
+         * This is found by searching up the destState's ancestors for a
+         * state that is already active i.e. StateInfo.active == true.
+         * The destStae and all of its inactive parents will be on the
+         * TempStateStack as the list of states to enter.
+         *
+         * @return StateInfo of the common ancestor for the destState and
+         * current state or null if there is no common parent.
+         */
+        private final StateInfo setupTempStateStackWithStatesToEnter(State destState) {
+            /**
+             * Search up the parent list of the destination state for an active
+             * state. Use a do while() loop as the destState must always be entered
+             * even if it is active. This can happen if we are exiting/entering
+             * the current state.
+             */
+            mTempStateStackCount = 0;
+            StateInfo curStateInfo = mStateInfo.get(destState);
+            do {
+                mTempStateStack[mTempStateStackCount++] = curStateInfo;
+                curStateInfo = curStateInfo.parentStateInfo;
+            } while ((curStateInfo != null) && !curStateInfo.active);
+
+            if (mDbg) {
+                mSm.log("setupTempStateStackWithStatesToEnter: X mTempStateStackCount="
+                        + mTempStateStackCount + ",curStateInfo: " + curStateInfo);
+            }
+            return curStateInfo;
+        }
+
+        /**
+         * Initialize StateStack to mInitialState.
+         */
+        private final void setupInitialStateStack() {
+            if (mDbg) {
+                mSm.log("setupInitialStateStack: E mInitialState=" + mInitialState.getName());
+            }
+
+            StateInfo curStateInfo = mStateInfo.get(mInitialState);
+            for (mTempStateStackCount = 0; curStateInfo != null; mTempStateStackCount++) {
+                mTempStateStack[mTempStateStackCount] = curStateInfo;
+                curStateInfo = curStateInfo.parentStateInfo;
+            }
+
+            // Empty the StateStack
+            mStateStackTopIndex = -1;
+
+            moveTempStateStackToStateStack();
+        }
+
+        /**
+         * @return current message
+         */
+        private final Message getCurrentMessage() {
+            return mMsg;
+        }
+
+        /**
+         * @return current state
+         */
+        private final IState getCurrentState() {
+            return mStateStack[mStateStackTopIndex].state;
+        }
+
+        /**
+         * Add a new state to the state machine. Bottom up addition
+         * of states is allowed but the same state may only exist
+         * in one hierarchy.
+         *
+         * @param state the state to add
+         * @param parent the parent of state
+         * @return stateInfo for this state
+         */
+        private final StateInfo addState(State state, State parent) {
+            if (mDbg) {
+                mSm.log("addStateInternal: E state=" + state.getName() + ",parent="
+                        + ((parent == null) ? "" : parent.getName()));
+            }
+            StateInfo parentStateInfo = null;
+            if (parent != null) {
+                parentStateInfo = mStateInfo.get(parent);
+                if (parentStateInfo == null) {
+                    // Recursively add our parent as it's not been added yet.
+                    parentStateInfo = addState(parent, null);
+                }
+            }
+            StateInfo stateInfo = mStateInfo.get(state);
+            if (stateInfo == null) {
+                stateInfo = new StateInfo();
+                mStateInfo.put(state, stateInfo);
+            }
+
+            // Validate that we aren't adding the same state in two different hierarchies.
+            if ((stateInfo.parentStateInfo != null)
+                    && (stateInfo.parentStateInfo != parentStateInfo)) {
+                throw new RuntimeException("state already added");
+            }
+            stateInfo.state = state;
+            stateInfo.parentStateInfo = parentStateInfo;
+            stateInfo.active = false;
+            if (mDbg) mSm.log("addStateInternal: X stateInfo: " + stateInfo);
+            return stateInfo;
+        }
+
+        /**
+         * Remove a state from the state machine. Will not remove the state if it is currently
+         * active or if it has any children in the hierarchy.
+         * @param state the state to remove
+         */
+        private void removeState(State state) {
+            StateInfo stateInfo = mStateInfo.get(state);
+            if (stateInfo == null || stateInfo.active) {
+                return;
+            }
+            boolean isParent = mStateInfo.values().stream()
+                    .filter(si -> si.parentStateInfo == stateInfo)
+                    .findAny()
+                    .isPresent();
+            if (isParent) {
+                return;
+            }
+            mStateInfo.remove(state);
+        }
+
+        /**
+         * Constructor
+         *
+         * @param looper for dispatching messages
+         * @param sm the hierarchical state machine
+         */
+        private SmHandler(Looper looper, StateMachine sm) {
+            super(looper);
+            mSm = sm;
+
+            addState(mHaltingState, null);
+            addState(mQuittingState, null);
+        }
+
+        /** @see StateMachine#setInitialState(State) */
+        private final void setInitialState(State initialState) {
+            if (mDbg) mSm.log("setInitialState: initialState=" + initialState.getName());
+            mInitialState = initialState;
+        }
+
+        /** @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);
+            }
+            mDestState = (State) destState;
+            if (mDbg) mSm.log("transitionTo: destState=" + mDestState.getName());
+        }
+
+        /** @see StateMachine#deferMessage(Message) */
+        private final void deferMessage(Message msg) {
+            if (mDbg) mSm.log("deferMessage: msg=" + msg.what);
+
+            /* Copy the "msg" to "newMsg" as "msg" will be recycled */
+            Message newMsg = obtainMessage();
+            newMsg.copyFrom(msg);
+
+            mDeferredMessages.add(newMsg);
+        }
+
+        /** @see StateMachine#quit() */
+        private final void quit() {
+            if (mDbg) mSm.log("quit:");
+            sendMessage(obtainMessage(SM_QUIT_CMD, sSmHandlerObj));
+        }
+
+        /** @see StateMachine#quitNow() */
+        private final void quitNow() {
+            if (mDbg) mSm.log("quitNow:");
+            sendMessageAtFrontOfQueue(obtainMessage(SM_QUIT_CMD, sSmHandlerObj));
+        }
+
+        /** Validate that the message was sent by quit or quitNow. */
+        private final boolean isQuit(Message msg) {
+            return (msg.what == SM_QUIT_CMD) && (msg.obj == sSmHandlerObj);
+        }
+
+        /** @see StateMachine#isDbg() */
+        private final boolean isDbg() {
+            return mDbg;
+        }
+
+        /** @see StateMachine#setDbg(boolean) */
+        private final void setDbg(boolean dbg) {
+            mDbg = dbg;
+        }
+
+    }
+
+    private SmHandler mSmHandler;
+    private HandlerThread mSmThread;
+
+    /**
+     * Initialize.
+     *
+     * @param looper for this state machine
+     * @param name of the state machine
+     */
+    private void initStateMachine(String name, Looper looper) {
+        mName = name;
+        mSmHandler = new SmHandler(looper, this);
+    }
+
+    /**
+     * Constructor creates a StateMachine with its own thread.
+     *
+     * @param name of the state machine
+     */
+    @UnsupportedAppUsage
+    protected StateMachine(String name) {
+        mSmThread = new HandlerThread(name);
+        mSmThread.start();
+        Looper looper = mSmThread.getLooper();
+
+        initStateMachine(name, looper);
+    }
+
+    /**
+     * Constructor creates a StateMachine using the looper.
+     *
+     * @param name of the state machine
+     */
+    @UnsupportedAppUsage
+    protected StateMachine(String name, Looper looper) {
+        initStateMachine(name, looper);
+    }
+
+    /**
+     * Constructor creates a StateMachine using the handler.
+     *
+     * @param name of the state machine
+     */
+    @UnsupportedAppUsage
+    protected StateMachine(String name, Handler handler) {
+        initStateMachine(name, handler.getLooper());
+    }
+
+    /**
+     * Notifies subclass that the StateMachine handler is about to process the Message msg
+     * @param msg The message that is being handled
+     */
+    protected void onPreHandleMessage(Message msg) {
+    }
+
+    /**
+     * Notifies subclass that the StateMachine handler has finished processing the Message msg and
+     * has possibly transitioned to a new state.
+     * @param msg The message that is being handled
+     */
+    protected void onPostHandleMessage(Message msg) {
+    }
+
+    /**
+     * Add a new state to the state machine
+     * @param state the state to add
+     * @param parent the parent of state
+     */
+    public final void addState(State state, State parent) {
+        mSmHandler.addState(state, parent);
+    }
+
+    /**
+     * Add a new state to the state machine, parent will be null
+     * @param state to add
+     */
+    @UnsupportedAppUsage
+    public final void addState(State state) {
+        mSmHandler.addState(state, null);
+    }
+
+    /**
+     * Removes a state from the state machine, unless it is currently active or if it has children.
+     * @param state state to remove
+     */
+    public final void removeState(State state) {
+        mSmHandler.removeState(state);
+    }
+
+    /**
+     * Set the initial state. This must be invoked before
+     * and messages are sent to the state machine.
+     *
+     * @param initialState is the state which will receive the first message.
+     */
+    @UnsupportedAppUsage
+    public final void setInitialState(State initialState) {
+        mSmHandler.setInitialState(initialState);
+    }
+
+    /**
+     * @return current message
+     */
+    public final Message getCurrentMessage() {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return null;
+        return smh.getCurrentMessage();
+    }
+
+    /**
+     * @return current state
+     */
+    public final IState getCurrentState() {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return null;
+        return smh.getCurrentState();
+    }
+
+    /**
+     * transition to destination state. Upon returning
+     * from processMessage the current state's exit will
+     * be executed and upon the next message arriving
+     * destState.enter will be invoked.
+     *
+     * this function can also be called inside the enter function of the
+     * previous transition target, but the behavior is undefined when it is
+     * called mid-way through a previous transition (for example, calling this
+     * in the enter() routine of a intermediate node when the current transition
+     * target is one of the nodes descendants).
+     *
+     * @param destState will be the state that receives the next message.
+     */
+    @UnsupportedAppUsage
+    public final void transitionTo(IState destState) {
+        mSmHandler.transitionTo(destState);
+    }
+
+    /**
+     * transition to halt state. Upon returning
+     * from processMessage we will exit all current
+     * states, execute the onHalting() method and then
+     * for all subsequent messages haltedProcessMessage
+     * will be called.
+     */
+    public final void transitionToHaltingState() {
+        mSmHandler.transitionTo(mSmHandler.mHaltingState);
+    }
+
+    /**
+     * Defer this message until next state transition.
+     * Upon transitioning all deferred messages will be
+     * placed on the queue and reprocessed in the original
+     * order. (i.e. The next state the oldest messages will
+     * be processed first)
+     *
+     * @param msg is deferred until the next transition.
+     */
+    public final void deferMessage(Message msg) {
+        mSmHandler.deferMessage(msg);
+    }
+
+    /**
+     * Called when message wasn't handled
+     *
+     * @param msg that couldn't be handled.
+     */
+    protected void unhandledMessage(Message msg) {
+        if (mSmHandler.mDbg) loge(" - unhandledMessage: msg.what=" + msg.what);
+    }
+
+    /**
+     * Called for any message that is received after
+     * transitionToHalting is called.
+     */
+    protected void haltedProcessMessage(Message msg) {
+    }
+
+    /**
+     * This will be called once after handling a message that called
+     * transitionToHalting. All subsequent messages will invoke
+     * {@link StateMachine#haltedProcessMessage(Message)}
+     */
+    protected void onHalting() {
+    }
+
+    /**
+     * This will be called once after a quit message that was NOT handled by
+     * the derived StateMachine. The StateMachine will stop and any subsequent messages will be
+     * ignored. In addition, if this StateMachine created the thread, the thread will
+     * be stopped after this method returns.
+     */
+    protected void onQuitting() {
+    }
+
+    /**
+     * @return the name
+     */
+    public final String getName() {
+        return mName;
+    }
+
+    /**
+     * Set number of log records to maintain and clears all current records.
+     *
+     * @param maxSize number of messages to maintain at anyone time.
+     */
+    public final void setLogRecSize(int maxSize) {
+        mSmHandler.mLogRecords.setSize(maxSize);
+    }
+
+    /**
+     * Set to log only messages that cause a state transition
+     *
+     * @param enable {@code true} to enable, {@code false} to disable
+     */
+    public final void setLogOnlyTransitions(boolean enable) {
+        mSmHandler.mLogRecords.setLogOnlyTransitions(enable);
+    }
+
+    /**
+     * @return the number of log records currently readable
+     */
+    public final int getLogRecSize() {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return 0;
+        return smh.mLogRecords.size();
+    }
+
+    /**
+     * @return the number of log records we can store
+     */
+    @VisibleForTesting
+    public final int getLogRecMaxSize() {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return 0;
+        return smh.mLogRecords.mMaxSize;
+    }
+
+    /**
+     * @return the total number of records processed
+     */
+    public final int getLogRecCount() {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return 0;
+        return smh.mLogRecords.count();
+    }
+
+    /**
+     * @return a log record, or null if index is out of range
+     */
+    public final LogRec getLogRec(int index) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return null;
+        return smh.mLogRecords.get(index);
+    }
+
+    /**
+     * @return a copy of LogRecs as a collection
+     */
+    public final Collection<LogRec> copyLogRecs() {
+        Vector<LogRec> vlr = new Vector<LogRec>();
+        SmHandler smh = mSmHandler;
+        if (smh != null) {
+            for (LogRec lr : smh.mLogRecords.mLogRecVector) {
+                vlr.add(lr);
+            }
+        }
+        return vlr;
+    }
+
+    /**
+     * Add the string to LogRecords.
+     *
+     * @param string
+     */
+    public void addLogRec(String string) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+        smh.mLogRecords.add(this, smh.getCurrentMessage(), string, smh.getCurrentState(),
+                smh.mStateStack[smh.mStateStackTopIndex].state, smh.mDestState);
+    }
+
+    /**
+     * @return true if msg should be saved in the log, default is true.
+     */
+    protected boolean recordLogRec(Message msg) {
+        return true;
+    }
+
+    /**
+     * Return a string to be logged by LogRec, default
+     * is an empty string. Override if additional information is desired.
+     *
+     * @param msg that was processed
+     * @return information to be logged as a String
+     */
+    protected String getLogRecString(Message msg) {
+        return "";
+    }
+
+    /**
+     * @return the string for msg.what
+     */
+    protected String getWhatToString(int what) {
+        return null;
+    }
+
+    /**
+     * @return Handler, maybe null if state machine has quit.
+     */
+    public final Handler getHandler() {
+        return mSmHandler;
+    }
+
+    /**
+     * Get a message and set Message.target state machine handler.
+     *
+     * Note: The handler can be null if the state machine has quit,
+     * which means target will be null and may cause a AndroidRuntimeException
+     * in MessageQueue#enqueMessage if sent directly or if sent using
+     * StateMachine#sendMessage the message will just be ignored.
+     *
+     * @return  A Message object from the global pool
+     */
+    public final Message obtainMessage() {
+        return Message.obtain(mSmHandler);
+    }
+
+    /**
+     * Get a message and set Message.target state machine handler, what.
+     *
+     * Note: The handler can be null if the state machine has quit,
+     * which means target will be null and may cause a AndroidRuntimeException
+     * in MessageQueue#enqueMessage if sent directly or if sent using
+     * StateMachine#sendMessage the message will just be ignored.
+     *
+     * @param what is the assigned to Message.what.
+     * @return  A Message object from the global pool
+     */
+    public final Message obtainMessage(int what) {
+        return Message.obtain(mSmHandler, what);
+    }
+
+    /**
+     * Get a message and set Message.target state machine handler,
+     * what and obj.
+     *
+     * Note: The handler can be null if the state machine has quit,
+     * which means target will be null and may cause a AndroidRuntimeException
+     * in MessageQueue#enqueMessage if sent directly or if sent using
+     * StateMachine#sendMessage the message will just be ignored.
+     *
+     * @param what is the assigned to Message.what.
+     * @param obj is assigned to Message.obj.
+     * @return  A Message object from the global pool
+     */
+    public final Message obtainMessage(int what, Object obj) {
+        return Message.obtain(mSmHandler, what, obj);
+    }
+
+    /**
+     * Get a message and set Message.target state machine handler,
+     * what, arg1 and arg2
+     *
+     * Note: The handler can be null if the state machine has quit,
+     * which means target will be null and may cause a AndroidRuntimeException
+     * in MessageQueue#enqueMessage if sent directly or if sent using
+     * StateMachine#sendMessage the message will just be ignored.
+     *
+     * @param what  is assigned to Message.what
+     * @param arg1  is assigned to Message.arg1
+     * @return  A Message object from the global pool
+     */
+    public final Message obtainMessage(int what, int arg1) {
+        // use this obtain so we don't match the obtain(h, what, Object) method
+        return Message.obtain(mSmHandler, what, arg1, 0);
+    }
+
+    /**
+     * Get a message and set Message.target state machine handler,
+     * what, arg1 and arg2
+     *
+     * Note: The handler can be null if the state machine has quit,
+     * which means target will be null and may cause a AndroidRuntimeException
+     * in MessageQueue#enqueMessage if sent directly or if sent using
+     * StateMachine#sendMessage the message will just be ignored.
+     *
+     * @param what  is assigned to Message.what
+     * @param arg1  is assigned to Message.arg1
+     * @param arg2  is assigned to Message.arg2
+     * @return  A Message object from the global pool
+     */
+    @UnsupportedAppUsage
+    public final Message obtainMessage(int what, int arg1, int arg2) {
+        return Message.obtain(mSmHandler, what, arg1, arg2);
+    }
+
+    /**
+     * Get a message and set Message.target state machine handler,
+     * what, arg1, arg2 and obj
+     *
+     * Note: The handler can be null if the state machine has quit,
+     * which means target will be null and may cause a AndroidRuntimeException
+     * in MessageQueue#enqueMessage if sent directly or if sent using
+     * StateMachine#sendMessage the message will just be ignored.
+     *
+     * @param what  is assigned to Message.what
+     * @param arg1  is assigned to Message.arg1
+     * @param arg2  is assigned to Message.arg2
+     * @param obj is assigned to Message.obj
+     * @return  A Message object from the global pool
+     */
+    @UnsupportedAppUsage
+    public final Message obtainMessage(int what, int arg1, int arg2, Object obj) {
+        return Message.obtain(mSmHandler, what, arg1, arg2, obj);
+    }
+
+    /**
+     * Enqueue a message to this state machine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    @UnsupportedAppUsage
+    public void sendMessage(int what) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessage(obtainMessage(what));
+    }
+
+    /**
+     * Enqueue a message to this state machine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    @UnsupportedAppUsage
+    public void sendMessage(int what, Object obj) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessage(obtainMessage(what, obj));
+    }
+
+    /**
+     * Enqueue a message to this state machine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    @UnsupportedAppUsage
+    public void sendMessage(int what, int arg1) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessage(obtainMessage(what, arg1));
+    }
+
+    /**
+     * Enqueue a message to this state machine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessage(int what, int arg1, int arg2) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessage(obtainMessage(what, arg1, arg2));
+    }
+
+    /**
+     * Enqueue a message to this state machine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    @UnsupportedAppUsage
+    public void sendMessage(int what, int arg1, int arg2, Object obj) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessage(obtainMessage(what, arg1, arg2, obj));
+    }
+
+    /**
+     * Enqueue a message to this state machine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    @UnsupportedAppUsage
+    public void sendMessage(Message msg) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessage(msg);
+    }
+
+    /**
+     * Enqueue a message to this state machine after a delay.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessageDelayed(int what, long delayMillis) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageDelayed(obtainMessage(what), delayMillis);
+    }
+
+    /**
+     * Enqueue a message to this state machine after a delay.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessageDelayed(int what, Object obj, long delayMillis) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageDelayed(obtainMessage(what, obj), delayMillis);
+    }
+
+    /**
+     * Enqueue a message to this state machine after a delay.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessageDelayed(int what, int arg1, long delayMillis) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageDelayed(obtainMessage(what, arg1), delayMillis);
+    }
+
+    /**
+     * Enqueue a message to this state machine after a delay.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessageDelayed(int what, int arg1, int arg2, long delayMillis) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageDelayed(obtainMessage(what, arg1, arg2), delayMillis);
+    }
+
+    /**
+     * Enqueue a message to this state machine after a delay.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessageDelayed(int what, int arg1, int arg2, Object obj,
+            long delayMillis) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageDelayed(obtainMessage(what, arg1, arg2, obj), delayMillis);
+    }
+
+    /**
+     * Enqueue a message to this state machine after a delay.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    public void sendMessageDelayed(Message msg, long delayMillis) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageDelayed(msg, delayMillis);
+    }
+
+    /**
+     * Enqueue a message to the front of the queue for this state machine.
+     * Protected, may only be called by instances of StateMachine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    protected final void sendMessageAtFrontOfQueue(int what) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageAtFrontOfQueue(obtainMessage(what));
+    }
+
+    /**
+     * Enqueue a message to the front of the queue for this state machine.
+     * Protected, may only be called by instances of StateMachine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    protected final void sendMessageAtFrontOfQueue(int what, Object obj) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageAtFrontOfQueue(obtainMessage(what, obj));
+    }
+
+    /**
+     * Enqueue a message to the front of the queue for this state machine.
+     * Protected, may only be called by instances of StateMachine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    protected final void sendMessageAtFrontOfQueue(int what, int arg1) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageAtFrontOfQueue(obtainMessage(what, arg1));
+    }
+
+
+    /**
+     * Enqueue a message to the front of the queue for this state machine.
+     * Protected, may only be called by instances of StateMachine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    protected final void sendMessageAtFrontOfQueue(int what, int arg1, int arg2) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageAtFrontOfQueue(obtainMessage(what, arg1, arg2));
+    }
+
+    /**
+     * Enqueue a message to the front of the queue for this state machine.
+     * Protected, may only be called by instances of StateMachine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    protected final void sendMessageAtFrontOfQueue(int what, int arg1, int arg2, Object obj) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageAtFrontOfQueue(obtainMessage(what, arg1, arg2, obj));
+    }
+
+    /**
+     * Enqueue a message to the front of the queue for this state machine.
+     * Protected, may only be called by instances of StateMachine.
+     *
+     * Message is ignored if state machine has quit.
+     */
+    protected final void sendMessageAtFrontOfQueue(Message msg) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.sendMessageAtFrontOfQueue(msg);
+    }
+
+    /**
+     * Removes a message from the message queue.
+     * Protected, may only be called by instances of StateMachine.
+     */
+    protected final void removeMessages(int what) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.removeMessages(what);
+    }
+
+    /**
+     * Removes a message from the deferred messages queue.
+     */
+    protected final void removeDeferredMessages(int what) {
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        Iterator<Message> iterator = smh.mDeferredMessages.iterator();
+        while (iterator.hasNext()) {
+            Message msg = iterator.next();
+            if (msg.what == what) iterator.remove();
+        }
+    }
+
+    /**
+     * Check if there are any pending messages with code 'what' in deferred messages queue.
+     */
+    protected final boolean hasDeferredMessages(int what) {
+        SmHandler smh = mSmHandler;
+        if (smh == null) return false;
+
+        Iterator<Message> iterator = smh.mDeferredMessages.iterator();
+        while (iterator.hasNext()) {
+            Message msg = iterator.next();
+            if (msg.what == what) return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if there are any pending posts of messages with code 'what' in
+     * the message queue. This does NOT check messages in deferred message queue.
+     */
+    protected final boolean hasMessages(int what) {
+        SmHandler smh = mSmHandler;
+        if (smh == null) return false;
+
+        return smh.hasMessages(what);
+    }
+
+    /**
+     * Validate that the message was sent by
+     * {@link StateMachine#quit} or {@link StateMachine#quitNow}.
+     */
+    protected final boolean isQuit(Message msg) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return msg.what == SM_QUIT_CMD;
+
+        return smh.isQuit(msg);
+    }
+
+    /**
+     * Quit the state machine after all currently queued up messages are processed.
+     */
+    public final void quit() {
+        // mSmHandler can be null if the state machine is already stopped.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.quit();
+    }
+
+    /**
+     * Quit the state machine immediately all currently queued messages will be discarded.
+     */
+    public final void quitNow() {
+        // mSmHandler can be null if the state machine is already stopped.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.quitNow();
+    }
+
+    /**
+     * @return if debugging is enabled
+     */
+    public boolean isDbg() {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return false;
+
+        return smh.isDbg();
+    }
+
+    /**
+     * Set debug enable/disabled.
+     *
+     * @param dbg is true to enable debugging.
+     */
+    public void setDbg(boolean dbg) {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        smh.setDbg(dbg);
+    }
+
+    /**
+     * Start the state machine.
+     */
+    @UnsupportedAppUsage
+    public void start() {
+        // mSmHandler can be null if the state machine has quit.
+        SmHandler smh = mSmHandler;
+        if (smh == null) return;
+
+        /** Send the complete construction message */
+        smh.completeConstruction();
+    }
+
+    /**
+     * Dump the current state.
+     *
+     * @param fd
+     * @param pw
+     * @param args
+     */
+    @UnsupportedAppUsage
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println(getName() + ":");
+        pw.println(" total records=" + getLogRecCount());
+        for (int i = 0; i < getLogRecSize(); i++) {
+            pw.println(" rec[" + i + "]: " + getLogRec(i).toString());
+            pw.flush();
+        }
+        pw.println("curState=" + getCurrentState().getName());
+    }
+
+    @Override
+    public String toString() {
+        String name = "(null)";
+        String state = "(null)";
+        try {
+            name = mName.toString();
+            state = mSmHandler.getCurrentState().getName().toString();
+        } catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
+            // Will use default(s) initialized above.
+        }
+        return "name=" + name + " state=" + state;
+    }
+
+    /**
+     * Log with debug and add to the LogRecords.
+     *
+     * @param s is string log
+     */
+    protected void logAndAddLogRec(String s) {
+        addLogRec(s);
+        log(s);
+    }
+
+    /**
+     * Log with debug
+     *
+     * @param s is string log
+     */
+    protected void log(String s) {
+        Log.d(mName, s);
+    }
+
+    /**
+     * Log with debug attribute
+     *
+     * @param s is string log
+     */
+    protected void logd(String s) {
+        Log.d(mName, s);
+    }
+
+    /**
+     * Log with verbose attribute
+     *
+     * @param s is string log
+     */
+    protected void logv(String s) {
+        Log.v(mName, s);
+    }
+
+    /**
+     * Log with info attribute
+     *
+     * @param s is string log
+     */
+    protected void logi(String s) {
+        Log.i(mName, s);
+    }
+
+    /**
+     * Log with warning attribute
+     *
+     * @param s is string log
+     */
+    protected void logw(String s) {
+        Log.w(mName, s);
+    }
+
+    /**
+     * Log with error attribute
+     *
+     * @param s is string log
+     */
+    protected void loge(String s) {
+        Log.e(mName, s);
+    }
+
+    /**
+     * Log with error attribute
+     *
+     * @param s is string log
+     * @param e is a Throwable which logs additional information.
+     */
+    protected void loge(String s, Throwable e) {
+        Log.e(mName, s, e);
+    }
+}
diff --git a/src/com/android/cellbroadcastservice/UserData.java b/src/com/android/cellbroadcastservice/UserData.java
new file mode 100644
index 0000000..8758d8d
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/UserData.java
@@ -0,0 +1,166 @@
+/*
+ * 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.util.SparseIntArray;
+
+import com.android.internal.telephony.SmsHeader;
+import com.android.internal.util.HexDump;
+
+import dalvik.annotation.compat.UnsupportedAppUsage;
+
+public class UserData {
+
+    /**
+     * 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;
+
+    /**
+     * User data message type encoding types.
+     * (See 3GPP2 C.S0015-B, 4.5.2 and 3GPP 23.038, Section 4)
+     */
+    public static final int ENCODING_GSM_DCS_7BIT               = 0x00;
+    public static final int ENCODING_GSM_DCS_8BIT               = 0x01;
+    public static final int ENCODING_GSM_DCS_16BIT              = 0x02;
+
+    /**
+     * 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;
+
+    /**
+     * US ASCII character mapping table.
+     *
+     * This table contains only the printable ASCII characters, with a
+     * 0x20 offset, meaning that the ASCII SPACE character is at index
+     * 0, with the resulting code of 0x20.
+     *
+     * Note this mapping is also equivalent to that used by both the
+     * IA5 and the IS-91 encodings.  For the former this is defined
+     * using CCITT Rec. T.50 Tables 1 and 3.  For the latter IS 637 B,
+     * Table 4.3.1.4.1-1 -- and note the encoding uses only 6 bits,
+     * and hence only maps entries up to the '_' character.
+     *
+     */
+    public static final char[] ASCII_MAP = {
+        ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/',
+        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',
+        '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
+        'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_',
+        '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
+        'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~'};
+
+    /**
+     * Character to use when forced to encode otherwise unencodable
+     * characters, meaning those not in the respective ASCII or GSM
+     * 7-bit encoding tables.  Current choice is SPACE, which is 0x20
+     * in both the GSM-7bit and ASCII-7bit encodings.
+     */
+    static final byte UNENCODABLE_7_BIT_CHAR = 0x20;
+
+    /**
+     * Only elements between these indices in the ASCII table are printable.
+     */
+    public static final int PRINTABLE_ASCII_MIN_INDEX = 0x20;
+    public static final int ASCII_NL_INDEX = 0x0A;
+    public static final int ASCII_CR_INDEX = 0x0D;
+    @UnsupportedAppUsage
+    public static final SparseIntArray charToAscii = new SparseIntArray();
+    static {
+        for (int i = 0; i < ASCII_MAP.length; i++) {
+            charToAscii.put(ASCII_MAP[i], PRINTABLE_ASCII_MIN_INDEX + i);
+        }
+        charToAscii.put('\n', ASCII_NL_INDEX);
+        charToAscii.put('\r', ASCII_CR_INDEX);
+    }
+
+    @UnsupportedAppUsage
+    public UserData() {
+    }
+
+    /**
+     * Mapping for ASCII values less than 32 are flow control signals
+     * and not used here.
+     */
+    public static final int ASCII_MAP_BASE_INDEX = 0x20;
+    public static final int ASCII_MAP_MAX_INDEX = ASCII_MAP_BASE_INDEX + ASCII_MAP.length - 1;
+
+    /**
+     * Contains the data header of the user data
+     */
+    @UnsupportedAppUsage
+    public SmsHeader userDataHeader;
+
+    /**
+     * Contains the data encoding type for the SMS message
+     */
+    @UnsupportedAppUsage
+    public int msgEncoding;
+    @UnsupportedAppUsage
+    public boolean msgEncodingSet = false;
+
+    public int msgType;
+
+    /**
+     * Number of invalid bits in the last byte of data.
+     */
+    public int paddingBits;
+
+    @UnsupportedAppUsage
+    public int numFields;
+
+    /**
+     * Contains the user data of a SMS message
+     * (See 3GPP2 C.S0015-B, v2, 4.5.2)
+     */
+    @UnsupportedAppUsage
+    public byte[] payload;
+    @UnsupportedAppUsage
+    public String payloadStr;
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("UserData ");
+        builder.append("{ msgEncoding=" + (msgEncodingSet ? msgEncoding : "unset"));
+        builder.append(", msgType=" + msgType);
+        builder.append(", paddingBits=" + paddingBits);
+        builder.append(", numFields=" + numFields);
+        builder.append(", userDataHeader=" + userDataHeader);
+        builder.append(", payload='" + HexDump.toHexString(payload) + "'");
+        builder.append(", payloadStr='" + payloadStr + "'");
+        builder.append(" }");
+        return builder.toString();
+    }
+
+}
diff --git a/src/com/android/cellbroadcastservice/WakeLockStateMachine.java b/src/com/android/cellbroadcastservice/WakeLockStateMachine.java
new file mode 100644
index 0000000..99b935c
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/WakeLockStateMachine.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cellbroadcastservice;
+
+import android.annotation.UnsupportedAppUsage;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Message;
+import android.os.PowerManager;
+import android.util.Log;
+
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Generic state machine for handling messages and waiting for ordered broadcasts to complete.
+ * Subclasses implement {@link #handleSmsMessage}, which returns true to transition into waiting
+ * state, or false to remain in idle state. The wakelock is acquired on exit from idle state,
+ * and is released a few seconds after returning to idle state, or immediately upon calling
+ * {@link #quit}.
+ */
+public abstract class WakeLockStateMachine extends StateMachine {
+    protected static final boolean DBG = Build.IS_DEBUGGABLE;
+
+    private final PowerManager.WakeLock mWakeLock;
+
+    /** New message to process. */
+    public static final int EVENT_NEW_SMS_MESSAGE = 1;
+
+    /** Result receiver called for current cell broadcast. */
+    protected static final int EVENT_BROADCAST_COMPLETE = 2;
+
+    /** Release wakelock after a short timeout when returning to idle state. */
+    static final int EVENT_RELEASE_WAKE_LOCK = 3;
+
+    @UnsupportedAppUsage
+    protected Context mContext;
+
+    protected AtomicInteger mReceiverCount = new AtomicInteger(0);
+
+    /** Wakelock release delay when returning to idle state. */
+    private static final int WAKE_LOCK_TIMEOUT = 3000;
+
+    private final DefaultState mDefaultState = new DefaultState();
+    @UnsupportedAppUsage
+    private final IdleState mIdleState = new IdleState();
+    private final WaitingState mWaitingState = new WaitingState();
+
+    protected WakeLockStateMachine(String debugTag, Context context) {
+        super(debugTag);
+
+        mContext = context;
+
+        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, debugTag);
+        // wake lock released after we enter idle state
+        mWakeLock.acquire();
+
+        addState(mDefaultState);
+        addState(mIdleState, mDefaultState);
+        addState(mWaitingState, mDefaultState);
+        setInitialState(mIdleState);
+    }
+
+    private void releaseWakeLock() {
+        if (mWakeLock.isHeld()) {
+            mWakeLock.release();
+        }
+
+        if (mWakeLock.isHeld()) {
+            loge("Wait lock is held after release.");
+        }
+    }
+
+    /**
+     * Tell the state machine to quit after processing all messages.
+     */
+    public final void dispose() {
+        quit();
+    }
+
+    @Override
+    protected void onQuitting() {
+        // fully release the wakelock
+        while (mWakeLock.isHeld()) {
+            mWakeLock.release();
+        }
+    }
+
+    /**
+     * Send a message with the specified object for {@link #handleSmsMessage}.
+     * @param obj the object to pass in the msg.obj field
+     */
+    public final void onCdmaCellBroadcastSms(Object obj) {
+        sendMessage(EVENT_NEW_SMS_MESSAGE, obj);
+    }
+
+    /**
+     * This parent state throws an exception (for debug builds) or prints an error for unhandled
+     * message types.
+     */
+    class DefaultState extends State {
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                default: {
+                    String errorText = "processMessage: unhandled message type " + msg.what;
+                    if (Build.IS_DEBUGGABLE) {
+                        throw new RuntimeException(errorText);
+                    } else {
+                        loge(errorText);
+                    }
+                    break;
+                }
+            }
+            return HANDLED;
+        }
+    }
+
+    /**
+     * Idle state delivers Cell Broadcasts to receivers. It acquires the wakelock, which is
+     * released when the broadcast completes.
+     */
+    class IdleState extends State {
+        @Override
+        public void enter() {
+            sendMessageDelayed(EVENT_RELEASE_WAKE_LOCK, WAKE_LOCK_TIMEOUT);
+        }
+
+        @Override
+        public void exit() {
+            mWakeLock.acquire();
+            if (DBG) log("acquired wakelock, leaving Idle state");
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case EVENT_NEW_SMS_MESSAGE:
+                    // transition to waiting state if we sent a broadcast
+                    if (handleSmsMessage(msg)) {
+                        transitionTo(mWaitingState);
+                    }
+                    return HANDLED;
+
+                case EVENT_RELEASE_WAKE_LOCK:
+                    releaseWakeLock();
+                    return HANDLED;
+
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    /**
+     * Waiting state waits for the result receiver to be called for the current cell broadcast.
+     * In this state, any new cell broadcasts are deferred until we return to Idle state.
+     */
+    class WaitingState extends State {
+        @Override
+        public boolean processMessage(Message msg) {
+            switch (msg.what) {
+                case EVENT_NEW_SMS_MESSAGE:
+                    log("deferring message until return to idle");
+                    deferMessage(msg);
+                    return HANDLED;
+
+                case EVENT_BROADCAST_COMPLETE:
+                    log("broadcast complete, returning to idle");
+                    transitionTo(mIdleState);
+                    return HANDLED;
+
+                case EVENT_RELEASE_WAKE_LOCK:
+                    releaseWakeLock();
+                    return HANDLED;
+
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    /**
+     * Implemented by subclass to handle messages in {@link IdleState}.
+     * @param message the message to process
+     * @return true to transition to {@link WaitingState}; false to stay in {@link IdleState}
+     */
+    protected abstract boolean handleSmsMessage(Message message);
+
+    /**
+     * BroadcastReceiver to send message to return to idle state.
+     */
+    protected final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (mReceiverCount.decrementAndGet() == 0) {
+                sendMessage(EVENT_BROADCAST_COMPLETE);
+            }
+        }
+    };
+
+    /**
+     * Log with debug level.
+     * @param s the string to log
+     */
+    @UnsupportedAppUsage
+    @Override
+    protected void log(String s) {
+        Log.d(getName(), s);
+    }
+
+    /**
+     * Log with error level.
+     * @param s the string to log
+     */
+    @Override
+    protected void loge(String s) {
+        Log.e(getName(), s);
+    }
+
+    /**
+     * Log with error level.
+     * @param s the string to log
+     * @param e is a Throwable which logs additional information.
+     */
+    @Override
+    protected void loge(String s, Throwable e) {
+        Log.e(getName(), s, e);
+    }
+}