/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.telephony; import android.text.format.Time; import android.util.Log; import com.android.internal.telephony.GsmAlphabet; import com.android.internal.telephony.IccUtils; import com.android.internal.telephony.gsm.SmsCbHeader; import java.io.UnsupportedEncodingException; /** * Describes an SMS-CB message. * * {@hide} */ public class SmsCbMessage { /** * Cell wide immediate geographical scope */ public static final int GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE = 0; /** * PLMN wide geographical scope */ public static final int GEOGRAPHICAL_SCOPE_PLMN_WIDE = 1; /** * Location / service area wide geographical scope */ public static final int GEOGRAPHICAL_SCOPE_LA_WIDE = 2; /** * Cell wide geographical scope */ public static final int GEOGRAPHICAL_SCOPE_CELL_WIDE = 3; /** * Create an instance of this class from a received PDU * * @param pdu PDU bytes * @return An instance of this class, or null if invalid pdu */ public static SmsCbMessage createFromPdu(byte[] pdu) { try { return new SmsCbMessage(pdu); } catch (IllegalArgumentException e) { Log.w(LOG_TAG, "Failed parsing SMS-CB pdu", e); return null; } } private static final String LOG_TAG = "SMSCB"; /** * Languages in the 0000xxxx DCS group as defined in 3GPP TS 23.038, section 5. */ private static final String[] LANGUAGE_CODES_GROUP_0 = { "de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi", "no", "el", "tr", "hu", "pl", null }; /** * Languages in the 0010xxxx DCS group as defined in 3GPP TS 23.038, section 5. */ private static final String[] LANGUAGE_CODES_GROUP_2 = { "cs", "he", "ar", "ru", "is", 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; private SmsCbHeader mHeader; private String mLanguage; private String mBody; /** Timestamp of ETWS primary notification with security. */ private long mPrimaryNotificationTimestamp; /** 43 byte digital signature of ETWS primary notification with security. */ private byte[] mPrimaryNotificationDigitalSignature; private SmsCbMessage(byte[] pdu) throws IllegalArgumentException { mHeader = new SmsCbHeader(pdu); if (mHeader.format == SmsCbHeader.FORMAT_ETWS_PRIMARY) { mBody = "ETWS"; // ETWS primary notification with security is 56 octets in length if (pdu.length >= SmsCbHeader.PDU_LENGTH_ETWS) { mPrimaryNotificationTimestamp = getTimestampMillis(pdu); mPrimaryNotificationDigitalSignature = new byte[43]; // digital signature starts after 6 byte header and 7 byte timestamp System.arraycopy(pdu, 13, mPrimaryNotificationDigitalSignature, 0, 43); } } else { parseBody(pdu); } } /** * Return the geographical scope of this message, one of * {@link #GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE}, * {@link #GEOGRAPHICAL_SCOPE_PLMN_WIDE}, * {@link #GEOGRAPHICAL_SCOPE_LA_WIDE}, * {@link #GEOGRAPHICAL_SCOPE_CELL_WIDE} * * @return Geographical scope */ public int getGeographicalScope() { return mHeader.geographicalScope; } /** * Get the ISO-639-1 language code for this message, or null if unspecified * * @return Language code */ public String getLanguageCode() { return mLanguage; } /** * Get the body of this message, or null if no body available * * @return Body, or null */ public String getMessageBody() { return mBody; } /** * Get the message identifier of this message (0-65535) * * @return Message identifier */ public int getMessageIdentifier() { return mHeader.messageIdentifier; } /** * Get the message code of this message (0-1023) * * @return Message code */ public int getMessageCode() { return mHeader.messageCode; } /** * Get the update number of this message (0-15) * * @return Update number */ public int getUpdateNumber() { return mHeader.updateNumber; } /** * Get the format of this message. * @return {@link SmsCbHeader#FORMAT_GSM}, {@link SmsCbHeader#FORMAT_UMTS}, or * {@link SmsCbHeader#FORMAT_ETWS_PRIMARY} */ public int getMessageFormat() { return mHeader.format; } /** * For ETWS primary notifications, return the emergency user alert flag. * @return true to notify terminal to activate emergency user alert; false otherwise */ public boolean getEtwsEmergencyUserAlert() { return mHeader.etwsEmergencyUserAlert; } /** * For ETWS primary notifications, return the popup flag. * @return true to notify terminal to activate display popup; false otherwise */ public boolean getEtwsPopup() { return mHeader.etwsPopup; } /** * For ETWS primary notifications, return the warning type. * @return a value such as {@link SmsCbConstants#ETWS_WARNING_TYPE_EARTHQUAKE} */ public int getEtwsWarningType() { return mHeader.etwsWarningType; } /** * For ETWS primary notifications, return the Warning-Security-Information timestamp. * @return a timestamp in System.currentTimeMillis() format. */ public long getEtwsSecurityTimestamp() { return mPrimaryNotificationTimestamp; } /** * For ETWS primary notifications, return the 43 byte digital signature. * @return a byte array containing a copy of the digital signature */ public byte[] getEtwsSecuritySignature() { return mPrimaryNotificationDigitalSignature.clone(); } /** * 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 pdu The pdu */ private void parseBody(byte[] pdu) { int encoding; boolean hasLanguageIndicator = false; // Extract encoding and language from DCS, as defined in 3gpp TS 23.038, // section 5. switch ((mHeader.dataCodingScheme & 0xf0) >> 4) { case 0x00: encoding = SmsMessage.ENCODING_7BIT; mLanguage = LANGUAGE_CODES_GROUP_0[mHeader.dataCodingScheme & 0x0f]; break; case 0x01: hasLanguageIndicator = true; if ((mHeader.dataCodingScheme & 0x0f) == 0x01) { encoding = SmsMessage.ENCODING_16BIT; } else { encoding = SmsMessage.ENCODING_7BIT; } break; case 0x02: encoding = SmsMessage.ENCODING_7BIT; mLanguage = LANGUAGE_CODES_GROUP_2[mHeader.dataCodingScheme & 0x0f]; break; case 0x03: encoding = SmsMessage.ENCODING_7BIT; break; case 0x04: case 0x05: switch ((mHeader.dataCodingScheme & 0x0c) >> 2) { case 0x01: encoding = SmsMessage.ENCODING_8BIT; break; case 0x02: encoding = SmsMessage.ENCODING_16BIT; break; case 0x00: default: encoding = SmsMessage.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 encoding = SmsMessage.ENCODING_UNKNOWN; break; case 0x0f: if (((mHeader.dataCodingScheme & 0x04) >> 2) == 0x01) { encoding = SmsMessage.ENCODING_8BIT; } else { encoding = SmsMessage.ENCODING_7BIT; } break; default: // Reserved values are to be treated as 7-bit encoding = SmsMessage.ENCODING_7BIT; break; } if (mHeader.format == SmsCbHeader.FORMAT_UMTS) { // 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); } sb.append(unpackBody(pdu, encoding, offset, length, hasLanguageIndicator)); } mBody = sb.toString(); } else { // Payload is one single page int offset = SmsCbHeader.PDU_HEADER_LENGTH; int length = pdu.length - offset; mBody = unpackBody(pdu, encoding, offset, length, hasLanguageIndicator); } } /** * 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 * @return Body text */ private String unpackBody(byte[] pdu, int encoding, int offset, int length, boolean hasLanguageIndicator) { String body = null; switch (encoding) { case SmsMessage.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. mLanguage = body.substring(0, 2); body = body.substring(3); } break; case SmsMessage.ENCODING_16BIT: if (hasLanguageIndicator && pdu.length >= offset + 2) { // Language is two GSM characters. // The actual body text is offset by 2 bytes. mLanguage = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2); offset += 2; length -= 2; } try { body = new String(pdu, offset, (length & 0xfffe), "utf-16"); } catch (UnsupportedEncodingException e) { // Eeeek } 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 body; } /** * Parses an ETWS primary notification timestamp and returns a currentTimeMillis()-style * timestamp. Copied from com.android.internal.telephony.gsm.SmsMessage. * @param pdu the ETWS primary notification PDU to decode * @return the UTC timestamp from the Warning-Security-Information parameter */ private long getTimestampMillis(byte[] pdu) { // Timestamp starts after CB header, in pdu[6] int year = IccUtils.gsmBcdByteToInt(pdu[6]); int month = IccUtils.gsmBcdByteToInt(pdu[7]); int day = IccUtils.gsmBcdByteToInt(pdu[8]); int hour = IccUtils.gsmBcdByteToInt(pdu[9]); int minute = IccUtils.gsmBcdByteToInt(pdu[10]); int second = IccUtils.gsmBcdByteToInt(pdu[11]); // For the timezone, the most significant bit of the // least significant nibble is the sign byte // (meaning the max range of this field is 79 quarter-hours, // which is more than enough) byte tzByte = pdu[12]; // Mask out sign bit. int timezoneOffset = IccUtils.gsmBcdByteToInt((byte) (tzByte & (~0x08))); timezoneOffset = ((tzByte & 0x08) == 0) ? timezoneOffset : -timezoneOffset; Time time = new Time(Time.TIMEZONE_UTC); // It's 2006. Should I really support years < 2000? time.year = year >= 90 ? year + 1900 : year + 2000; time.month = month - 1; time.monthDay = day; time.hour = hour; time.minute = minute; time.second = second; // Timezone offset is in quarter hours. return time.toMillis(true) - (timezoneOffset * 15 * 60 * 1000); } /** * Append text to the message body. This is used to concatenate multi-page GSM broadcasts. * @param body the text to append to this message */ public void appendToBody(String body) { mBody = mBody + body; } @Override public String toString() { return "SmsCbMessage{" + mHeader.toString() + ", language=" + mLanguage + ", body=\"" + mBody + "\"}"; } }