1 /* 2 * Copyright (C) 2013 Samsung System LSI 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 package com.android.bluetooth.map; 16 17 import static android.telephony.TelephonyManager.PHONE_TYPE_CDMA; 18 19 import android.bluetooth.BluetoothProfile; 20 import android.bluetooth.BluetoothProtoEnums; 21 import android.content.Context; 22 import android.telephony.PhoneNumberUtils; 23 import android.telephony.SmsManager; 24 import android.telephony.SmsMessage; 25 import android.telephony.TelephonyManager; 26 import android.util.Log; 27 28 import com.android.bluetooth.BluetoothStatsLog; 29 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils; 30 import com.android.bluetooth.util.GsmAlphabet; 31 32 import java.io.ByteArrayInputStream; 33 import java.io.ByteArrayOutputStream; 34 import java.io.IOException; 35 import java.io.UnsupportedEncodingException; 36 import java.nio.charset.StandardCharsets; 37 import java.text.SimpleDateFormat; 38 import java.util.ArrayList; 39 import java.util.Calendar; 40 import java.util.Date; 41 import java.util.List; 42 import java.util.Locale; 43 import java.util.Random; 44 45 // Next tag value for ContentProfileErrorReportUtils.report(): 10 46 public class BluetoothMapSmsPdu { 47 private static final String TAG = BluetoothMapSmsPdu.class.getSimpleName(); 48 49 private static final int INVALID_VALUE = -1; 50 public static final int SMS_TYPE_GSM = 1; 51 public static final int SMS_TYPE_CDMA = 2; 52 53 /** from SMS user data header information element identifiers. (see TS 23.040 9.2.3.24) */ 54 private static final int ELT_ID_NATIONAL_LANGUAGE_SINGLE_SHIFT = 0x24; 55 56 private static final int ELT_ID_NATIONAL_LANGUAGE_LOCKING_SHIFT = 0x25; 57 58 /** Supported message types for CDMA SMS messages (See 3GPP2 C.S0015-B, v2.0, table 4.5.1-1) */ 59 private static final int MESSAGE_TYPE_DELIVER = 0x01; 60 61 /** 62 * We need to handle the SC-address mentioned in errata 4335. Since the definition could be read 63 * in three different ways, I have asked the car working group for clarification, and are 64 * awaiting confirmation that this clarification will go into the MAP spec: The native format 65 * should be <sc_addr><tpdu> where <sc_addr> is <length><ton><1..10 octet of address> coded 66 * according to 24.011. The IEI is not to be used, as the fixed order of the data makes a type 4 67 * LV information element sufficient. <length> is a single octet which value is the length of 68 * the value-field in octets including both the <ton> and the <address>. 69 */ 70 public static class SmsPdu { 71 private byte[] mData; 72 private final byte[] mScAddress = {0}; 73 // At the moment we do not use the scAddress, hence set the length to 0. 74 private int mUserDataMsgOffset = 0; 75 private int mEncoding; 76 private int mLanguageTable; 77 private int mLanguageShiftTable; 78 private final int mType; 79 80 /* Members used for pdu decoding */ 81 private int mUserDataSeptetPadding = INVALID_VALUE; 82 private int mMsgSeptetCount = 0; 83 SmsPdu(byte[] data, int type)84 SmsPdu(byte[] data, int type) { 85 this.mData = data; 86 this.mEncoding = INVALID_VALUE; 87 this.mType = type; 88 this.mLanguageTable = INVALID_VALUE; 89 this.mLanguageShiftTable = INVALID_VALUE; 90 this.mUserDataMsgOffset = gsmSubmitGetTpUdOffset(); // Assume no user data header 91 } 92 93 /** Create a pdu instance based on the data generated on this device. */ SmsPdu(byte[] data, int encoding, int type, int languageTable)94 SmsPdu(byte[] data, int encoding, int type, int languageTable) { 95 this.mData = data; 96 this.mEncoding = encoding; 97 this.mType = type; 98 this.mLanguageTable = languageTable; 99 } 100 getData()101 public byte[] getData() { 102 return mData; 103 } 104 getScAddress()105 public byte[] getScAddress() { 106 return mScAddress; 107 } 108 setEncoding(int encoding)109 public void setEncoding(int encoding) { 110 this.mEncoding = encoding; 111 } 112 getEncoding()113 public int getEncoding() { 114 return mEncoding; 115 } 116 getType()117 public int getType() { 118 return mType; 119 } 120 getUserDataMsgOffset()121 public int getUserDataMsgOffset() { 122 return mUserDataMsgOffset; 123 } 124 125 /** The user data message payload size in bytes - excluding the user data header. */ getUserDataMsgSize()126 public int getUserDataMsgSize() { 127 return mData.length - mUserDataMsgOffset; 128 } 129 getLanguageShiftTable()130 public int getLanguageShiftTable() { 131 return mLanguageShiftTable; 132 } 133 getLanguageTable()134 public int getLanguageTable() { 135 return mLanguageTable; 136 } 137 getUserDataSeptetPadding()138 public int getUserDataSeptetPadding() { 139 return mUserDataSeptetPadding; 140 } 141 getMsgSeptetCount()142 public int getMsgSeptetCount() { 143 return mMsgSeptetCount; 144 } 145 146 /* PDU parsing/modification functionality */ 147 private static final byte ORIGINATING_ADDRESS = 0x02; 148 private static final byte ORIGINATING_SUB_ADDRESS = 0x03; 149 private static final byte DESTINATION_ADDRESS = 0x04; 150 private static final byte DESTINATION_SUB_ADDRESS = 0x05; 151 private static final byte BEARER_DATA = 0x08; 152 153 /** 154 * Find and return the offset to the specified parameter ID 155 * 156 * @param parameterId The parameter ID to find 157 * @return the offset in number of bytes to the parameterID entry in the pdu data. The byte 158 * at the offset contains the parameter ID, the byte following contains the parameter 159 * length, and offset + 2 is the first byte of the parameter data. 160 */ cdmaGetParameterOffset(byte parameterId)161 private int cdmaGetParameterOffset(byte parameterId) { 162 ByteArrayInputStream pdu = new ByteArrayInputStream(mData); 163 int offset = 0; 164 boolean found = false; 165 166 try { 167 pdu.skip(1); // Skip the message type 168 169 while (pdu.available() > 0) { 170 int currentId = pdu.read(); 171 int currentLen = pdu.read(); 172 173 if (currentId == parameterId) { 174 found = true; 175 break; 176 } else { 177 pdu.skip(currentLen); 178 offset += 2 + currentLen; 179 } 180 } 181 pdu.close(); 182 } catch (Exception e) { 183 ContentProfileErrorReportUtils.report( 184 BluetoothProfile.MAP, 185 BluetoothProtoEnums.BLUETOOTH_MAP_SMS_PDU, 186 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 187 0); 188 Log.e(TAG, "cdmaGetParameterOffset: ", e); 189 } 190 191 if (found) { 192 return offset; 193 } else { 194 return 0; 195 } 196 } 197 198 private static final byte BEARER_DATA_MSG_ID = 0x00; 199 cdmaGetSubParameterOffset(byte subParameterId)200 private int cdmaGetSubParameterOffset(byte subParameterId) { 201 ByteArrayInputStream pdu = new ByteArrayInputStream(mData); 202 boolean found = false; 203 int offset = 204 cdmaGetParameterOffset(BEARER_DATA) 205 + 2; // Add to offset the BEARER_DATA parameter id and length bytes 206 pdu.skip(offset); 207 try { 208 209 while (pdu.available() > 0) { 210 int currentId = pdu.read(); 211 int currentLen = pdu.read(); 212 213 if (currentId == subParameterId) { 214 found = true; 215 break; 216 } else { 217 pdu.skip(currentLen); 218 offset += 2 + currentLen; 219 } 220 } 221 pdu.close(); 222 } catch (Exception e) { 223 ContentProfileErrorReportUtils.report( 224 BluetoothProfile.MAP, 225 BluetoothProtoEnums.BLUETOOTH_MAP_SMS_PDU, 226 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 227 1); 228 Log.e(TAG, "cdmaGetParameterOffset: ", e); 229 } 230 231 if (found) { 232 return offset; 233 } else { 234 return 0; 235 } 236 } 237 cdmaChangeToDeliverPdu(long date)238 public void cdmaChangeToDeliverPdu(long date) { 239 /* Things to change: 240 * - Message Type in bearer data (Not the overall point-to-point type) 241 * - Change address ID from destination to originating (sub addresses are not used) 242 * - A time stamp is not mandatory. 243 */ 244 int offset; 245 if (mData == null) { 246 throw new IllegalArgumentException("Unable to convert PDU to Deliver type"); 247 } 248 offset = cdmaGetParameterOffset(DESTINATION_ADDRESS); 249 if (mData.length < offset) { 250 throw new IllegalArgumentException("Unable to convert PDU to Deliver type"); 251 } 252 mData[offset] = ORIGINATING_ADDRESS; 253 254 offset = cdmaGetParameterOffset(DESTINATION_SUB_ADDRESS); 255 if (mData.length < offset) { 256 throw new IllegalArgumentException("Unable to convert PDU to Deliver type"); 257 } 258 mData[offset] = ORIGINATING_SUB_ADDRESS; 259 260 offset = cdmaGetSubParameterOffset(BEARER_DATA_MSG_ID); 261 262 if (mData.length > (2 + offset)) { 263 int tmp = 264 mData[offset + 2] 265 & 0xff; // Skip the subParam ID and length, and read the first byte. 266 // Mask out the type 267 tmp &= 0x0f; 268 // Set the new type 269 tmp |= ((MESSAGE_TYPE_DELIVER << 4) & 0xf0); 270 // Store the result 271 mData[offset + 2] = (byte) tmp; 272 273 } else { 274 throw new IllegalArgumentException("Unable to convert PDU to Deliver type"); 275 } 276 /* TODO: Do we need to change anything in the user data? Not sure if the user 277 data is 278 * just encoded using GSM encoding, or it is an actual GSM submit PDU 279 * embedded 280 * in the user data? 281 */ 282 } 283 284 private static final byte TP_MIT_DELIVER = 0x00; // bit 0 and 1 285 private static final byte TP_MMS_NO_MORE = 0x04; // bit 2 286 private static final byte TP_RP_NO_REPLY_PATH = 0x00; // bit 7 287 private static final byte TP_UDHI_MASK = 0x40; // bit 6 288 private static final byte TP_SRI_NO_REPORT = 0x00; // bit 5 289 gsmSubmitGetTpPidOffset()290 private int gsmSubmitGetTpPidOffset() { 291 /* calculate the offset to TP_PID. 292 * The TP-DA has variable length, and the length excludes the 2 byte length and type 293 * headers. 294 * The TP-DA is two bytes within the PDU */ 295 // data[2] is the number of semi-octets in the phone number (ceil result) 296 int offset = 2 + ((mData[2] + 1) & 0xff) / 2 + 2; 297 // max length of TP_DA is 12 bytes + two byte offset. 298 if ((offset > mData.length) || (offset > (2 + 12))) { 299 throw new IllegalArgumentException( 300 "wrongly formatted gsm submit PDU. offset = " + offset); 301 } 302 return offset; 303 } 304 gsmSubmitGetTpDcs()305 public int gsmSubmitGetTpDcs() { 306 return mData[gsmSubmitGetTpDcsOffset()] & 0xff; 307 } 308 gsmSubmitHasUserDataHeader()309 public boolean gsmSubmitHasUserDataHeader() { 310 return ((mData[0] & 0xff) & TP_UDHI_MASK) == TP_UDHI_MASK; 311 } 312 gsmSubmitGetTpDcsOffset()313 private int gsmSubmitGetTpDcsOffset() { 314 return gsmSubmitGetTpPidOffset() + 1; 315 } 316 gsmSubmitGetTpUdlOffset()317 private int gsmSubmitGetTpUdlOffset() { 318 switch (((mData[0] & 0xff) & (0x08 | 0x04)) >> 2) { 319 case 0: // Not TP-VP present 320 return gsmSubmitGetTpPidOffset() + 2; 321 case 1: // TP-VP relative format 322 return gsmSubmitGetTpPidOffset() + 2 + 1; 323 case 2: // TP-VP enhanced format 324 case 3: // TP-VP absolute format 325 break; 326 } 327 return gsmSubmitGetTpPidOffset() + 2 + 7; 328 } 329 gsmSubmitGetTpUdOffset()330 private int gsmSubmitGetTpUdOffset() { 331 return gsmSubmitGetTpUdlOffset() + 1; 332 } 333 gsmDecodeUserDataHeader()334 public void gsmDecodeUserDataHeader() { 335 ByteArrayInputStream pdu = new ByteArrayInputStream(mData); 336 337 pdu.skip(gsmSubmitGetTpUdlOffset()); 338 int userDataLength = pdu.read(); 339 if (gsmSubmitHasUserDataHeader()) { 340 int userDataHeaderLength = pdu.read(); 341 342 // This part is only needed to extract the language info, hence only needed for 7 343 // bit encoding 344 if (mEncoding == SmsConstants.ENCODING_7BIT) { 345 byte[] udh = new byte[userDataHeaderLength]; 346 try { 347 pdu.read(udh); 348 } catch (IOException e) { 349 ContentProfileErrorReportUtils.report( 350 BluetoothProfile.MAP, 351 BluetoothProtoEnums.BLUETOOTH_MAP_SMS_PDU, 352 BluetoothStatsLog 353 .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 354 2); 355 Log.w(TAG, "unable to read userDataHeader", e); 356 } 357 int[] tableValue = getTableFromByteArray(udh); 358 mLanguageTable = tableValue[0]; 359 mLanguageShiftTable = tableValue[1]; 360 361 int headerBits = (userDataHeaderLength + 1) * 8; 362 int headerSeptets = headerBits / 7; 363 headerSeptets += (headerBits % 7) > 0 ? 1 : 0; 364 mUserDataSeptetPadding = (headerSeptets * 7) - headerBits; 365 mMsgSeptetCount = userDataLength - headerSeptets; 366 } 367 mUserDataMsgOffset = 368 gsmSubmitGetTpUdOffset() 369 + userDataHeaderLength 370 + 1; // Add the byte containing the length 371 } else { 372 mUserDataSeptetPadding = 0; 373 mMsgSeptetCount = userDataLength; 374 mUserDataMsgOffset = gsmSubmitGetTpUdOffset(); 375 } 376 Log.v(TAG, "encoding:" + mEncoding); 377 Log.v(TAG, "msgSeptetCount:" + mMsgSeptetCount); 378 Log.v(TAG, "userDataSeptetPadding:" + mUserDataSeptetPadding); 379 Log.v(TAG, "languageShiftTable:" + mLanguageShiftTable); 380 Log.v(TAG, "languageTable:" + mLanguageTable); 381 Log.v(TAG, "userDataMsgOffset:" + mUserDataMsgOffset); 382 } 383 384 @SuppressWarnings("JavaUtilDate") // TODO: b/365629730 -- prefer Instant or LocalDate gsmWriteDate(ByteArrayOutputStream header, long time)385 private static void gsmWriteDate(ByteArrayOutputStream header, long time) { 386 SimpleDateFormat format = new SimpleDateFormat("yyMMddHHmmss", Locale.ROOT); 387 Date date = new Date(time); 388 String timeStr = format.format(date); // Format to YYMMDDTHHMMSS UTC time 389 Log.v(TAG, "Generated time string: " + timeStr); 390 byte[] timeChars = timeStr.getBytes(StandardCharsets.US_ASCII); 391 392 for (int i = 0, n = timeStr.length(); i < n; i += 2) { 393 header.write( 394 (timeChars[i + 1] - 0x30) << 4 395 | (timeChars[i] - 0x30)); // Offset from ascii char to decimal value 396 } 397 398 Calendar cal = Calendar.getInstance(); 399 int offset = 400 (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) 401 / (15 * 60 * 1000); /* offset in quarters of an hour */ 402 String offsetString; 403 if (offset < 0) { 404 offsetString = String.format(Locale.ROOT, "%1$02d", -(offset)); 405 char[] offsetChars = offsetString.toCharArray(); 406 header.write((offsetChars[1] - 0x30) << 4 | 0x40 | (offsetChars[0] - 0x30)); 407 } else { 408 offsetString = String.format(Locale.ROOT, "%1$02d", offset); 409 char[] offsetChars = offsetString.toCharArray(); 410 header.write((offsetChars[1] - 0x30) << 4 | (offsetChars[0] - 0x30)); 411 } 412 } 413 414 /* private void gsmSubmitExtractUserData() { 415 int userDataLength = data[gsmSubmitGetTpUdlOffset()]; 416 userData = new byte[userDataLength]; 417 System.arraycopy(userData, 0, data, gsmSubmitGetTpUdOffset(), userDataLength); 418 419 }*/ 420 421 /** 422 * Change the GSM Submit Pdu data in this object to a deliver PDU: - Build the new header 423 * with deliver PDU type, originator and time stamp. - Extract encoding details from the 424 * submit PDU - Extract user data length and user data from the submitPdu - Build the new 425 * PDU 426 * 427 * @param date the time stamp to include (The value is the number of milliseconds since Jan. 428 * 1, 1970 GMT.) 429 * @param originator the phone number to include in the deliver PDU header. Any undesired 430 * characters, such as '-' will be striped from this string. 431 */ gsmChangeToDeliverPdu(long date, String originator)432 public void gsmChangeToDeliverPdu(long date, String originator) { 433 ByteArrayOutputStream newPdu = 434 new ByteArrayOutputStream(22); // 22 is the max length of the deliver pdu header 435 byte[] encodedAddress; 436 int userDataLength = 0; 437 try { 438 newPdu.write( 439 TP_MIT_DELIVER 440 | TP_MMS_NO_MORE 441 | TP_RP_NO_REPLY_PATH 442 | TP_SRI_NO_REPORT 443 | ((mData[0] & 0xff) & TP_UDHI_MASK)); 444 encodedAddress = 445 PhoneNumberUtils.networkPortionToCalledPartyBCDWithLength(originator); 446 if (encodedAddress != null) { 447 int padding = 448 (encodedAddress[encodedAddress.length - 1] & 0xf0) == 0xf0 ? 1 : 0; 449 encodedAddress[0] = 450 (byte) 451 ((encodedAddress[0] - 1) * 2 452 - padding); // Convert from octet length to semi octet 453 // length 454 // Insert originator address into the header - this includes the length 455 newPdu.write(encodedAddress); 456 } else { 457 newPdu.write(0); /* zero length */ 458 newPdu.write(0x81); /* International type */ 459 } 460 461 newPdu.write(mData[gsmSubmitGetTpPidOffset()]); 462 newPdu.write(mData[gsmSubmitGetTpDcsOffset()]); 463 // Generate service center time stamp 464 gsmWriteDate(newPdu, date); 465 userDataLength = (mData[gsmSubmitGetTpUdlOffset()] & 0xff); 466 newPdu.write(userDataLength); 467 // Copy the pdu user data - keep in mind that the userDataLength is not the 468 // length in bytes for 7-bit encoding. 469 newPdu.write( 470 mData, gsmSubmitGetTpUdOffset(), mData.length - gsmSubmitGetTpUdOffset()); 471 } catch (IOException e) { 472 ContentProfileErrorReportUtils.report( 473 BluetoothProfile.MAP, 474 BluetoothProtoEnums.BLUETOOTH_MAP_SMS_PDU, 475 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 476 3); 477 Log.e(TAG, "", e); 478 throw new IllegalArgumentException("Failed to change type to deliver PDU."); 479 } 480 mData = newPdu.toByteArray(); 481 } 482 483 /* SMS encoding to bmessage strings */ 484 485 /** get the encoding type as a bMessage string */ getEncodingString()486 public String getEncodingString() { 487 if (mType == SMS_TYPE_GSM) { 488 switch (mEncoding) { 489 case SmsMessage.ENCODING_7BIT: 490 if (mLanguageTable == 0) { 491 return "G-7BIT"; 492 } else { 493 return "G-7BITEXT"; 494 } 495 case SmsMessage.ENCODING_8BIT: 496 return "G-8BIT"; 497 case SmsMessage.ENCODING_16BIT: 498 return "G-16BIT"; 499 case SmsMessage.ENCODING_UNKNOWN: 500 default: 501 return ""; 502 } 503 } else /* SMS_TYPE_CDMA */ { 504 switch (mEncoding) { 505 case SmsMessage.ENCODING_7BIT: 506 return "C-7ASCII"; 507 case SmsMessage.ENCODING_8BIT: 508 return "C-8BIT"; 509 case SmsMessage.ENCODING_16BIT: 510 return "C-UNICODE"; 511 case SmsMessage.ENCODING_KSC5601: 512 return "C-KOREAN"; 513 case SmsMessage.ENCODING_UNKNOWN: 514 default: 515 return ""; 516 } 517 } 518 } 519 } 520 521 private static int sConcatenatedRef = new Random().nextInt(256); 522 getNextConcatenatedRef()523 protected static int getNextConcatenatedRef() { 524 sConcatenatedRef += 1; 525 return sConcatenatedRef; 526 } 527 getSubmitPdus(Context context, String messageText, String address)528 public static List<SmsPdu> getSubmitPdus(Context context, String messageText, String address) { 529 /* Use the generic GSM/CDMA SMS Message functionality within Android to generate the 530 * SMS PDU's as once generated to send the SMS message. 531 */ 532 533 int activePhone = context.getSystemService(TelephonyManager.class).getCurrentPhoneType(); 534 int phoneType; 535 int[] ted = SmsMessage.calculateLength((CharSequence) messageText, false); 536 537 SmsPdu newPdu; 538 String destinationAddress; 539 int msgCount = ted[0]; 540 int encoding; 541 int languageTable; 542 int languageShiftTable; 543 int refNumber = getNextConcatenatedRef() & 0x00FF; 544 SmsManager smsMng = SmsManager.getDefault(); 545 List<String> smsFragments = smsMng.divideMessage(messageText); 546 List<SmsPdu> pdus = new ArrayList<>(msgCount); 547 byte[] data; 548 549 // Default to GSM, as this code should not be used, if we neither have CDMA not GSM. 550 phoneType = (activePhone == PHONE_TYPE_CDMA) ? SMS_TYPE_CDMA : SMS_TYPE_GSM; 551 encoding = ted[3]; 552 languageTable = ted[4]; 553 languageShiftTable = ted[5]; 554 destinationAddress = PhoneNumberUtils.stripSeparators(address); 555 if (destinationAddress == null || destinationAddress.length() < 2) { 556 destinationAddress = 557 "12"; // Ensure we add a number at least 2 digits as specified in the GSM spec. 558 } 559 560 if (msgCount == 1) { 561 data = 562 SmsMessage.getSubmitPdu(null, destinationAddress, smsFragments.get(0), false) 563 .encodedMessage; 564 newPdu = new SmsPdu(data, encoding, phoneType, languageTable); 565 pdus.add(newPdu); 566 } else { 567 /* This code is a reduced copy of the actual code used in the Android SMS sub system, 568 * hence the comments have been left untouched. */ 569 for (int i = 0; i < msgCount; i++) { 570 data = 571 SmsMessage.getSubmitPduEncodedMessage( 572 phoneType == SMS_TYPE_GSM, 573 destinationAddress, 574 smsFragments.get(i), 575 encoding, 576 languageTable, 577 languageShiftTable, 578 refNumber, 579 i + 1, 580 msgCount); 581 newPdu = new SmsPdu(data, encoding, phoneType, languageTable); 582 pdus.add(newPdu); 583 } 584 } 585 586 return pdus; 587 } 588 589 /** 590 * Generate a list of deliver PDUs. The messageText and address parameters must be different 591 * from null, for CDMA the date can be omitted (and will be ignored if supplied) 592 * 593 * @param messageText The text to include. 594 * @param address The originator address. 595 * @param date The delivery time stamp. 596 */ getDeliverPdus( Context context, String messageText, String address, long date)597 public static List<SmsPdu> getDeliverPdus( 598 Context context, String messageText, String address, long date) { 599 List<SmsPdu> deliverPdus = getSubmitPdus(context, messageText, address); 600 601 /* 602 * For CDMA the only difference between deliver and submit pdus are the messageType, 603 * which is set in encodeMessageId, (the higher 4 bits of the 1st byte 604 * of the Message identification sub parameter data.) and the address type. 605 * 606 * For GSM, a larger part of the header needs to be generated. 607 */ 608 for (SmsPdu currentPdu : deliverPdus) { 609 if (currentPdu.getType() == SMS_TYPE_CDMA) { 610 currentPdu.cdmaChangeToDeliverPdu(date); 611 } else { 612 /* SMS_TYPE_GSM */ 613 currentPdu.gsmChangeToDeliverPdu(date, address); 614 } 615 } 616 617 return deliverPdus; 618 } 619 620 /** 621 * The decoding only supports decoding the actual textual content of the PDU received from the 622 * MAP client. (As the Android system has no interface to send pre encoded PDUs) The destination 623 * address must be extracted from the bmessage vCard(s). 624 */ decodePdu(byte[] data, int type)625 public static String decodePdu(byte[] data, int type) { 626 String ret = ""; 627 if (type == SMS_TYPE_CDMA) { 628 /* This is able to handle both submit and deliver PDUs */ 629 SmsMessage smsMessage = SmsMessage.createFromNativeSmsSubmitPdu(data, true); 630 if (smsMessage != null) { 631 ret = smsMessage.getMessageBody(); 632 } 633 } else { 634 /* For GSM, there is no submit pdu decoder, and most parser utils are private, and 635 only minded for submit pdus */ 636 ret = gsmParseSubmitPdu(data); 637 } 638 return ret; 639 } 640 641 /* At the moment we do not support using a SC-address. Use this function to strip off 642 * the SC-address before parsing it to the SmsPdu. (this was added in errata 4335) 643 */ gsmStripOffScAddress(byte[] data)644 private static byte[] gsmStripOffScAddress(byte[] data) { 645 /* The format of a native GSM SMS is: <sc-address><pdu> where sc-address is: 646 * <length-byte><type-byte><number-bytes> */ 647 int addressLength = data[0] & 0xff; // Treat the byte value as an unsigned value 648 // We could verify that the address-length is no longer than 11 bytes 649 if (addressLength >= data.length) { 650 throw new IllegalArgumentException( 651 "Length of address exceeds the length of the PDU data."); 652 } 653 int pduLength = data.length - (1 + addressLength); 654 byte[] newData = new byte[pduLength]; 655 System.arraycopy(data, 1 + addressLength, newData, 0, pduLength); 656 return newData; 657 } 658 gsmParseSubmitPdu(byte[] data)659 private static String gsmParseSubmitPdu(byte[] data) { 660 /* Things to do: 661 * - extract hasUsrData bit 662 * - extract TP-DCS -> Character set, compressed etc. 663 * - extract user data header to get the language properties 664 * - extract user data 665 * - decode the string */ 666 // Strip off the SC-address before parsing 667 SmsPdu pdu = new SmsPdu(gsmStripOffScAddress(data), SMS_TYPE_GSM); 668 boolean userDataCompressed = false; 669 int dataCodingScheme = pdu.gsmSubmitGetTpDcs(); 670 int encodingType = SmsConstants.ENCODING_UNKNOWN; 671 String messageBody = null; 672 673 // Look up the data encoding scheme 674 if ((dataCodingScheme & 0x80) == 0) { 675 // Bits 7..4 == 0xxx 676 userDataCompressed = (0 != (dataCodingScheme & 0x20)); 677 678 if (userDataCompressed) { 679 Log.w( 680 TAG, 681 "4 - Unsupported SMS data coding scheme " 682 + "(compression) " 683 + (dataCodingScheme & 0xff)); 684 ContentProfileErrorReportUtils.report( 685 BluetoothProfile.MAP, 686 BluetoothProtoEnums.BLUETOOTH_MAP_SMS_PDU, 687 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN, 688 4); 689 } else { 690 switch ((dataCodingScheme >> 2) & 0x3) { 691 case 0: // GSM 7 bit default alphabet 692 encodingType = SmsConstants.ENCODING_7BIT; 693 break; 694 695 case 2: // UCS 2 (16bit) 696 encodingType = SmsConstants.ENCODING_16BIT; 697 break; 698 699 case 1: // 8 bit data 700 case 3: // reserved 701 Log.w( 702 TAG, 703 "1 - Unsupported SMS data coding scheme " 704 + (dataCodingScheme & 0xff)); 705 ContentProfileErrorReportUtils.report( 706 BluetoothProfile.MAP, 707 BluetoothProtoEnums.BLUETOOTH_MAP_SMS_PDU, 708 BluetoothStatsLog 709 .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN, 710 5); 711 encodingType = SmsConstants.ENCODING_8BIT; 712 break; 713 } 714 } 715 } else if ((dataCodingScheme & 0xf0) == 0xf0) { 716 userDataCompressed = false; 717 718 if (0 == (dataCodingScheme & 0x04)) { 719 // GSM 7 bit default alphabet 720 encodingType = SmsConstants.ENCODING_7BIT; 721 } else { 722 // 8 bit data 723 encodingType = SmsConstants.ENCODING_8BIT; 724 } 725 } else if ((dataCodingScheme & 0xF0) == 0xC0 726 || (dataCodingScheme & 0xF0) == 0xD0 727 || (dataCodingScheme & 0xF0) == 0xE0) { 728 // 3GPP TS 23.038 V7.0.0 (2006-03) section 4 729 730 // 0xC0 == 7 bit, don't store 731 // 0xD0 == 7 bit, store 732 // 0xE0 == UCS-2, store 733 734 if ((dataCodingScheme & 0xF0) == 0xE0) { 735 encodingType = SmsConstants.ENCODING_16BIT; 736 } else { 737 encodingType = SmsConstants.ENCODING_7BIT; 738 } 739 740 userDataCompressed = false; 741 742 // bit 0x04 reserved 743 } else if ((dataCodingScheme & 0xC0) == 0x80) { 744 // 3GPP TS 23.038 V7.0.0 (2006-03) section 4 745 // 0x80..0xBF == Reserved coding groups 746 if (dataCodingScheme == 0x84) { 747 // This value used for KSC5601 by carriers in Korea. 748 encodingType = SmsConstants.ENCODING_KSC5601; 749 } else { 750 Log.w(TAG, "5 - Unsupported SMS data coding scheme " + (dataCodingScheme & 0xff)); 751 ContentProfileErrorReportUtils.report( 752 BluetoothProfile.MAP, 753 BluetoothProtoEnums.BLUETOOTH_MAP_SMS_PDU, 754 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN, 755 6); 756 } 757 } else { 758 Log.w(TAG, "3 - Unsupported SMS data coding scheme " + (dataCodingScheme & 0xff)); 759 ContentProfileErrorReportUtils.report( 760 BluetoothProfile.MAP, 761 BluetoothProtoEnums.BLUETOOTH_MAP_SMS_PDU, 762 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN, 763 7); 764 } 765 766 pdu.setEncoding(encodingType); 767 pdu.gsmDecodeUserDataHeader(); 768 769 try { 770 switch (encodingType) { 771 case SmsConstants.ENCODING_UNKNOWN: 772 case SmsConstants.ENCODING_8BIT: 773 Log.w(TAG, "Unknown encoding type: " + encodingType); 774 ContentProfileErrorReportUtils.report( 775 BluetoothProfile.MAP, 776 BluetoothProtoEnums.BLUETOOTH_MAP_SMS_PDU, 777 BluetoothStatsLog 778 .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN, 779 8); 780 messageBody = null; 781 break; 782 783 case SmsConstants.ENCODING_7BIT: 784 messageBody = 785 GsmAlphabet.gsm7BitPackedToString( 786 pdu.getData(), 787 pdu.getUserDataMsgOffset(), 788 pdu.getMsgSeptetCount(), 789 pdu.getUserDataSeptetPadding(), 790 pdu.getLanguageTable(), 791 pdu.getLanguageShiftTable()); 792 Log.i(TAG, "Decoded as 7BIT: " + messageBody); 793 794 break; 795 796 case SmsConstants.ENCODING_16BIT: 797 messageBody = 798 new String( 799 pdu.getData(), 800 pdu.getUserDataMsgOffset(), 801 pdu.getUserDataMsgSize(), 802 "utf-16"); 803 Log.i(TAG, "Decoded as 16BIT: " + messageBody); 804 break; 805 806 case SmsConstants.ENCODING_KSC5601: 807 messageBody = 808 new String( 809 pdu.getData(), 810 pdu.getUserDataMsgOffset(), 811 pdu.getUserDataMsgSize(), 812 "KSC5601"); 813 Log.i(TAG, "Decoded as KSC5601: " + messageBody); 814 break; 815 } 816 } catch (UnsupportedEncodingException e) { 817 ContentProfileErrorReportUtils.report( 818 BluetoothProfile.MAP, 819 BluetoothProtoEnums.BLUETOOTH_MAP_SMS_PDU, 820 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 821 9); 822 Log.e(TAG, "Unsupported encoding type???", e); // This should never happen. 823 return null; 824 } 825 826 return messageBody; 827 } 828 getTableFromByteArray(byte[] data)829 private static int[] getTableFromByteArray(byte[] data) { 830 ByteArrayInputStream inStream = new ByteArrayInputStream(data); 831 /* tableValue[0]: languageTable tableValue[1]: languageShiftTable */ 832 int[] tableValue = new int[2]; 833 while (inStream.available() > 0) { 834 int id = inStream.read(); 835 int length = inStream.read(); 836 switch (id) { 837 case ELT_ID_NATIONAL_LANGUAGE_SINGLE_SHIFT: 838 tableValue[1] = inStream.read(); 839 break; 840 case ELT_ID_NATIONAL_LANGUAGE_LOCKING_SHIFT: 841 tableValue[0] = inStream.read(); 842 break; 843 default: 844 inStream.skip(length); 845 } 846 } 847 return tableValue; 848 } 849 850 private static class SmsConstants { 851 // User data text encoding code unit size 852 public static final int ENCODING_UNKNOWN = 0; 853 854 public static final int ENCODING_7BIT = 1; 855 public static final int ENCODING_8BIT = 2; 856 public static final int ENCODING_16BIT = 3; 857 858 // This value is not defined in global standard. Only in Korea, this is used. 859 public static final int ENCODING_KSC5601 = 4; 860 } 861 } 862