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 while(!line.contains("END:")) { 685 if(line.contains("PARTID:")) { 686 String arg[] = line.split(":"); 687 if (arg != null && arg.length == 2) { 688 try { 689 mPartId = Long.parseLong(arg[1].trim()); 690 } catch (NumberFormatException e) { 691 throw new IllegalArgumentException("Wrong value in 'PARTID': " + arg[1]); 692 } 693 } else { 694 throw new IllegalArgumentException("Missing value for 'PARTID': " + line); 695 } 696 } 697 else if(line.contains("ENCODING:")) { 698 String arg[] = line.split(":"); 699 if (arg != null && arg.length == 2) { 700 mEncoding = arg[1].trim(); 701 // If needed validation will be done when the value is used 702 } else { 703 throw new IllegalArgumentException("Missing value for 'ENCODING': " + line); 704 } 705 } 706 else if(line.contains("CHARSET:")) { 707 String arg[] = line.split(":"); 708 if (arg != null && arg.length == 2) { 709 mCharset = arg[1].trim(); 710 // If needed validation will be done when the value is used 711 } else { 712 throw new IllegalArgumentException("Missing value for 'CHARSET': " + line); 713 } 714 } 715 else if(line.contains("LANGUAGE:")) { 716 String arg[] = line.split(":"); 717 if (arg != null && arg.length == 2) { 718 mLanguage = arg[1].trim(); 719 // If needed validation will be done when the value is used 720 } else { 721 throw new IllegalArgumentException("Missing value for 'LANGUAGE': " + line); 722 } 723 } 724 else if(line.contains("LENGTH:")) { 725 String arg[] = line.split(":"); 726 if (arg != null && arg.length == 2) { 727 try { 728 mBMsgLength = Integer.parseInt(arg[1].trim()); 729 } catch (NumberFormatException e) { 730 throw new IllegalArgumentException("Wrong value in 'LENGTH': " + arg[1]); 731 } 732 } else { 733 throw new IllegalArgumentException("Missing value for 'LENGTH': " + line); 734 } 735 } 736 else if(line.contains("BEGIN:MSG")) { 737 if(mBMsgLength == INVALID_VALUE) 738 throw new IllegalArgumentException("Missing value for 'LENGTH'. " + 739 "Unable to read remaining part of the message"); 740 /* For SMS: Encoding of MSG is always UTF-8 compliant, regardless of any properties, 741 since PDUs are encodes as hex-strings */ 742 /* PTS has a bug regarding the message length, and sets it 2 bytes too short, hence 743 * using the length field to determine the amount of data to read, might not be the 744 * best solution. 745 * Since errata ???(bluetooth.org is down at the moment) introduced escaping of 746 * END:MSG in the actual message content, it is now safe to use the END:MSG tag 747 * as terminator, and simply ignore the length field.*/ 748 749 /* 2 added to compensate for the removed \r\n */ 750 byte[] rawData = reader.getDataBytes(mBMsgLength - (line.getBytes().length + 2)); 751 String data; 752 try { 753 data = new String(rawData, "UTF-8"); 754 if(V) { 755 Log.v(TAG,"MsgLength: " + mBMsgLength); 756 Log.v(TAG,"line.getBytes().length: " + line.getBytes().length); 757 String debug = line.replaceAll("\\n", "<LF>\n"); 758 debug = debug.replaceAll("\\r", "<CR>"); 759 Log.v(TAG,"The line: \"" + debug + "\""); 760 debug = data.replaceAll("\\n", "<LF>\n"); 761 debug = debug.replaceAll("\\r", "<CR>"); 762 Log.v(TAG,"The msgString: \"" + debug + "\""); 763 } 764 } catch (UnsupportedEncodingException e) { 765 Log.w(TAG,e); 766 throw new IllegalArgumentException("Unable to convert to UTF-8"); 767 } 768 /* Decoding of MSG: 769 * 1) split on "\r\nEND:MSG\r\n" 770 * 2) delete "BEGIN:MSG\r\n" for each msg 771 * 3) replace any occurrence of "\END:MSG" with "END:MSG" 772 * 4) based on charset from application properties either store as String[] or 773 * decode to raw PDUs 774 * */ 775 String messages[] = data.split("\r\nEND:MSG\r\n"); 776 parseMsgInit(); 777 for(int i = 0; i < messages.length; i++) { 778 messages[i] = messages[i].replaceFirst("^BEGIN:MSG\r\n", ""); 779 messages[i] = messages[i].replaceAll("\r\n([/]*)/END\\:MSG", "\r\n$1END:MSG"); 780 messages[i] = messages[i].trim(); 781 parseMsgPart(messages[i]); 782 } 783 } 784 line = reader.getLineEnforce(); 785 } 786 } 787 788 /** 789 * Parse the 'message' part of <bmessage-body-content>" 790 * @param msgPart 791 */ parseMsgPart(String msgPart)792 public abstract void parseMsgPart(String msgPart); 793 /** 794 * Set initial values before parsing - will be called is a message body is found 795 * during parsing. 796 */ parseMsgInit()797 public abstract void parseMsgInit(); 798 encode()799 public abstract byte[] encode() throws UnsupportedEncodingException; 800 setStatus(boolean read)801 public void setStatus(boolean read) { 802 if(read) 803 this.mStatus = "READ"; 804 else 805 this.mStatus = "UNREAD"; 806 } 807 setType(TYPE type)808 public void setType(TYPE type) { 809 this.mType = type; 810 } 811 812 /** 813 * @return the type 814 */ getType()815 public TYPE getType() { 816 return mType; 817 } 818 setCompleteFolder(String folder)819 public void setCompleteFolder(String folder) { 820 this.mFolder = folder; 821 } 822 setFolder(String folder)823 public void setFolder(String folder) { 824 this.mFolder = "telecom/msg/" + folder; 825 } 826 getFolder()827 public String getFolder() { 828 return mFolder; 829 } 830 831 setEncoding(String encoding)832 public void setEncoding(String encoding) { 833 this.mEncoding = encoding; 834 } 835 getOriginators()836 public ArrayList<vCard> getOriginators() { 837 return mOriginator; 838 } 839 addOriginator(vCard originator)840 public void addOriginator(vCard originator) { 841 if(this.mOriginator == null) 842 this.mOriginator = new ArrayList<vCard>(); 843 this.mOriginator.add(originator); 844 } 845 846 /** 847 * Add a version 3.0 vCard with a formatted name 848 * @param name e.g. Bonde;Casper 849 * @param formattedName e.g. "Casper Bonde" 850 * @param phoneNumbers 851 * @param emailAddresses 852 */ addOriginator(String name, String formattedName, String[] phoneNumbers, String[] emailAddresses, String[] btUids, String[] btUcis)853 public void addOriginator(String name, String formattedName, 854 String[] phoneNumbers, 855 String[] emailAddresses, 856 String[] btUids, 857 String[] btUcis) { 858 if(mOriginator == null) 859 mOriginator = new ArrayList<vCard>(); 860 mOriginator.add(new vCard(name, formattedName, phoneNumbers, 861 emailAddresses, btUids, btUcis)); 862 } 863 864 addOriginator(String[] btUcis, String[] btUids)865 public void addOriginator(String[] btUcis, String[] btUids) { 866 if(mOriginator == null) 867 mOriginator = new ArrayList<vCard>(); 868 mOriginator.add(new vCard(null,null,null,null,btUids, btUcis)); 869 } 870 871 872 /** Add a version 2.1 vCard with only a name. 873 * 874 * @param name e.g. Bonde;Casper 875 * @param phoneNumbers 876 * @param emailAddresses 877 */ addOriginator(String name, String[] phoneNumbers, String[] emailAddresses)878 public void addOriginator(String name, String[] phoneNumbers, String[] emailAddresses) { 879 if(mOriginator == null) 880 mOriginator = new ArrayList<vCard>(); 881 mOriginator.add(new vCard(name, phoneNumbers, emailAddresses)); 882 } 883 getRecipients()884 public ArrayList<vCard> getRecipients() { 885 return mRecipient; 886 } 887 setRecipient(vCard recipient)888 public void setRecipient(vCard recipient) { 889 if(this.mRecipient == null) 890 this.mRecipient = new ArrayList<vCard>(); 891 this.mRecipient.add(recipient); 892 } addRecipient(String[] btUcis, String[] btUids)893 public void addRecipient(String[] btUcis, String[] btUids) { 894 if(mRecipient == null) 895 mRecipient = new ArrayList<vCard>(); 896 mRecipient.add(new vCard(null,null,null,null,btUids, btUcis)); 897 } addRecipient(String name, String formattedName, String[] phoneNumbers, String[] emailAddresses, String[] btUids, String[] btUcis)898 public void addRecipient(String name, String formattedName, 899 String[] phoneNumbers, 900 String[] emailAddresses, 901 String[] btUids, 902 String[] btUcis) { 903 if(mRecipient == null) 904 mRecipient = new ArrayList<vCard>(); 905 mRecipient.add(new vCard(name, formattedName, phoneNumbers, 906 emailAddresses,btUids, btUcis)); 907 } 908 addRecipient(String name, String[] phoneNumbers, String[] emailAddresses)909 public void addRecipient(String name, String[] phoneNumbers, String[] emailAddresses) { 910 if(mRecipient == null) 911 mRecipient = new ArrayList<vCard>(); 912 mRecipient.add(new vCard(name, phoneNumbers, emailAddresses)); 913 } 914 915 /** 916 * Convert a byte[] of data to a hex string representation, converting each nibble to the 917 * corresponding hex char. 918 * NOTE: There is not need to escape instances of "\r\nEND:MSG" in the binary data represented 919 * as a string as only the characters [0-9] and [a-f] is used. 920 * @param pduData the byte-array of data. 921 * @param scAddressData the byte-array of the encoded sc-Address. 922 * @return the resulting string. 923 */ encodeBinary(byte[] pduData, byte[] scAddressData)924 protected String encodeBinary(byte[] pduData, byte[] scAddressData) { 925 StringBuilder out = new StringBuilder((pduData.length + scAddressData.length)*2); 926 for(int i = 0; i < scAddressData.length; i++) { 927 out.append(Integer.toString((scAddressData[i] >> 4) & 0x0f,16)); // MS-nibble first 928 out.append(Integer.toString( scAddressData[i] & 0x0f,16)); 929 } 930 for(int i = 0; i < pduData.length; i++) { 931 out.append(Integer.toString((pduData[i] >> 4) & 0x0f,16)); // MS-nibble first 932 out.append(Integer.toString( pduData[i] & 0x0f,16)); 933 /*out.append(Integer.toHexString(data[i]));*/ /* This is the same as above, but does not 934 * include the needed 0's 935 * e.g. it converts the value 3 to "3" 936 * and not "03" */ 937 } 938 return out.toString(); 939 } 940 941 /** 942 * Decodes a binary hex-string encoded UTF-8 string to the represented binary data set. 943 * @param data The string representation of the data - must have an even number of characters. 944 * @return the byte[] represented in the data. 945 */ decodeBinary(String data)946 protected byte[] decodeBinary(String data) { 947 byte[] out = new byte[data.length()/2]; 948 String value; 949 if(D) Log.d(TAG,"Decoding binary data: START:" + data + ":END"); 950 for(int i = 0, j = 0, n = out.length; i < n; i++) 951 { 952 value = data.substring(j++, ++j); 953 // same as data.substring(2*i, 2*i+1+1) - substring() uses end-1 for last index 954 out[i] = (byte)(Integer.valueOf(value, 16) & 0xff); 955 } 956 if(D) { 957 StringBuilder sb = new StringBuilder(out.length); 958 for(int i = 0, n = out.length; i < n; i++) 959 { 960 sb.append(String.format("%02X",out[i] & 0xff)); 961 } 962 Log.d(TAG,"Decoded binary data: START:" + sb.toString() + ":END"); 963 } 964 return out; 965 } 966 encodeGeneric(ArrayList<byte[]> bodyFragments)967 public byte[] encodeGeneric(ArrayList<byte[]> bodyFragments) throws UnsupportedEncodingException 968 { 969 StringBuilder sb = new StringBuilder(256); 970 byte[] msgStart, msgEnd; 971 sb.append("BEGIN:BMSG").append("\r\n"); 972 973 sb.append(mVersionString).append("\r\n"); 974 sb.append("STATUS:").append(mStatus).append("\r\n"); 975 sb.append("TYPE:").append(mType.name()).append("\r\n"); 976 if(mFolder.length() > 512) 977 sb.append("FOLDER:").append( 978 mFolder.substring(mFolder.length()-512, mFolder.length())).append("\r\n"); 979 else 980 sb.append("FOLDER:").append(mFolder).append("\r\n"); 981 if(!mVersionString.contains("1.0")){ 982 sb.append("EXTENDEDDATA:").append("\r\n"); 983 } 984 if(mOriginator != null){ 985 for(vCard element : mOriginator) 986 element.encode(sb); 987 } 988 /* If we need the three levels of env. at some point - we do have a level in the 989 * vCards that could be used to determine the levels of the envelope. 990 */ 991 992 sb.append("BEGIN:BENV").append("\r\n"); 993 if(mRecipient != null){ 994 for(vCard element : mRecipient) { 995 if(V) Log.v(TAG, "encodeGeneric: recipient email" + element.getFirstEmail()); 996 element.encode(sb); 997 } 998 } 999 sb.append("BEGIN:BBODY").append("\r\n"); 1000 if(mEncoding != null && mEncoding != "") 1001 sb.append("ENCODING:").append(mEncoding).append("\r\n"); 1002 if(mCharset != null && mCharset != "") 1003 sb.append("CHARSET:").append(mCharset).append("\r\n"); 1004 1005 1006 int length = 0; 1007 /* 22 is the length of the 'BEGIN:MSG' and 'END:MSG' + 3*CRLF */ 1008 for (byte[] fragment : bodyFragments) { 1009 length += fragment.length + 22; 1010 } 1011 sb.append("LENGTH:").append(length).append("\r\n"); 1012 1013 // Extract the initial part of the bMessage string 1014 msgStart = sb.toString().getBytes("UTF-8"); 1015 1016 sb = new StringBuilder(31); 1017 sb.append("END:BBODY").append("\r\n"); 1018 sb.append("END:BENV").append("\r\n"); 1019 sb.append("END:BMSG").append("\r\n"); 1020 1021 msgEnd = sb.toString().getBytes("UTF-8"); 1022 1023 try { 1024 1025 ByteArrayOutputStream stream = new ByteArrayOutputStream( 1026 msgStart.length + msgEnd.length + length); 1027 stream.write(msgStart); 1028 1029 for (byte[] fragment : bodyFragments) { 1030 stream.write("BEGIN:MSG\r\n".getBytes("UTF-8")); 1031 stream.write(fragment); 1032 stream.write("\r\nEND:MSG\r\n".getBytes("UTF-8")); 1033 } 1034 stream.write(msgEnd); 1035 1036 if(V) Log.v(TAG,stream.toString("UTF-8")); 1037 return stream.toByteArray(); 1038 } catch (IOException e) { 1039 Log.w(TAG,e); 1040 return null; 1041 } 1042 } 1043 } 1044