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