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