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