blob: c63452753733b6d8fbab428c5167156185cf08c4 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.cellbroadcastservice;
import android.content.Context;
import android.telephony.SmsCbCmasInfo;
import android.telephony.cdma.CdmaSmsCbProgramData;
import android.util.Log;
/**
* An object to decode CDMA SMS bearer data.
*/
public final class BearerData {
private final static String LOG_TAG = "BearerData";
/**
* Bearer Data Subparameter Identifiers
* (See 3GPP2 C.S0015-B, v2.0, table 4.5-1)
* NOTE: Unneeded subparameter types are not included
*/
private 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 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;
/**
* Language Indicator values. NOTE: the spec (3GPP2 C.S0015-B,
* v2, 4.5.14) is ambiguous as to the meaning of this field, as it
* refers to C.R1001-D but that reference has been crossed out.
* It would seem reasonable to assume the values from C.R1001-F
* (table 9.2-1) are to be used instead.
*/
public static final int LANGUAGE_UNKNOWN = 0x00;
public static final int LANGUAGE_ENGLISH = 0x01;
public static final int LANGUAGE_FRENCH = 0x02;
public static final int LANGUAGE_SPANISH = 0x03;
public static final int LANGUAGE_JAPANESE = 0x04;
public static final int LANGUAGE_KOREAN = 0x05;
public static final int LANGUAGE_CHINESE = 0x06;
public static final int LANGUAGE_HEBREW = 0x07;
/**
* Supported message types for CDMA SMS messages
* (See 3GPP2 C.S0015-B, v2.0, table 4.5.1-1)
* Used for CdmaSmsCbTest.
*/
public static final int MESSAGE_TYPE_DELIVER = 0x01;
/**
* 16-bit value indicating the message ID, which increments modulo 65536.
* (Special rules apply for WAP-messages.)
* (See 3GPP2 C.S0015-B, v2, 4.5.1)
*/
public int messageId;
/**
* Priority modes for CDMA SMS message (See 3GPP2 C.S0015-B, v2.0, table 4.5.9-1)
*/
public int priority = PRIORITY_NORMAL;
/**
* Language indicator for CDMA SMS message.
*/
public int language = LANGUAGE_UNKNOWN;
/**
* 1-bit value that indicates whether a User Data Header (UDH) is present.
* (See 3GPP2 C.S0015-B, v2, 4.5.1)
*
* NOTE: during encoding, this value will be set based on the
* presence of a UDH in the structured data, any existing setting
* will be overwritten.
*/
public boolean hasUserDataHeader;
/**
* Information on the user data
* (e.g. padding bits, user data, user data header, etc)
* (See 3GPP2 C.S.0015-B, v2, 4.5.2)
*/
public UserData userData;
/**
* CMAS warning notification information.
*
* @see #decodeCmasUserData(BearerData, int)
*/
public SmsCbCmasInfo cmasWarningInfo;
/**
* Construct an empty BearerData.
*/
private BearerData() {
}
private static class CodingException extends Exception {
public CodingException(String s) {
super(s);
}
}
/**
* Returns the language indicator as a two-character ISO 639 string.
*
* @return a two character ISO 639 language code
*/
public String getLanguage() {
return getLanguageCodeForValue(language);
}
/**
* Converts a CDMA language indicator value to an ISO 639 two character language code.
*
* @param languageValue the CDMA language value to convert
* @return the two character ISO 639 language code for the specified value, or null if unknown
*/
private static String getLanguageCodeForValue(int languageValue) {
switch (languageValue) {
case LANGUAGE_ENGLISH:
return "en";
case LANGUAGE_FRENCH:
return "fr";
case LANGUAGE_SPANISH:
return "es";
case LANGUAGE_JAPANESE:
return "ja";
case LANGUAGE_KOREAN:
return "ko";
case LANGUAGE_CHINESE:
return "zh";
case LANGUAGE_HEBREW:
return "he";
default:
return null;
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("BearerData ");
builder.append(", messageId=" + messageId);
builder.append(", hasUserDataHeader=" + hasUserDataHeader);
builder.append(", userData=" + userData);
builder.append(" }");
return builder.toString();
}
private static boolean decodeMessageId(BearerData bData, BitwiseInputStream inStream)
throws BitwiseInputStream.AccessException {
final int EXPECTED_PARAM_SIZE = 3 * 8;
boolean decodeSuccess = false;
int paramBits = inStream.read(8) * 8;
if (paramBits >= EXPECTED_PARAM_SIZE) {
paramBits -= EXPECTED_PARAM_SIZE;
decodeSuccess = true;
inStream.skip(4); // skip messageType
bData.messageId = inStream.read(8) << 8;
bData.messageId |= inStream.read(8);
bData.hasUserDataHeader = (inStream.read(1) == 1);
inStream.skip(3);
}
if ((!decodeSuccess) || (paramBits > 0)) {
Log.d(LOG_TAG, "MESSAGE_IDENTIFIER decode "
+ (decodeSuccess ? "succeeded" : "failed")
+ " (extra bits = " + paramBits + ")");
}
inStream.skip(paramBits);
return decodeSuccess;
}
private static boolean decodeReserved(BitwiseInputStream inStream, int subparamId)
throws BitwiseInputStream.AccessException, CodingException {
boolean decodeSuccess = false;
int subparamLen = inStream.read(8); // SUBPARAM_LEN
int paramBits = subparamLen * 8;
if (paramBits <= inStream.available()) {
decodeSuccess = true;
inStream.skip(paramBits);
}
Log.d(LOG_TAG, "RESERVED bearer data subparameter " + subparamId + " decode "
+ (decodeSuccess ? "succeeded" : "failed") + " (param bits = " + paramBits + ")");
if (!decodeSuccess) {
throw new CodingException("RESERVED bearer data subparameter " + subparamId
+ " had invalid SUBPARAM_LEN " + subparamLen);
}
return decodeSuccess;
}
private static boolean decodeUserData(BearerData bData, BitwiseInputStream inStream)
throws BitwiseInputStream.AccessException {
int paramBits = inStream.read(8) * 8;
bData.userData = new UserData();
bData.userData.msgEncoding = inStream.read(5);
bData.userData.msgEncodingSet = true;
bData.userData.msgType = 0;
int consumedBits = 5;
if ((bData.userData.msgEncoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) ||
(bData.userData.msgEncoding == UserData.ENCODING_GSM_DCS)) {
bData.userData.msgType = inStream.read(8);
consumedBits += 8;
}
bData.userData.numFields = inStream.read(8);
consumedBits += 8;
int dataBits = paramBits - consumedBits;
bData.userData.payload = inStream.readByteArray(dataBits);
return true;
}
private static String decodeUtf8(byte[] data, int offset, int numFields)
throws CodingException {
return decodeCharset(data, offset, numFields, 1, "UTF-8");
}
private static String decodeUtf16(byte[] data, int offset, int numFields)
throws CodingException {
// Subtract header and possible padding byte (at end) from num fields.
int padding = offset % 2;
numFields -= (offset + padding) / 2;
return decodeCharset(data, offset, numFields, 2, "utf-16be");
}
private static String decodeCharset(byte[] data, int offset, int numFields, int width,
String charset) throws CodingException {
if (numFields < 0 || (numFields * width + offset) > data.length) {
// Try to decode the max number of characters in payload
int padding = offset % width;
int maxNumFields = (data.length - offset - padding) / width;
if (maxNumFields < 0) {
throw new CodingException(charset + " decode failed: offset out of range");
}
Log.e(LOG_TAG, charset + " decode error: offset = " + offset + " numFields = "
+ numFields + " data.length = " + data.length + " maxNumFields = "
+ maxNumFields);
numFields = maxNumFields;
}
try {
return new String(data, offset, numFields * width, charset);
} catch (java.io.UnsupportedEncodingException ex) {
throw new CodingException(charset + " decode failed: " + ex);
}
}
private static String decode7bitAscii(byte[] data, int offset, int numFields)
throws CodingException {
try {
int offsetBits = offset * 8;
int offsetSeptets = (offsetBits + 6) / 7;
numFields -= offsetSeptets;
StringBuffer strBuf = new StringBuffer(numFields);
BitwiseInputStream inStream = new BitwiseInputStream(data);
int wantedBits = (offsetSeptets * 7) + (numFields * 7);
if (inStream.available() < wantedBits) {
throw new CodingException("insufficient data (wanted " + wantedBits +
" bits, but only have " + inStream.available() + ")");
}
inStream.skip(offsetSeptets * 7);
for (int i = 0; i < numFields; i++) {
int charCode = inStream.read(7);
if ((charCode >= UserData.ASCII_MAP_BASE_INDEX) &&
(charCode <= UserData.ASCII_MAP_MAX_INDEX)) {
strBuf.append(UserData.ASCII_MAP[charCode - UserData.ASCII_MAP_BASE_INDEX]);
} else if (charCode == UserData.ASCII_NL_INDEX) {
strBuf.append('\n');
} else if (charCode == UserData.ASCII_CR_INDEX) {
strBuf.append('\r');
} else {
/* For other charCodes, they are unprintable, and so simply use SPACE. */
strBuf.append(' ');
}
}
return strBuf.toString();
} catch (BitwiseInputStream.AccessException ex) {
throw new CodingException("7bit ASCII decode failed: " + ex);
}
}
private static String decode7bitGsm(byte[] data, int offset, int numFields)
throws CodingException {
// Start reading from the next 7-bit aligned boundary after offset.
int offsetBits = offset * 8;
int offsetSeptets = (offsetBits + 6) / 7;
numFields -= offsetSeptets;
int paddingBits = (offsetSeptets * 7) - offsetBits;
String result = GsmAlphabet.gsm7BitPackedToString(data, offset, numFields,
paddingBits, 0, 0);
if (result == null) {
throw new CodingException("7bit GSM decoding failed");
}
return result;
}
private static String decodeLatin(byte[] data, int offset, int numFields)
throws CodingException {
return decodeCharset(data, offset, numFields, 1, "ISO-8859-1");
}
private static String decodeShiftJis(byte[] data, int offset, int numFields)
throws CodingException {
return decodeCharset(data, offset, numFields, 1, "Shift_JIS");
}
private static String decodeGsmDcs(byte[] data, int offset, int numFields,
int msgType)
throws CodingException {
if ((msgType & 0xC0) != 0) {
throw new CodingException("unsupported coding group ("
+ msgType + ")");
}
switch ((msgType >> 2) & 0x3) {
case UserData.ENCODING_GSM_DCS_7BIT:
return decode7bitGsm(data, offset, numFields);
case UserData.ENCODING_GSM_DCS_8BIT:
return decodeUtf8(data, offset, numFields);
case UserData.ENCODING_GSM_DCS_16BIT:
return decodeUtf16(data, offset, numFields);
default:
throw new CodingException("unsupported user msgType encoding ("
+ msgType + ")");
}
}
private static void decodeUserDataPayload(Context context, UserData userData,
boolean hasUserDataHeader) throws CodingException {
int offset = 0;
if (hasUserDataHeader) {
int udhLen = userData.payload[0] & 0x00FF;
offset += udhLen + 1;
byte[] headerData = new byte[udhLen];
System.arraycopy(userData.payload, 1, headerData, 0, udhLen);
userData.userDataHeader = SmsHeader.fromByteArray(headerData);
}
switch (userData.msgEncoding) {
case UserData.ENCODING_OCTET:
/*
* Octet decoding depends on the carrier service.
*/
boolean decodingtypeUTF8 = context.getResources()
.getBoolean(R.bool.config_sms_utf8_support);
// Strip off any padding bytes, meaning any differences between the length of the
// array and the target length specified by numFields. This is to avoid any
// confusion by code elsewhere that only considers the payload array length.
byte[] payload = new byte[userData.numFields];
int copyLen = userData.numFields < userData.payload.length
? userData.numFields : userData.payload.length;
System.arraycopy(userData.payload, 0, payload, 0, copyLen);
userData.payload = payload;
if (!decodingtypeUTF8) {
// There are many devices in the market that send 8bit text sms (latin
// encoded) as
// octet encoded.
userData.payloadStr = decodeLatin(userData.payload, offset, userData.numFields);
} else {
userData.payloadStr = decodeUtf8(userData.payload, offset, userData.numFields);
}
break;
case UserData.ENCODING_IA5:
case UserData.ENCODING_7BIT_ASCII:
userData.payloadStr = decode7bitAscii(userData.payload, offset, userData.numFields);
break;
case UserData.ENCODING_UNICODE_16:
userData.payloadStr = decodeUtf16(userData.payload, offset, userData.numFields);
break;
case UserData.ENCODING_GSM_7BIT_ALPHABET:
userData.payloadStr = decode7bitGsm(userData.payload, offset,
userData.numFields);
break;
case UserData.ENCODING_LATIN:
userData.payloadStr = decodeLatin(userData.payload, offset, userData.numFields);
break;
case UserData.ENCODING_SHIFT_JIS:
userData.payloadStr = decodeShiftJis(userData.payload, offset, userData.numFields);
break;
case UserData.ENCODING_GSM_DCS:
userData.payloadStr = decodeGsmDcs(userData.payload, offset,
userData.numFields, userData.msgType);
break;
default:
throw new CodingException("unsupported user data encoding ("
+ userData.msgEncoding + ")");
}
}
private static boolean decodeLanguageIndicator(BearerData bData, BitwiseInputStream inStream)
throws BitwiseInputStream.AccessException {
final int EXPECTED_PARAM_SIZE = 1 * 8;
boolean decodeSuccess = false;
int paramBits = inStream.read(8) * 8;
if (paramBits >= EXPECTED_PARAM_SIZE) {
paramBits -= EXPECTED_PARAM_SIZE;
decodeSuccess = true;
bData.language = inStream.read(8);
}
if ((!decodeSuccess) || (paramBits > 0)) {
Log.d(LOG_TAG, "LANGUAGE_INDICATOR decode "
+ (decodeSuccess ? "succeeded" : "failed")
+ " (extra bits = " + paramBits + ")");
}
inStream.skip(paramBits);
return decodeSuccess;
}
private static boolean decodePriorityIndicator(BearerData bData, BitwiseInputStream inStream)
throws BitwiseInputStream.AccessException {
final int EXPECTED_PARAM_SIZE = 1 * 8;
boolean decodeSuccess = false;
int paramBits = inStream.read(8) * 8;
if (paramBits >= EXPECTED_PARAM_SIZE) {
paramBits -= EXPECTED_PARAM_SIZE;
decodeSuccess = true;
bData.priority = inStream.read(2);
inStream.skip(6);
}
if ((!decodeSuccess) || (paramBits > 0)) {
Log.d(LOG_TAG, "PRIORITY_INDICATOR decode "
+ (decodeSuccess ? "succeeded" : "failed")
+ " (extra bits = " + paramBits + ")");
}
inStream.skip(paramBits);
return decodeSuccess;
}
private static int serviceCategoryToCmasMessageClass(int serviceCategory) {
switch (serviceCategory) {
case CdmaSmsCbProgramData.CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT:
return SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT;
case CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT:
return SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT;
case CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT:
return SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT;
case CdmaSmsCbProgramData.CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY:
return SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY;
case CdmaSmsCbProgramData.CATEGORY_CMAS_TEST_MESSAGE:
return SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST;
default:
return SmsCbCmasInfo.CMAS_CLASS_UNKNOWN;
}
}
/**
* CMAS message decoding.
* (See TIA-1149-0-1, CMAS over CDMA)
*
* @param serviceCategory is the service category from the SMS envelope
*/
private static void decodeCmasUserData(Context context, BearerData bData, int serviceCategory)
throws BitwiseInputStream.AccessException, CodingException {
BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload);
if (inStream.available() < 8) {
throw new CodingException("emergency CB with no CMAE_protocol_version");
}
int protocolVersion = inStream.read(8);
if (protocolVersion != 0) {
throw new CodingException("unsupported CMAE_protocol_version " + protocolVersion);
}
int messageClass = serviceCategoryToCmasMessageClass(serviceCategory);
int category = SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN;
int responseType = SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN;
int severity = SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN;
int urgency = SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN;
int certainty = SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN;
while (inStream.available() >= 16) {
int recordType = inStream.read(8);
int recordLen = inStream.read(8);
switch (recordType) {
case 0: // Type 0 elements (Alert text)
UserData alertUserData = new UserData();
alertUserData.msgEncoding = inStream.read(5);
alertUserData.msgEncodingSet = true;
alertUserData.msgType = 0;
int numFields; // number of chars to decode
switch (alertUserData.msgEncoding) {
case UserData.ENCODING_OCTET:
case UserData.ENCODING_LATIN:
numFields = recordLen - 1; // subtract 1 byte for encoding
break;
case UserData.ENCODING_IA5:
case UserData.ENCODING_7BIT_ASCII:
case UserData.ENCODING_GSM_7BIT_ALPHABET:
numFields = ((recordLen * 8) - 5) / 7; // subtract 5 bits for encoding
break;
case UserData.ENCODING_UNICODE_16:
numFields = (recordLen - 1) / 2;
break;
default:
numFields = 0; // unsupported encoding
}
alertUserData.numFields = numFields;
alertUserData.payload = inStream.readByteArray(recordLen * 8 - 5);
decodeUserDataPayload(context, alertUserData, false);
bData.userData = alertUserData;
break;
case 1: // Type 1 elements
category = inStream.read(8);
responseType = inStream.read(8);
severity = inStream.read(4);
urgency = inStream.read(4);
certainty = inStream.read(4);
inStream.skip(recordLen * 8 - 28);
break;
default:
Log.w(LOG_TAG, "skipping unsupported CMAS record type " + recordType);
inStream.skip(recordLen * 8);
break;
}
}
bData.cmasWarningInfo = new SmsCbCmasInfo(messageClass, category, responseType, severity,
urgency, certainty);
}
private static boolean isCmasAlertCategory(int category) {
return category >= CdmaSmsCbProgramData.CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT
&& category <= CdmaSmsCbProgramData.CATEGORY_CMAS_LAST_RESERVED_VALUE;
}
/**
* Create BearerData object from serialized representation.
* (See 3GPP2 C.R1001-F, v1.0, section 4.5 for layout details)
*
* @param smsData byte array of raw encoded SMS bearer data.
* @param serviceCategory the envelope service category (for CMAS alert handling)
* @return an instance of BearerData.
*/
public static BearerData decode(Context context, byte[] smsData, int serviceCategory)
throws CodingException, BitwiseInputStream.AccessException {
BitwiseInputStream inStream = new BitwiseInputStream(smsData);
BearerData bData = new BearerData();
int foundSubparamMask = 0;
while (inStream.available() > 0) {
int subparamId = inStream.read(8);
int subparamIdBit = 1 << subparamId;
// int is 4 bytes. This duplicate check has a limit to Id number up to 32 (4*8)
// as 32th bit is the max bit in int.
// Per 3GPP2 C.S0015-B Table 4.5-1 Bearer Data Subparameter Identifiers:
// last defined subparam ID is 23 (00010111 = 0x17 = 23).
// Only do duplicate subparam ID check if subparam is within defined value as
// reserved subparams are just skipped.
if ((foundSubparamMask & subparamIdBit) != 0 && (
subparamId >= SUBPARAM_MESSAGE_IDENTIFIER
&& subparamId <= SUBPARAM_ID_LAST_DEFINED)) {
throw new CodingException("illegal duplicate subparameter (" + subparamId + ")");
}
boolean decodeSuccess;
switch (subparamId) {
case SUBPARAM_MESSAGE_IDENTIFIER:
decodeSuccess = decodeMessageId(bData, inStream);
break;
case SUBPARAM_USER_DATA:
decodeSuccess = decodeUserData(bData, inStream);
break;
case SUBPARAM_LANGUAGE_INDICATOR:
decodeSuccess = decodeLanguageIndicator(bData, inStream);
break;
case SUBPARAM_PRIORITY_INDICATOR:
decodeSuccess = decodePriorityIndicator(bData, inStream);
break;
default:
decodeSuccess = decodeReserved(inStream, subparamId);
}
if (decodeSuccess && (subparamId >= SUBPARAM_MESSAGE_IDENTIFIER
&& subparamId <= SUBPARAM_ID_LAST_DEFINED)) {
foundSubparamMask |= subparamIdBit;
}
}
if ((foundSubparamMask & (1 << SUBPARAM_MESSAGE_IDENTIFIER)) == 0) {
throw new CodingException("missing MESSAGE_IDENTIFIER subparam");
}
if (bData.userData != null) {
if (isCmasAlertCategory(serviceCategory)) {
decodeCmasUserData(context, bData, serviceCategory);
} else {
decodeUserDataPayload(context, bData.userData, bData.hasUserDataHeader);
}
}
return bData;
}
}