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