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