1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.telephony; 18 19 import android.text.format.Time; 20 import android.util.Log; 21 22 import com.android.internal.telephony.GsmAlphabet; 23 import com.android.internal.telephony.IccUtils; 24 import com.android.internal.telephony.gsm.SmsCbHeader; 25 26 import java.io.UnsupportedEncodingException; 27 28 /** 29 * Describes an SMS-CB message. 30 * 31 * {@hide} 32 */ 33 public class SmsCbMessage { 34 35 /** 36 * Cell wide immediate geographical scope 37 */ 38 public static final int GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE = 0; 39 40 /** 41 * PLMN wide geographical scope 42 */ 43 public static final int GEOGRAPHICAL_SCOPE_PLMN_WIDE = 1; 44 45 /** 46 * Location / service area wide geographical scope 47 */ 48 public static final int GEOGRAPHICAL_SCOPE_LA_WIDE = 2; 49 50 /** 51 * Cell wide geographical scope 52 */ 53 public static final int GEOGRAPHICAL_SCOPE_CELL_WIDE = 3; 54 55 /** 56 * Create an instance of this class from a received PDU 57 * 58 * @param pdu PDU bytes 59 * @return An instance of this class, or null if invalid pdu 60 */ createFromPdu(byte[] pdu)61 public static SmsCbMessage createFromPdu(byte[] pdu) { 62 try { 63 return new SmsCbMessage(pdu); 64 } catch (IllegalArgumentException e) { 65 Log.w(LOG_TAG, "Failed parsing SMS-CB pdu", e); 66 return null; 67 } 68 } 69 70 private static final String LOG_TAG = "SMSCB"; 71 72 /** 73 * Languages in the 0000xxxx DCS group as defined in 3GPP TS 23.038, section 5. 74 */ 75 private static final String[] LANGUAGE_CODES_GROUP_0 = { 76 "de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi", "no", "el", "tr", "hu", 77 "pl", null 78 }; 79 80 /** 81 * Languages in the 0010xxxx DCS group as defined in 3GPP TS 23.038, section 5. 82 */ 83 private static final String[] LANGUAGE_CODES_GROUP_2 = { 84 "cs", "he", "ar", "ru", "is", null, null, null, null, null, null, null, null, null, 85 null, null 86 }; 87 88 private static final char CARRIAGE_RETURN = 0x0d; 89 90 private static final int PDU_BODY_PAGE_LENGTH = 82; 91 92 private SmsCbHeader mHeader; 93 94 private String mLanguage; 95 96 private String mBody; 97 98 /** Timestamp of ETWS primary notification with security. */ 99 private long mPrimaryNotificationTimestamp; 100 101 /** 43 byte digital signature of ETWS primary notification with security. */ 102 private byte[] mPrimaryNotificationDigitalSignature; 103 SmsCbMessage(byte[] pdu)104 private SmsCbMessage(byte[] pdu) throws IllegalArgumentException { 105 mHeader = new SmsCbHeader(pdu); 106 if (mHeader.format == SmsCbHeader.FORMAT_ETWS_PRIMARY) { 107 mBody = "ETWS"; 108 // ETWS primary notification with security is 56 octets in length 109 if (pdu.length >= SmsCbHeader.PDU_LENGTH_ETWS) { 110 mPrimaryNotificationTimestamp = getTimestampMillis(pdu); 111 mPrimaryNotificationDigitalSignature = new byte[43]; 112 // digital signature starts after 6 byte header and 7 byte timestamp 113 System.arraycopy(pdu, 13, mPrimaryNotificationDigitalSignature, 0, 43); 114 } 115 } else { 116 parseBody(pdu); 117 } 118 } 119 120 /** 121 * Return the geographical scope of this message, one of 122 * {@link #GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE}, 123 * {@link #GEOGRAPHICAL_SCOPE_PLMN_WIDE}, 124 * {@link #GEOGRAPHICAL_SCOPE_LA_WIDE}, 125 * {@link #GEOGRAPHICAL_SCOPE_CELL_WIDE} 126 * 127 * @return Geographical scope 128 */ getGeographicalScope()129 public int getGeographicalScope() { 130 return mHeader.geographicalScope; 131 } 132 133 /** 134 * Get the ISO-639-1 language code for this message, or null if unspecified 135 * 136 * @return Language code 137 */ getLanguageCode()138 public String getLanguageCode() { 139 return mLanguage; 140 } 141 142 /** 143 * Get the body of this message, or null if no body available 144 * 145 * @return Body, or null 146 */ getMessageBody()147 public String getMessageBody() { 148 return mBody; 149 } 150 151 /** 152 * Get the message identifier of this message (0-65535) 153 * 154 * @return Message identifier 155 */ getMessageIdentifier()156 public int getMessageIdentifier() { 157 return mHeader.messageIdentifier; 158 } 159 160 /** 161 * Get the message code of this message (0-1023) 162 * 163 * @return Message code 164 */ getMessageCode()165 public int getMessageCode() { 166 return mHeader.messageCode; 167 } 168 169 /** 170 * Get the update number of this message (0-15) 171 * 172 * @return Update number 173 */ getUpdateNumber()174 public int getUpdateNumber() { 175 return mHeader.updateNumber; 176 } 177 178 /** 179 * Get the format of this message. 180 * @return {@link SmsCbHeader#FORMAT_GSM}, {@link SmsCbHeader#FORMAT_UMTS}, or 181 * {@link SmsCbHeader#FORMAT_ETWS_PRIMARY} 182 */ getMessageFormat()183 public int getMessageFormat() { 184 return mHeader.format; 185 } 186 187 /** 188 * For ETWS primary notifications, return the emergency user alert flag. 189 * @return true to notify terminal to activate emergency user alert; false otherwise 190 */ getEtwsEmergencyUserAlert()191 public boolean getEtwsEmergencyUserAlert() { 192 return mHeader.etwsEmergencyUserAlert; 193 } 194 195 /** 196 * For ETWS primary notifications, return the popup flag. 197 * @return true to notify terminal to activate display popup; false otherwise 198 */ getEtwsPopup()199 public boolean getEtwsPopup() { 200 return mHeader.etwsPopup; 201 } 202 203 /** 204 * For ETWS primary notifications, return the warning type. 205 * @return a value such as {@link SmsCbConstants#ETWS_WARNING_TYPE_EARTHQUAKE} 206 */ getEtwsWarningType()207 public int getEtwsWarningType() { 208 return mHeader.etwsWarningType; 209 } 210 211 /** 212 * For ETWS primary notifications, return the Warning-Security-Information timestamp. 213 * @return a timestamp in System.currentTimeMillis() format. 214 */ getEtwsSecurityTimestamp()215 public long getEtwsSecurityTimestamp() { 216 return mPrimaryNotificationTimestamp; 217 } 218 219 /** 220 * For ETWS primary notifications, return the 43 byte digital signature. 221 * @return a byte array containing a copy of the digital signature 222 */ getEtwsSecuritySignature()223 public byte[] getEtwsSecuritySignature() { 224 return mPrimaryNotificationDigitalSignature.clone(); 225 } 226 227 /** 228 * Parse and unpack the body text according to the encoding in the DCS. 229 * After completing successfully this method will have assigned the body 230 * text into mBody, and optionally the language code into mLanguage 231 * 232 * @param pdu The pdu 233 */ parseBody(byte[] pdu)234 private void parseBody(byte[] pdu) { 235 int encoding; 236 boolean hasLanguageIndicator = false; 237 238 // Extract encoding and language from DCS, as defined in 3gpp TS 23.038, 239 // section 5. 240 switch ((mHeader.dataCodingScheme & 0xf0) >> 4) { 241 case 0x00: 242 encoding = SmsMessage.ENCODING_7BIT; 243 mLanguage = LANGUAGE_CODES_GROUP_0[mHeader.dataCodingScheme & 0x0f]; 244 break; 245 246 case 0x01: 247 hasLanguageIndicator = true; 248 if ((mHeader.dataCodingScheme & 0x0f) == 0x01) { 249 encoding = SmsMessage.ENCODING_16BIT; 250 } else { 251 encoding = SmsMessage.ENCODING_7BIT; 252 } 253 break; 254 255 case 0x02: 256 encoding = SmsMessage.ENCODING_7BIT; 257 mLanguage = LANGUAGE_CODES_GROUP_2[mHeader.dataCodingScheme & 0x0f]; 258 break; 259 260 case 0x03: 261 encoding = SmsMessage.ENCODING_7BIT; 262 break; 263 264 case 0x04: 265 case 0x05: 266 switch ((mHeader.dataCodingScheme & 0x0c) >> 2) { 267 case 0x01: 268 encoding = SmsMessage.ENCODING_8BIT; 269 break; 270 271 case 0x02: 272 encoding = SmsMessage.ENCODING_16BIT; 273 break; 274 275 case 0x00: 276 default: 277 encoding = SmsMessage.ENCODING_7BIT; 278 break; 279 } 280 break; 281 282 case 0x06: 283 case 0x07: 284 // Compression not supported 285 case 0x09: 286 // UDH structure not supported 287 case 0x0e: 288 // Defined by the WAP forum not supported 289 encoding = SmsMessage.ENCODING_UNKNOWN; 290 break; 291 292 case 0x0f: 293 if (((mHeader.dataCodingScheme & 0x04) >> 2) == 0x01) { 294 encoding = SmsMessage.ENCODING_8BIT; 295 } else { 296 encoding = SmsMessage.ENCODING_7BIT; 297 } 298 break; 299 300 default: 301 // Reserved values are to be treated as 7-bit 302 encoding = SmsMessage.ENCODING_7BIT; 303 break; 304 } 305 306 if (mHeader.format == SmsCbHeader.FORMAT_UMTS) { 307 // Payload may contain multiple pages 308 int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH]; 309 310 if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) 311 * nrPages) { 312 throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match " 313 + nrPages + " pages"); 314 } 315 316 StringBuilder sb = new StringBuilder(); 317 318 for (int i = 0; i < nrPages; i++) { 319 // Each page is 82 bytes followed by a length octet indicating 320 // the number of useful octets within those 82 321 int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i; 322 int length = pdu[offset + PDU_BODY_PAGE_LENGTH]; 323 324 if (length > PDU_BODY_PAGE_LENGTH) { 325 throw new IllegalArgumentException("Page length " + length 326 + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH); 327 } 328 329 sb.append(unpackBody(pdu, encoding, offset, length, hasLanguageIndicator)); 330 } 331 mBody = sb.toString(); 332 } else { 333 // Payload is one single page 334 int offset = SmsCbHeader.PDU_HEADER_LENGTH; 335 int length = pdu.length - offset; 336 337 mBody = unpackBody(pdu, encoding, offset, length, hasLanguageIndicator); 338 } 339 } 340 341 /** 342 * Unpack body text from the pdu using the given encoding, position and 343 * length within the pdu 344 * 345 * @param pdu The pdu 346 * @param encoding The encoding, as derived from the DCS 347 * @param offset Position of the first byte to unpack 348 * @param length Number of bytes to unpack 349 * @param hasLanguageIndicator true if the body text is preceded by a 350 * language indicator. If so, this method will as a side-effect 351 * assign the extracted language code into mLanguage 352 * @return Body text 353 */ unpackBody(byte[] pdu, int encoding, int offset, int length, boolean hasLanguageIndicator)354 private String unpackBody(byte[] pdu, int encoding, int offset, int length, 355 boolean hasLanguageIndicator) { 356 String body = null; 357 358 switch (encoding) { 359 case SmsMessage.ENCODING_7BIT: 360 body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7); 361 362 if (hasLanguageIndicator && body != null && body.length() > 2) { 363 // Language is two GSM characters followed by a CR. 364 // The actual body text is offset by 3 characters. 365 mLanguage = body.substring(0, 2); 366 body = body.substring(3); 367 } 368 break; 369 370 case SmsMessage.ENCODING_16BIT: 371 if (hasLanguageIndicator && pdu.length >= offset + 2) { 372 // Language is two GSM characters. 373 // The actual body text is offset by 2 bytes. 374 mLanguage = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2); 375 offset += 2; 376 length -= 2; 377 } 378 379 try { 380 body = new String(pdu, offset, (length & 0xfffe), "utf-16"); 381 } catch (UnsupportedEncodingException e) { 382 // Eeeek 383 } 384 break; 385 386 default: 387 break; 388 } 389 390 if (body != null) { 391 // Remove trailing carriage return 392 for (int i = body.length() - 1; i >= 0; i--) { 393 if (body.charAt(i) != CARRIAGE_RETURN) { 394 body = body.substring(0, i + 1); 395 break; 396 } 397 } 398 } else { 399 body = ""; 400 } 401 402 return body; 403 } 404 405 /** 406 * Parses an ETWS primary notification timestamp and returns a currentTimeMillis()-style 407 * timestamp. Copied from com.android.internal.telephony.gsm.SmsMessage. 408 * @param pdu the ETWS primary notification PDU to decode 409 * @return the UTC timestamp from the Warning-Security-Information parameter 410 */ getTimestampMillis(byte[] pdu)411 private long getTimestampMillis(byte[] pdu) { 412 // Timestamp starts after CB header, in pdu[6] 413 int year = IccUtils.gsmBcdByteToInt(pdu[6]); 414 int month = IccUtils.gsmBcdByteToInt(pdu[7]); 415 int day = IccUtils.gsmBcdByteToInt(pdu[8]); 416 int hour = IccUtils.gsmBcdByteToInt(pdu[9]); 417 int minute = IccUtils.gsmBcdByteToInt(pdu[10]); 418 int second = IccUtils.gsmBcdByteToInt(pdu[11]); 419 420 // For the timezone, the most significant bit of the 421 // least significant nibble is the sign byte 422 // (meaning the max range of this field is 79 quarter-hours, 423 // which is more than enough) 424 425 byte tzByte = pdu[12]; 426 427 // Mask out sign bit. 428 int timezoneOffset = IccUtils.gsmBcdByteToInt((byte) (tzByte & (~0x08))); 429 430 timezoneOffset = ((tzByte & 0x08) == 0) ? timezoneOffset : -timezoneOffset; 431 432 Time time = new Time(Time.TIMEZONE_UTC); 433 434 // It's 2006. Should I really support years < 2000? 435 time.year = year >= 90 ? year + 1900 : year + 2000; 436 time.month = month - 1; 437 time.monthDay = day; 438 time.hour = hour; 439 time.minute = minute; 440 time.second = second; 441 442 // Timezone offset is in quarter hours. 443 return time.toMillis(true) - (timezoneOffset * 15 * 60 * 1000); 444 } 445 446 /** 447 * Append text to the message body. This is used to concatenate multi-page GSM broadcasts. 448 * @param body the text to append to this message 449 */ appendToBody(String body)450 public void appendToBody(String body) { 451 mBody = mBody + body; 452 } 453 454 @Override toString()455 public String toString() { 456 return "SmsCbMessage{" + mHeader.toString() + ", language=" + mLanguage + 457 ", body=\"" + mBody + "\"}"; 458 } 459 } 460