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