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