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