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