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 java.io.ByteArrayOutputStream; 18 import java.io.File; 19 import java.io.FileInputStream; 20 import java.io.FileNotFoundException; 21 import java.io.FileOutputStream; 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.io.UnsupportedEncodingException; 25 import java.util.ArrayList; 26 27 import android.os.Environment; 28 import android.telephony.PhoneNumberUtils; 29 import android.util.Log; 30 31 import com.android.bluetooth.map.BluetoothMapUtils.TYPE; 32 33 public abstract class BluetoothMapbMessage { 34 35 protected static String TAG = "BluetoothMapbMessage"; 36 protected static final boolean D = BluetoothMapService.DEBUG; 37 protected static final boolean V = BluetoothMapService.VERBOSE; 38 39 private static final String VERSION = "VERSION:1.0"; 40 41 public static int INVALID_VALUE = -1; 42 43 protected int mAppParamCharset = BluetoothMapAppParams.INVALID_VALUE_PARAMETER; 44 45 /* BMSG attributes */ 46 private String mStatus = null; // READ/UNREAD 47 protected TYPE mType = null; // SMS/MMS/EMAIL 48 49 private String mFolder = null; 50 51 /* BBODY attributes */ 52 private long mPartId = INVALID_VALUE; 53 protected String mEncoding = null; 54 protected String mCharset = null; 55 private String mLanguage = null; 56 57 private int mBMsgLength = INVALID_VALUE; 58 59 private ArrayList<vCard> mOriginator = null; 60 private ArrayList<vCard> mRecipient = null; 61 62 63 public static class vCard { 64 /* VCARD attributes */ 65 private String mVersion; 66 private String mName = null; 67 private String mFormattedName = null; 68 private String[] mPhoneNumbers = {}; 69 private String[] mEmailAddresses = {}; 70 private int mEnvLevel = 0; 71 72 /** 73 * Construct a version 3.0 vCard 74 * @param name Structured 75 * @param formattedName Formatted name 76 * @param phoneNumbers a String[] of phone numbers 77 * @param emailAddresses a String[] of email addresses 78 * @param the bmessage envelope level (0 is the top/most outer level) 79 */ 80 public vCard(String name, String formattedName, String[] phoneNumbers, 81 String[] emailAddresses, int envLevel) { 82 this.mEnvLevel = envLevel; 83 this.mVersion = "3.0"; 84 this.mName = name != null ? name : ""; 85 this.mFormattedName = formattedName != null ? formattedName : ""; 86 setPhoneNumbers(phoneNumbers); 87 if (emailAddresses != null) 88 this.mEmailAddresses = emailAddresses; 89 } 90 91 /** 92 * Construct a version 2.1 vCard 93 * @param name Structured name 94 * @param phoneNumbers a String[] of phone numbers 95 * @param emailAddresses a String[] of email addresses 96 * @param the bmessage envelope level (0 is the top/most outer level) 97 */ 98 public vCard(String name, String[] phoneNumbers, 99 String[] emailAddresses, int envLevel) { 100 this.mEnvLevel = envLevel; 101 this.mVersion = "2.1"; 102 this.mName = name != null ? name : ""; 103 setPhoneNumbers(phoneNumbers); 104 if (emailAddresses != null) 105 this.mEmailAddresses = emailAddresses; 106 } 107 108 /** 109 * Construct a version 3.0 vCard 110 * @param name Structured name 111 * @param formattedName Formatted name 112 * @param phoneNumbers a String[] of phone numbers 113 * @param emailAddresses a String[] of email addresses 114 */ 115 public vCard(String name, String formattedName, String[] phoneNumbers, String[] emailAddresses) { 116 this.mVersion = "3.0"; 117 this.mName = (name != null) ? name : ""; 118 this.mFormattedName = (formattedName != null) ? formattedName : ""; 119 setPhoneNumbers(phoneNumbers); 120 if (emailAddresses != null) 121 this.mEmailAddresses = emailAddresses; 122 } 123 124 /** 125 * Construct a version 2.1 vCard 126 * @param name Structured Name 127 * @param phoneNumbers a String[] of phone numbers 128 * @param emailAddresses a String[] of email addresses 129 */ 130 public vCard(String name, String[] phoneNumbers, String[] emailAddresses) { 131 this.mVersion = "2.1"; 132 this.mName = name != null ? name : ""; 133 setPhoneNumbers(phoneNumbers); 134 if (emailAddresses != null) 135 this.mEmailAddresses = emailAddresses; 136 } 137 138 private void setPhoneNumbers(String[] numbers) { 139 if(numbers != null && numbers.length > 0) { 140 mPhoneNumbers = new String[numbers.length]; 141 for(int i = 0, n = numbers.length; i < n; i++){ 142 String networkNumber = PhoneNumberUtils.extractNetworkPortion(numbers[i]); 143 /* extractNetworkPortion can return N if the number is a service "number" = a string 144 * with the a name in (i.e. "Some-Tele-company" would return N because of the N in compaNy) 145 * Hence we need to check if the number is actually a string with alpha chars. 146 * */ 147 Boolean alpha = PhoneNumberUtils.stripSeparators(numbers[i]).matches("[0-9]*[a-zA-Z]+[0-9]*"); 148 if(networkNumber != null && networkNumber.length() > 1 && !alpha) { 149 mPhoneNumbers[i] = networkNumber; 150 } else { 151 mPhoneNumbers[i] = numbers[i]; 152 } 153 } 154 } 155 } 156 157 public String getFirstPhoneNumber() { 158 if(mPhoneNumbers.length > 0) { 159 return mPhoneNumbers[0]; 160 } else 161 return null; 162 } 163 164 public int getEnvLevel() { 165 return mEnvLevel; 166 } 167 168 public String getName() { 169 return mName; 170 } 171 172 public String getFirstEmail() { 173 if(mEmailAddresses.length > 0) { 174 return mEmailAddresses[0]; 175 } else 176 return null; 177 } 178 179 public void encode(StringBuilder sb) 180 { 181 sb.append("BEGIN:VCARD").append("\r\n"); 182 sb.append("VERSION:").append(mVersion).append("\r\n"); 183 if(mVersion.equals("3.0") && mFormattedName != null) 184 { 185 sb.append("FN:").append(mFormattedName).append("\r\n"); 186 } 187 if (mName != null) 188 sb.append("N:").append(mName).append("\r\n"); 189 for(String phoneNumber : mPhoneNumbers) 190 { 191 sb.append("TEL:").append(phoneNumber).append("\r\n"); 192 } 193 for(String emailAddress : mEmailAddresses) 194 { 195 sb.append("EMAIL:").append(emailAddress).append("\r\n"); 196 } 197 sb.append("END:VCARD").append("\r\n"); 198 } 199 200 /** 201 * Parse a vCard from a BMgsReader, where a line containing "BEGIN:VCARD" have just been read. 202 * @param reader 203 * @param mOriginator 204 * @return 205 */ 206 public static vCard parseVcard(BMsgReader reader, int envLevel) { 207 String formattedName = null; 208 String name = null; 209 ArrayList<String> phoneNumbers = null; 210 ArrayList<String> emailAddresses = null; 211 String[] parts; 212 String line = reader.getLineEnforce(); 213 214 while(!line.contains("END:VCARD")) { 215 line = line.trim(); 216 if(line.startsWith("N:")){ 217 parts = line.split("[^\\\\]:"); // Split on "un-escaped" ':' 218 if(parts.length == 2) { 219 name = parts[1]; 220 } else 221 name = ""; 222 } 223 else if(line.startsWith("FN:")){ 224 parts = line.split("[^\\\\]:"); // Split on "un-escaped" ':' 225 if(parts.length == 2) { 226 formattedName = parts[1]; 227 } else 228 formattedName = ""; 229 } 230 else if(line.startsWith("TEL:")){ 231 parts = line.split("[^\\\\]:"); // Split on "un-escaped" ':' 232 if(parts.length == 2) { 233 String[] subParts = parts[1].split("[^\\\\];"); 234 if(phoneNumbers == null) 235 phoneNumbers = new ArrayList<String>(1); 236 phoneNumbers.add(subParts[subParts.length-1]); // only keep actual phone number 237 } else {} 238 // Empty phone number - ignore 239 } 240 else if(line.startsWith("EMAIL:")){ 241 parts = line.split("[^\\\\]:"); // Split on "un-escaped" : 242 if(parts.length == 2) { 243 String[] subParts = parts[1].split("[^\\\\];"); 244 if(emailAddresses == null) 245 emailAddresses = new ArrayList<String>(1); 246 emailAddresses.add(subParts[subParts.length-1]); // only keep actual email address 247 } else {} 248 // Empty email address entry - ignore 249 } 250 line = reader.getLineEnforce(); 251 } 252 return new vCard(name, formattedName, 253 phoneNumbers == null? null : phoneNumbers.toArray(new String[phoneNumbers.size()]), 254 emailAddresses == null ? null : emailAddresses.toArray(new String[emailAddresses.size()]), 255 envLevel); 256 } 257 }; 258 259 private static class BMsgReader { 260 InputStream mInStream; 261 public BMsgReader(InputStream is) 262 { 263 this.mInStream = is; 264 } 265 266 private byte[] getLineAsBytes() { 267 int readByte; 268 269 /* TODO: Actually the vCard spec. allows to break lines by using a newLine 270 * followed by a white space character(space or tab). Not sure this is a good idea to implement 271 * as the Bluetooth MAP spec. illustrates vCards using tab alignment, hence actually 272 * showing an invalid vCard format... 273 * If we read such a folded line, the folded part will be skipped in the parser 274 * UPDATE: Check if we actually do unfold before parsing the input stream 275 */ 276 277 ByteArrayOutputStream output = new ByteArrayOutputStream(); 278 try { 279 while ((readByte = mInStream.read()) != -1) { 280 if (readByte == '\r') { 281 if ((readByte = mInStream.read()) != -1 && readByte == '\n') { 282 if(output.size() == 0) 283 continue; /* Skip empty lines */ 284 else 285 break; 286 } else { 287 output.write('\r'); 288 } 289 } else if (readByte == '\n' && output.size() == 0) { 290 /* Empty line - skip */ 291 continue; 292 } 293 294 output.write(readByte); 295 } 296 } catch (IOException e) { 297 Log.w(TAG, e); 298 return null; 299 } 300 return output.toByteArray(); 301 } 302 303 /** 304 * Read a line of text from the BMessage. 305 * @return the next line of text, or null at end of file, or if UTF-8 is not supported. 306 */ 307 public String getLine() { 308 try { 309 byte[] line = getLineAsBytes(); 310 if (line.length == 0) 311 return null; 312 else 313 return new String(line, "UTF-8"); 314 } catch (UnsupportedEncodingException e) { 315 Log.w(TAG, e); 316 return null; 317 } 318 } 319 320 /** 321 * same as getLine(), but throws an exception, if we run out of lines. 322 * Use this function when ever more lines are needed for the bMessage to be complete. 323 * @return the next line 324 */ 325 public String getLineEnforce() { 326 String line = getLine(); 327 if (line == null) 328 throw new IllegalArgumentException("Bmessage too short"); 329 330 return line; 331 } 332 333 334 /** 335 * Reads a line from the InputStream, and examines if the subString 336 * matches the line read. 337 * @param subString 338 * The string to match against the line. 339 * @throws IllegalArgumentException 340 * If the expected substring is not found. 341 * 342 */ 343 public void expect(String subString) throws IllegalArgumentException{ 344 String line = getLine(); 345 if(line == null || subString == null){ 346 throw new IllegalArgumentException("Line or substring is null"); 347 }else if(!line.toUpperCase().contains(subString.toUpperCase())) 348 throw new IllegalArgumentException("Expected \"" + subString + "\" in: \"" + line + "\""); 349 } 350 351 /** 352 * Same as expect(String), but with two strings. 353 * @param subString 354 * @param subString2 355 * @throws IllegalArgumentException 356 * If one or all of the strings are not found. 357 */ 358 public void expect(String subString, String subString2) throws IllegalArgumentException{ 359 String line = getLine(); 360 if(!line.toUpperCase().contains(subString.toUpperCase())) 361 throw new IllegalArgumentException("Expected \"" + subString + "\" in: \"" + line + "\""); 362 if(!line.toUpperCase().contains(subString2.toUpperCase())) 363 throw new IllegalArgumentException("Expected \"" + subString + "\" in: \"" + line + "\""); 364 } 365 366 /** 367 * Read a part of the bMessage as raw data. 368 * @param length the number of bytes to read 369 * @return the byte[] containing the number of bytes or null if an error occurs or EOF is reached 370 * before length bytes have been read. 371 */ 372 public byte[] getDataBytes(int length) { 373 byte[] data = new byte[length]; 374 try { 375 int bytesRead; 376 int offset=0; 377 while ((bytesRead = mInStream.read(data, offset, length-offset)) != (length - offset)) { 378 if(bytesRead == -1) 379 return null; 380 offset += bytesRead; 381 } 382 } catch (IOException e) { 383 Log.w(TAG, e); 384 return null; 385 } 386 return data; 387 } 388 }; 389 390 public BluetoothMapbMessage(){ 391 392 } 393 394 public static BluetoothMapbMessage parse(InputStream bMsgStream, int appParamCharset) throws IllegalArgumentException{ 395 BMsgReader reader; 396 String line = ""; 397 BluetoothMapbMessage newBMsg = null; 398 boolean status = false; 399 boolean statusFound = false; 400 TYPE type = null; 401 String folder = null; 402 403 /* This section is used for debug. It will write the incoming message to a file on the SD-card, 404 * hence should only be used for test/debug. 405 * If an error occurs, it will result in a OBEX_HTTP_PRECON_FAILED to be send to the client, 406 * even though the message might be formatted correctly, hence only enable this code for test. */ 407 if(V) { 408 /* Read the entire stream into a file on the SD card*/ 409 File sdCard = Environment.getExternalStorageDirectory(); 410 File dir = new File (sdCard.getAbsolutePath() + "/bluetooth/log/"); 411 dir.mkdirs(); 412 File file = new File(dir, "receivedBMessage.txt"); 413 FileOutputStream outStream = null; 414 boolean failed = false; 415 int writtenLen = 0; 416 417 try { 418 outStream = new FileOutputStream(file, false); /* overwrite if it does already exist */ 419 420 byte[] buffer = new byte[4*1024]; 421 int len = 0; 422 while ((len = bMsgStream.read(buffer)) > 0) { 423 outStream.write(buffer, 0, len); 424 writtenLen += len; 425 } 426 } catch (FileNotFoundException e) { 427 Log.e(TAG,"Unable to create output stream",e); 428 } catch (IOException e) { 429 Log.e(TAG,"Failed to copy the received message",e); 430 if(writtenLen != 0) 431 failed = true; /* We failed to write the complete file, hence the received message is lost... */ 432 } finally { 433 if(outStream != null) 434 try { 435 outStream.close(); 436 } catch (IOException e) { 437 } 438 } 439 440 /* Return if we corrupted the incoming bMessage. */ 441 if(failed) { 442 throw new IllegalArgumentException(); /* terminate this function with an error. */ 443 } 444 445 if (outStream == null) { 446 /* We failed to create the the log-file, just continue using the original bMsgStream. */ 447 } else { 448 /* overwrite the bMsgStream using the file written to the SD-Card */ 449 try { 450 bMsgStream.close(); 451 } catch (IOException e) { 452 /* Ignore if we cannot close the stream. */ 453 } 454 /* Open the file and overwrite bMsgStream to read from the file */ 455 try { 456 bMsgStream = new FileInputStream(file); 457 } catch (FileNotFoundException e) { 458 Log.e(TAG,"Failed to open the bMessage file", e); 459 throw new IllegalArgumentException(); /* terminate this function with an error. */ 460 } 461 } 462 Log.i(TAG, "The incoming bMessage have been dumped to " + file.getAbsolutePath()); 463 } /* End of if(V) log-section */ 464 465 reader = new BMsgReader(bMsgStream); 466 reader.expect("BEGIN:BMSG"); 467 reader.expect("VERSION","1.0"); 468 469 line = reader.getLineEnforce(); 470 // Parse the properties - which end with either a VCARD or a BENV 471 while(!line.contains("BEGIN:VCARD") && !line.contains("BEGIN:BENV")) { 472 if(line.contains("STATUS")){ 473 String arg[] = line.split(":"); 474 if (arg != null && arg.length == 2) { 475 if (arg[1].trim().equals("READ")) { 476 status = true; 477 } else if (arg[1].trim().equals("UNREAD")) { 478 status =false; 479 } else { 480 throw new IllegalArgumentException("Wrong value in 'STATUS': " + arg[1]); 481 } 482 } else { 483 throw new IllegalArgumentException("Missing value for 'STATUS': " + line); 484 } 485 } 486 if(line.contains("TYPE")) { 487 String arg[] = line.split(":"); 488 if (arg != null && arg.length == 2) { 489 String value = arg[1].trim(); 490 type = TYPE.valueOf(value); // Will throw IllegalArgumentException if value is wrong 491 if(appParamCharset == BluetoothMapAppParams.CHARSET_NATIVE 492 && type != TYPE.SMS_CDMA && type != TYPE.SMS_GSM) { 493 throw new IllegalArgumentException("Native appParamsCharset only supported for SMS"); 494 } 495 switch(type) { 496 case SMS_CDMA: 497 case SMS_GSM: 498 newBMsg = new BluetoothMapbMessageSms(); 499 break; 500 case MMS: 501 newBMsg = new BluetoothMapbMessageMms(); 502 break; 503 case EMAIL: 504 newBMsg = new BluetoothMapbMessageEmail(); 505 break; 506 default: 507 break; 508 } 509 } else { 510 throw new IllegalArgumentException("Missing value for 'TYPE':" + line); 511 } 512 } 513 if(line.contains("FOLDER")) { 514 String[] arg = line.split(":"); 515 if (arg != null && arg.length == 2) { 516 folder = arg[1].trim(); 517 } 518 // This can be empty for push message - hence ignore if there is no value 519 } 520 line = reader.getLineEnforce(); 521 } 522 if(newBMsg == null) 523 throw new IllegalArgumentException("Missing bMessage TYPE: - unable to parse body-content"); 524 newBMsg.setType(type); 525 newBMsg.mAppParamCharset = appParamCharset; 526 if(folder != null) 527 newBMsg.setCompleteFolder(folder); 528 if(statusFound) 529 newBMsg.setStatus(status); 530 531 // Now check for originator VCARDs 532 while(line.contains("BEGIN:VCARD")){ 533 if(D) Log.d(TAG,"Decoding vCard"); 534 newBMsg.addOriginator(vCard.parseVcard(reader,0)); 535 line = reader.getLineEnforce(); 536 } 537 if(line.contains("BEGIN:BENV")) { 538 newBMsg.parseEnvelope(reader, 0); 539 } else 540 throw new IllegalArgumentException("Bmessage has no BEGIN:BENV - line:" + line); 541 542 /* TODO: Do we need to validate the END:* tags? They are only needed if someone puts additional info 543 * below the END:MSG - in which case we don't handle it. 544 * We need to parse the message based on the length field, to ensure MAP 1.0 compatibility, 545 * since this spec. do not suggest to escape the end-tag if it occurs inside the message text. 546 */ 547 548 try { 549 bMsgStream.close(); 550 } catch (IOException e) { 551 /* Ignore if we cannot close the stream. */ 552 } 553 554 return newBMsg; 555 } 556 557 private void parseEnvelope(BMsgReader reader, int level) { 558 String line; 559 line = reader.getLineEnforce(); 560 if(D) Log.d(TAG,"Decoding envelope level " + level); 561 562 while(line.contains("BEGIN:VCARD")){ 563 if(D) Log.d(TAG,"Decoding recipient vCard level " + level); 564 if(mRecipient == null) 565 mRecipient = new ArrayList<vCard>(1); 566 mRecipient.add(vCard.parseVcard(reader, level)); 567 line = reader.getLineEnforce(); 568 } 569 if(line.contains("BEGIN:BENV")) { 570 if(D) Log.d(TAG,"Decoding nested envelope"); 571 parseEnvelope(reader, ++level); // Nested BENV 572 } 573 if(line.contains("BEGIN:BBODY")){ 574 if(D) Log.d(TAG,"Decoding bbody"); 575 parseBody(reader); 576 } 577 } 578 579 private void parseBody(BMsgReader reader) { 580 String line; 581 line = reader.getLineEnforce(); 582 while(!line.contains("END:")) { 583 if(line.contains("PARTID:")) { 584 String arg[] = line.split(":"); 585 if (arg != null && arg.length == 2) { 586 try { 587 mPartId = Long.parseLong(arg[1].trim()); 588 } catch (NumberFormatException e) { 589 throw new IllegalArgumentException("Wrong value in 'PARTID': " + arg[1]); 590 } 591 } else { 592 throw new IllegalArgumentException("Missing value for 'PARTID': " + line); 593 } 594 } 595 else if(line.contains("ENCODING:")) { 596 String arg[] = line.split(":"); 597 if (arg != null && arg.length == 2) { 598 mEncoding = arg[1].trim(); 599 // If needed validation will be done when the value is used 600 } else { 601 throw new IllegalArgumentException("Missing value for 'ENCODING': " + line); 602 } 603 } 604 else if(line.contains("CHARSET:")) { 605 String arg[] = line.split(":"); 606 if (arg != null && arg.length == 2) { 607 mCharset = arg[1].trim(); 608 // If needed validation will be done when the value is used 609 } else { 610 throw new IllegalArgumentException("Missing value for 'CHARSET': " + line); 611 } 612 } 613 else if(line.contains("LANGUAGE:")) { 614 String arg[] = line.split(":"); 615 if (arg != null && arg.length == 2) { 616 mLanguage = arg[1].trim(); 617 // If needed validation will be done when the value is used 618 } else { 619 throw new IllegalArgumentException("Missing value for 'LANGUAGE': " + line); 620 } 621 } 622 else if(line.contains("LENGTH:")) { 623 String arg[] = line.split(":"); 624 if (arg != null && arg.length == 2) { 625 try { 626 mBMsgLength = Integer.parseInt(arg[1].trim()); 627 } catch (NumberFormatException e) { 628 throw new IllegalArgumentException("Wrong value in 'LENGTH': " + arg[1]); 629 } 630 } else { 631 throw new IllegalArgumentException("Missing value for 'LENGTH': " + line); 632 } 633 } 634 else if(line.contains("BEGIN:MSG")) { 635 if(mBMsgLength == INVALID_VALUE) 636 throw new IllegalArgumentException("Missing value for 'LENGTH'. " + 637 "Unable to read remaining part of the message"); 638 /* For SMS: Encoding of MSG is always UTF-8 compliant, regardless of any properties, 639 since PDUs are encodes as hex-strings */ 640 /* PTS has a bug regarding the message length, and sets it 2 bytes too short, hence 641 * using the length field to determine the amount of data to read, might not be the 642 * best solution. 643 * Since errata ???(bluetooth.org is down at the moment) introduced escaping of END:MSG 644 * in the actual message content, it is now safe to use the END:MSG tag as terminator, 645 * and simply ignore the length field.*/ 646 byte[] rawData = reader.getDataBytes(mBMsgLength - (line.getBytes().length + 2)); // 2 added to compensate for the removed \r\n 647 String data; 648 try { 649 data = new String(rawData, "UTF-8"); 650 if(V) { 651 Log.v(TAG,"MsgLength: " + mBMsgLength); 652 Log.v(TAG,"line.getBytes().length: " + line.getBytes().length); 653 String debug = line.replaceAll("\\n", "<LF>\n"); 654 debug = debug.replaceAll("\\r", "<CR>"); 655 Log.v(TAG,"The line: \"" + debug + "\""); 656 debug = data.replaceAll("\\n", "<LF>\n"); 657 debug = debug.replaceAll("\\r", "<CR>"); 658 Log.v(TAG,"The msgString: \"" + debug + "\""); 659 } 660 } catch (UnsupportedEncodingException e) { 661 Log.w(TAG,e); 662 throw new IllegalArgumentException("Unable to convert to UTF-8"); 663 } 664 /* Decoding of MSG: 665 * 1) split on "\r\nEND:MSG\r\n" 666 * 2) delete "BEGIN:MSG\r\n" for each msg 667 * 3) replace any occurrence of "\END:MSG" with "END:MSG" 668 * 4) based on charset from application properties either store as String[] or decode to raw PDUs 669 * */ 670 String messages[] = data.split("\r\nEND:MSG\r\n"); 671 parseMsgInit(); 672 for(int i = 0; i < messages.length; i++) { 673 messages[i] = messages[i].replaceFirst("^BEGIN:MSG\r\n", ""); 674 messages[i] = messages[i].replaceAll("\r\n([/]*)/END\\:MSG", "\r\n$1END:MSG"); 675 messages[i] = messages[i].trim(); 676 parseMsgPart(messages[i]); 677 } 678 } 679 line = reader.getLineEnforce(); 680 } 681 } 682 683 /** 684 * Parse the 'message' part of <bmessage-body-content>" 685 * @param msgPart 686 */ 687 public abstract void parseMsgPart(String msgPart); 688 /** 689 * Set initial values before parsing - will be called is a message body is found 690 * during parsing. 691 */ 692 public abstract void parseMsgInit(); 693 694 public abstract byte[] encode() throws UnsupportedEncodingException; 695 696 public void setStatus(boolean read) { 697 if(read) 698 this.mStatus = "READ"; 699 else 700 this.mStatus = "UNREAD"; 701 } 702 703 public void setType(TYPE type) { 704 this.mType = type; 705 } 706 707 /** 708 * @return the type 709 */ 710 public TYPE getType() { 711 return mType; 712 } 713 714 public void setCompleteFolder(String folder) { 715 this.mFolder = folder; 716 } 717 718 public void setFolder(String folder) { 719 this.mFolder = "telecom/msg/" + folder; 720 } 721 722 public String getFolder() { 723 return mFolder; 724 } 725 726 727 public void setEncoding(String encoding) { 728 this.mEncoding = encoding; 729 } 730 731 public ArrayList<vCard> getOriginators() { 732 return mOriginator; 733 } 734 735 public void addOriginator(vCard originator) { 736 if(this.mOriginator == null) 737 this.mOriginator = new ArrayList<vCard>(); 738 this.mOriginator.add(originator); 739 } 740 741 /** 742 * Add a version 3.0 vCard with a formatted name 743 * @param name e.g. Bonde;Casper 744 * @param formattedName e.g. "Casper Bonde" 745 * @param phoneNumbers 746 * @param emailAddresses 747 */ 748 public void addOriginator(String name, String formattedName, String[] phoneNumbers, String[] emailAddresses) { 749 if(mOriginator == null) 750 mOriginator = new ArrayList<vCard>(); 751 mOriginator.add(new vCard(name, formattedName, phoneNumbers, emailAddresses)); 752 } 753 754 /** Add a version 2.1 vCard with only a name. 755 * 756 * @param name e.g. Bonde;Casper 757 * @param phoneNumbers 758 * @param emailAddresses 759 */ 760 public void addOriginator(String name, String[] phoneNumbers, String[] emailAddresses) { 761 if(mOriginator == null) 762 mOriginator = new ArrayList<vCard>(); 763 mOriginator.add(new vCard(name, phoneNumbers, emailAddresses)); 764 } 765 766 public ArrayList<vCard> getRecipients() { 767 return mRecipient; 768 } 769 770 public void setRecipient(vCard recipient) { 771 if(this.mRecipient == null) 772 this.mRecipient = new ArrayList<vCard>(); 773 this.mRecipient.add(recipient); 774 } 775 776 public void addRecipient(String name, String formattedName, String[] phoneNumbers, String[] emailAddresses) { 777 if(mRecipient == null) 778 mRecipient = new ArrayList<vCard>(); 779 mRecipient.add(new vCard(name, formattedName, phoneNumbers, emailAddresses)); 780 } 781 782 public void addRecipient(String name, String[] phoneNumbers, String[] emailAddresses) { 783 if(mRecipient == null) 784 mRecipient = new ArrayList<vCard>(); 785 mRecipient.add(new vCard(name, phoneNumbers, emailAddresses)); 786 } 787 788 /** 789 * Convert a byte[] of data to a hex string representation, converting each nibble to the corresponding 790 * hex char. 791 * NOTE: There is not need to escape instances of "\r\nEND:MSG" in the binary data represented as a string 792 * as only the characters [0-9] and [a-f] is used. 793 * @param pduData the byte-array of data. 794 * @param scAddressData the byte-array of the encoded sc-Address. 795 * @return the resulting string. 796 */ 797 protected String encodeBinary(byte[] pduData, byte[] scAddressData) { 798 StringBuilder out = new StringBuilder((pduData.length + scAddressData.length)*2); 799 for(int i = 0; i < scAddressData.length; i++) { 800 out.append(Integer.toString((scAddressData[i] >> 4) & 0x0f,16)); // MS-nibble first 801 out.append(Integer.toString( scAddressData[i] & 0x0f,16)); 802 } 803 for(int i = 0; i < pduData.length; i++) { 804 out.append(Integer.toString((pduData[i] >> 4) & 0x0f,16)); // MS-nibble first 805 out.append(Integer.toString( pduData[i] & 0x0f,16)); 806 /*out.append(Integer.toHexString(data[i]));*/ /* This is the same as above, but does not include the needed 0's 807 e.g. it converts the value 3 to "3" and not "03" */ 808 } 809 return out.toString(); 810 } 811 812 /** 813 * Decodes a binary hex-string encoded UTF-8 string to the represented binary data set. 814 * @param data The string representation of the data - must have an even number of characters. 815 * @return the byte[] represented in the data. 816 */ 817 protected byte[] decodeBinary(String data) { 818 byte[] out = new byte[data.length()/2]; 819 String value; 820 if(D) Log.d(TAG,"Decoding binary data: START:" + data + ":END"); 821 for(int i = 0, j = 0, n = out.length; i < n; i++) 822 { 823 value = data.substring(j++, ++j); // same as data.substring(2*i, 2*i+1+1) - substring() uses end-1 for last index 824 out[i] = (byte)(Integer.valueOf(value, 16) & 0xff); 825 } 826 if(D) { 827 StringBuilder sb = new StringBuilder(out.length); 828 for(int i = 0, n = out.length; i < n; i++) 829 { 830 sb.append(String.format("%02X",out[i] & 0xff)); 831 } 832 Log.d(TAG,"Decoded binary data: START:" + sb.toString() + ":END"); 833 } 834 return out; 835 } 836 837 public byte[] encodeGeneric(ArrayList<byte[]> bodyFragments) throws UnsupportedEncodingException 838 { 839 StringBuilder sb = new StringBuilder(256); 840 byte[] msgStart, msgEnd; 841 sb.append("BEGIN:BMSG").append("\r\n"); 842 sb.append(VERSION).append("\r\n"); 843 sb.append("STATUS:").append(mStatus).append("\r\n"); 844 sb.append("TYPE:").append(mType.name()).append("\r\n"); 845 if(mFolder.length() > 512) 846 sb.append("FOLDER:").append(mFolder.substring(mFolder.length()-512, mFolder.length())).append("\r\n"); 847 else 848 sb.append("FOLDER:").append(mFolder).append("\r\n"); 849 if(mOriginator != null){ 850 for(vCard element : mOriginator) 851 element.encode(sb); 852 } 853 /* If we need the three levels of env. at some point - we do have a level in the 854 * vCards that could be used to determine the levels of the envelope. 855 */ 856 857 sb.append("BEGIN:BENV").append("\r\n"); 858 if(mRecipient != null){ 859 for(vCard element : mRecipient) { 860 if(V) Log.v(TAG, "encodeGeneric: recipient email" + element.getFirstEmail()); 861 element.encode(sb); 862 } 863 } 864 sb.append("BEGIN:BBODY").append("\r\n"); 865 if(mEncoding != null && mEncoding != "") 866 sb.append("ENCODING:").append(mEncoding).append("\r\n"); 867 if(mCharset != null && mCharset != "") 868 sb.append("CHARSET:").append(mCharset).append("\r\n"); 869 870 871 int length = 0; 872 /* 22 is the length of the 'BEGIN:MSG' and 'END:MSG' + 3*CRLF */ 873 for (byte[] fragment : bodyFragments) { 874 length += fragment.length + 22; 875 } 876 sb.append("LENGTH:").append(length).append("\r\n"); 877 878 // Extract the initial part of the bMessage string 879 msgStart = sb.toString().getBytes("UTF-8"); 880 881 sb = new StringBuilder(31); 882 sb.append("END:BBODY").append("\r\n"); 883 sb.append("END:BENV").append("\r\n"); 884 sb.append("END:BMSG").append("\r\n"); 885 886 msgEnd = sb.toString().getBytes("UTF-8"); 887 888 try { 889 890 ByteArrayOutputStream stream = new ByteArrayOutputStream(msgStart.length + msgEnd.length + length); 891 stream.write(msgStart); 892 893 for (byte[] fragment : bodyFragments) { 894 stream.write("BEGIN:MSG\r\n".getBytes("UTF-8")); 895 stream.write(fragment); 896 stream.write("\r\nEND:MSG\r\n".getBytes("UTF-8")); 897 } 898 stream.write(msgEnd); 899 900 if(V) Log.v(TAG,stream.toString("UTF-8")); 901 return stream.toByteArray(); 902 } catch (IOException e) { 903 Log.w(TAG,e); 904 return null; 905 } 906 } 907 } 908