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