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