• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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