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 ----> 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 ---> 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 {
+ @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 {
+ @Override public void enter() {
+ log("mP1.enter");
+ }
+ @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;
+ }
+ @Override public void exit() {
+ log("mP1.exit");
+ }
+ }
+
+ class S1 extends State {
+ @Override public void enter() {
+ log("mS1.enter");
+ }
+ @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;
+ }
+ }
+ @Override public void exit() {
+ log("mS1.exit");
+ }
+ }
+
+ class S2 extends State {
+ @Override public void enter() {
+ log("mS2.enter");
+ }
+ @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;
+ }
+ @Override public void exit() {
+ log("mS2.exit");
+ }
+ }
+
+ class P2 extends State {
+ @Override public void enter() {
+ log("mP2.enter");
+ sendMessage(obtainMessage(CMD_5));
+ }
+ @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;
+ }
+ @Override public void exit() {
+ log("mP2.exit");
+ }
+ }
+
+ @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);
+ }
+}