/*
|
* 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.internal.telephony.gsm;
|
|
import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE;
|
import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI;
|
import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY;
|
import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE;
|
import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI;
|
|
import android.content.Context;
|
import android.content.res.Resources;
|
import android.telephony.SmsCbLocation;
|
import android.telephony.SmsCbMessage;
|
import android.util.Pair;
|
|
import com.android.internal.R;
|
import com.android.internal.telephony.GsmAlphabet;
|
import com.android.internal.telephony.SmsConstants;
|
|
import java.io.UnsupportedEncodingException;
|
import java.util.Locale;
|
|
/**
|
* Parses a GSM or UMTS format SMS-CB message into an {@link SmsCbMessage} object. The class is
|
* public because {@link #createSmsCbMessage(SmsCbLocation, byte[][])} is used by some test cases.
|
*/
|
public class GsmSmsCbMessage {
|
|
/**
|
* Languages in the 0000xxxx DCS group as defined in 3GPP TS 23.038, section 5.
|
*/
|
private static final String[] LANGUAGE_CODES_GROUP_0 = {
|
Locale.GERMAN.getLanguage(), // German
|
Locale.ENGLISH.getLanguage(), // English
|
Locale.ITALIAN.getLanguage(), // Italian
|
Locale.FRENCH.getLanguage(), // French
|
new Locale("es").getLanguage(), // Spanish
|
new Locale("nl").getLanguage(), // Dutch
|
new Locale("sv").getLanguage(), // Swedish
|
new Locale("da").getLanguage(), // Danish
|
new Locale("pt").getLanguage(), // Portuguese
|
new Locale("fi").getLanguage(), // Finnish
|
new Locale("nb").getLanguage(), // Norwegian
|
new Locale("el").getLanguage(), // Greek
|
new Locale("tr").getLanguage(), // Turkish
|
new Locale("hu").getLanguage(), // Hungarian
|
new Locale("pl").getLanguage(), // Polish
|
null
|
};
|
|
/**
|
* Languages in the 0010xxxx DCS group as defined in 3GPP TS 23.038, section 5.
|
*/
|
private static final String[] LANGUAGE_CODES_GROUP_2 = {
|
new Locale("cs").getLanguage(), // Czech
|
new Locale("he").getLanguage(), // Hebrew
|
new Locale("ar").getLanguage(), // Arabic
|
new Locale("ru").getLanguage(), // Russian
|
new Locale("is").getLanguage(), // Icelandic
|
null, null, null, null, null, null, null, null, null, null, null
|
};
|
|
private static final char CARRIAGE_RETURN = 0x0d;
|
|
private static final int PDU_BODY_PAGE_LENGTH = 82;
|
|
/** Utility class with only static methods. */
|
private GsmSmsCbMessage() { }
|
|
/**
|
* Get built-in ETWS primary messages by category. ETWS primary message does not contain text,
|
* so we have to show the pre-built messages to the user.
|
*
|
* @param context Device context
|
* @param category ETWS message category defined in SmsCbConstants
|
* @return ETWS text message in string. Return an empty string if no match.
|
*/
|
private static String getEtwsPrimaryMessage(Context context, int category) {
|
final Resources r = context.getResources();
|
switch (category) {
|
case ETWS_WARNING_TYPE_EARTHQUAKE:
|
return r.getString(R.string.etws_primary_default_message_earthquake);
|
case ETWS_WARNING_TYPE_TSUNAMI:
|
return r.getString(R.string.etws_primary_default_message_tsunami);
|
case ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI:
|
return r.getString(R.string.etws_primary_default_message_earthquake_and_tsunami);
|
case ETWS_WARNING_TYPE_TEST_MESSAGE:
|
return r.getString(R.string.etws_primary_default_message_test);
|
case ETWS_WARNING_TYPE_OTHER_EMERGENCY:
|
return r.getString(R.string.etws_primary_default_message_others);
|
default:
|
return "";
|
}
|
}
|
|
/**
|
* Create a new SmsCbMessage object from a header object plus one or more received PDUs.
|
*
|
* @param pdus PDU bytes
|
*/
|
public static SmsCbMessage createSmsCbMessage(Context context, SmsCbHeader header,
|
SmsCbLocation location, byte[][] pdus)
|
throws IllegalArgumentException {
|
if (header.isEtwsPrimaryNotification()) {
|
// ETSI TS 23.041 ETWS Primary Notification message
|
// ETWS primary message only contains 4 fields including serial number,
|
// message identifier, warning type, and warning security information.
|
// There is no field for the content/text so we get the text from the resources.
|
return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, header.getGeographicalScope(),
|
header.getSerialNumber(), location, header.getServiceCategory(), null,
|
getEtwsPrimaryMessage(context, header.getEtwsInfo().getWarningType()),
|
SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, header.getEtwsInfo(),
|
header.getCmasInfo());
|
} else {
|
String language = null;
|
StringBuilder sb = new StringBuilder();
|
for (byte[] pdu : pdus) {
|
Pair<String, String> p = parseBody(header, pdu);
|
language = p.first;
|
sb.append(p.second);
|
}
|
int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY
|
: SmsCbMessage.MESSAGE_PRIORITY_NORMAL;
|
|
return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
|
header.getGeographicalScope(), header.getSerialNumber(), location,
|
header.getServiceCategory(), language, sb.toString(), priority,
|
header.getEtwsInfo(), header.getCmasInfo());
|
}
|
}
|
|
/**
|
* Parse and unpack the body text according to the encoding in the DCS.
|
* After completing successfully this method will have assigned the body
|
* text into mBody, and optionally the language code into mLanguage
|
*
|
* @param header the message header to use
|
* @param pdu the PDU to decode
|
* @return a Pair of Strings containing the language and body of the message
|
*/
|
private static Pair<String, String> parseBody(SmsCbHeader header, byte[] pdu) {
|
int encoding;
|
String language = null;
|
boolean hasLanguageIndicator = false;
|
int dataCodingScheme = header.getDataCodingScheme();
|
|
// Extract encoding and language from DCS, as defined in 3gpp TS 23.038,
|
// section 5.
|
switch ((dataCodingScheme & 0xf0) >> 4) {
|
case 0x00:
|
encoding = SmsConstants.ENCODING_7BIT;
|
language = LANGUAGE_CODES_GROUP_0[dataCodingScheme & 0x0f];
|
break;
|
|
case 0x01:
|
hasLanguageIndicator = true;
|
if ((dataCodingScheme & 0x0f) == 0x01) {
|
encoding = SmsConstants.ENCODING_16BIT;
|
} else {
|
encoding = SmsConstants.ENCODING_7BIT;
|
}
|
break;
|
|
case 0x02:
|
encoding = SmsConstants.ENCODING_7BIT;
|
language = LANGUAGE_CODES_GROUP_2[dataCodingScheme & 0x0f];
|
break;
|
|
case 0x03:
|
encoding = SmsConstants.ENCODING_7BIT;
|
break;
|
|
case 0x04:
|
case 0x05:
|
switch ((dataCodingScheme & 0x0c) >> 2) {
|
case 0x01:
|
encoding = SmsConstants.ENCODING_8BIT;
|
break;
|
|
case 0x02:
|
encoding = SmsConstants.ENCODING_16BIT;
|
break;
|
|
case 0x00:
|
default:
|
encoding = SmsConstants.ENCODING_7BIT;
|
break;
|
}
|
break;
|
|
case 0x06:
|
case 0x07:
|
// Compression not supported
|
case 0x09:
|
// UDH structure not supported
|
case 0x0e:
|
// Defined by the WAP forum not supported
|
throw new IllegalArgumentException("Unsupported GSM dataCodingScheme "
|
+ dataCodingScheme);
|
|
case 0x0f:
|
if (((dataCodingScheme & 0x04) >> 2) == 0x01) {
|
encoding = SmsConstants.ENCODING_8BIT;
|
} else {
|
encoding = SmsConstants.ENCODING_7BIT;
|
}
|
break;
|
|
default:
|
// Reserved values are to be treated as 7-bit
|
encoding = SmsConstants.ENCODING_7BIT;
|
break;
|
}
|
|
if (header.isUmtsFormat()) {
|
// Payload may contain multiple pages
|
int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH];
|
|
if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1)
|
* nrPages) {
|
throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match "
|
+ nrPages + " pages");
|
}
|
|
StringBuilder sb = new StringBuilder();
|
|
for (int i = 0; i < nrPages; i++) {
|
// Each page is 82 bytes followed by a length octet indicating
|
// the number of useful octets within those 82
|
int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i;
|
int length = pdu[offset + PDU_BODY_PAGE_LENGTH];
|
|
if (length > PDU_BODY_PAGE_LENGTH) {
|
throw new IllegalArgumentException("Page length " + length
|
+ " exceeds maximum value " + PDU_BODY_PAGE_LENGTH);
|
}
|
|
Pair<String, String> p = unpackBody(pdu, encoding, offset, length,
|
hasLanguageIndicator, language);
|
language = p.first;
|
sb.append(p.second);
|
}
|
return new Pair<String, String>(language, sb.toString());
|
} else {
|
// Payload is one single page
|
int offset = SmsCbHeader.PDU_HEADER_LENGTH;
|
int length = pdu.length - offset;
|
|
return unpackBody(pdu, encoding, offset, length, hasLanguageIndicator, language);
|
}
|
}
|
|
/**
|
* Unpack body text from the pdu using the given encoding, position and
|
* length within the pdu
|
*
|
* @param pdu The pdu
|
* @param encoding The encoding, as derived from the DCS
|
* @param offset Position of the first byte to unpack
|
* @param length Number of bytes to unpack
|
* @param hasLanguageIndicator true if the body text is preceded by a
|
* language indicator. If so, this method will as a side-effect
|
* assign the extracted language code into mLanguage
|
* @param language the language to return if hasLanguageIndicator is false
|
* @return a Pair of Strings containing the language and body of the message
|
*/
|
private static Pair<String, String> unpackBody(byte[] pdu, int encoding, int offset, int length,
|
boolean hasLanguageIndicator, String language) {
|
String body = null;
|
|
switch (encoding) {
|
case SmsConstants.ENCODING_7BIT:
|
body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7);
|
|
if (hasLanguageIndicator && body != null && body.length() > 2) {
|
// Language is two GSM characters followed by a CR.
|
// The actual body text is offset by 3 characters.
|
language = body.substring(0, 2);
|
body = body.substring(3);
|
}
|
break;
|
|
case SmsConstants.ENCODING_16BIT:
|
if (hasLanguageIndicator && pdu.length >= offset + 2) {
|
// Language is two GSM characters.
|
// The actual body text is offset by 2 bytes.
|
language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2);
|
offset += 2;
|
length -= 2;
|
}
|
|
try {
|
body = new String(pdu, offset, (length & 0xfffe), "utf-16");
|
} catch (UnsupportedEncodingException e) {
|
// Apparently it wasn't valid UTF-16.
|
throw new IllegalArgumentException("Error decoding UTF-16 message", e);
|
}
|
break;
|
|
default:
|
break;
|
}
|
|
if (body != null) {
|
// Remove trailing carriage return
|
for (int i = body.length() - 1; i >= 0; i--) {
|
if (body.charAt(i) != CARRIAGE_RETURN) {
|
body = body.substring(0, i + 1);
|
break;
|
}
|
}
|
} else {
|
body = "";
|
}
|
|
return new Pair<String, String>(language, body);
|
}
|
}
|