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