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