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