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         while(!line.contains("END:")) {
685             if(line.contains("PARTID:")) {
686                 String arg[] = line.split(":");
687                 if (arg != null && arg.length == 2) {
688                     try {
689                     mPartId = Long.parseLong(arg[1].trim());
690                     } catch (NumberFormatException e) {
691                         throw new IllegalArgumentException("Wrong value in 'PARTID': " + arg[1]);
692                     }
693                 } else {
694                     throw new IllegalArgumentException("Missing value for 'PARTID': " + line);
695                 }
696             }
697             else if(line.contains("ENCODING:")) {
698                 String arg[] = line.split(":");
699                 if (arg != null && arg.length == 2) {
700                     mEncoding = arg[1].trim();
701                     // If needed validation will be done when the value is used
702                 } else {
703                     throw new IllegalArgumentException("Missing value for 'ENCODING': " + line);
704                 }
705             }
706             else if(line.contains("CHARSET:")) {
707                 String arg[] = line.split(":");
708                 if (arg != null && arg.length == 2) {
709                     mCharset = arg[1].trim();
710                     // If needed validation will be done when the value is used
711                 } else {
712                     throw new IllegalArgumentException("Missing value for 'CHARSET': " + line);
713                 }
714             }
715             else if(line.contains("LANGUAGE:")) {
716                 String arg[] = line.split(":");
717                 if (arg != null && arg.length == 2) {
718                     mLanguage = arg[1].trim();
719                     // If needed validation will be done when the value is used
720                 } else {
721                     throw new IllegalArgumentException("Missing value for 'LANGUAGE': " + line);
722                 }
723             }
724             else if(line.contains("LENGTH:")) {
725                 String arg[] = line.split(":");
726                 if (arg != null && arg.length == 2) {
727                     try {
728                         mBMsgLength = Integer.parseInt(arg[1].trim());
729                     } catch (NumberFormatException e) {
730                         throw new IllegalArgumentException("Wrong value in 'LENGTH': " + arg[1]);
731                     }
732                 } else {
733                     throw new IllegalArgumentException("Missing value for 'LENGTH': " + line);
734                 }
735             }
736             else if(line.contains("BEGIN:MSG")) {
737                 if(mBMsgLength == INVALID_VALUE)
738                     throw new IllegalArgumentException("Missing value for 'LENGTH'. " +
739                             "Unable to read remaining part of the message");
740                 /* For SMS: Encoding of MSG is always UTF-8 compliant, regardless of any properties,
741                    since PDUs are encodes as hex-strings */
742                 /* PTS has a bug regarding the message length, and sets it 2 bytes too short, hence
743                  * using the length field to determine the amount of data to read, might not be the
744                  * best solution.
745                  * Since errata ???(bluetooth.org is down at the moment) introduced escaping of
746                  * END:MSG in the actual message content, it is now safe to use the END:MSG tag
747                  * as terminator, and simply ignore the length field.*/
748 
749                 /* 2 added to compensate for the removed \r\n */
750                 byte[] rawData = reader.getDataBytes(mBMsgLength - (line.getBytes().length + 2));
751                 String data;
752                 try {
753                     data = new String(rawData, "UTF-8");
754                     if(V) {
755                         Log.v(TAG,"MsgLength: " + mBMsgLength);
756                         Log.v(TAG,"line.getBytes().length: " + line.getBytes().length);
757                         String debug = line.replaceAll("\\n", "<LF>\n");
758                         debug = debug.replaceAll("\\r", "<CR>");
759                         Log.v(TAG,"The line: \"" + debug + "\"");
760                         debug = data.replaceAll("\\n", "<LF>\n");
761                         debug = debug.replaceAll("\\r", "<CR>");
762                         Log.v(TAG,"The msgString: \"" + debug + "\"");
763                     }
764                 } catch (UnsupportedEncodingException e) {
765                     Log.w(TAG,e);
766                     throw new IllegalArgumentException("Unable to convert to UTF-8");
767                 }
768                 /* Decoding of MSG:
769                  * 1) split on "\r\nEND:MSG\r\n"
770                  * 2) delete "BEGIN:MSG\r\n" for each msg
771                  * 3) replace any occurrence of "\END:MSG" with "END:MSG"
772                  * 4) based on charset from application properties either store as String[] or
773                  *    decode to raw PDUs
774                  * */
775                 String messages[] = data.split("\r\nEND:MSG\r\n");
776                 parseMsgInit();
777                 for(int i = 0; i < messages.length; i++) {
778                     messages[i] = messages[i].replaceFirst("^BEGIN:MSG\r\n", "");
779                     messages[i] = messages[i].replaceAll("\r\n([/]*)/END\\:MSG", "\r\n$1END:MSG");
780                     messages[i] = messages[i].trim();
781                     parseMsgPart(messages[i]);
782                 }
783             }
784             line = reader.getLineEnforce();
785         }
786     }
787 
788     /**
789      * Parse the 'message' part of <bmessage-body-content>"
790      * @param msgPart
791      */
parseMsgPart(String msgPart)792     public abstract void parseMsgPart(String msgPart);
793     /**
794      * Set initial values before parsing - will be called is a message body is found
795      * during parsing.
796      */
parseMsgInit()797     public abstract void parseMsgInit();
798 
encode()799     public abstract byte[] encode() throws UnsupportedEncodingException;
800 
setStatus(boolean read)801     public void setStatus(boolean read) {
802         if(read)
803             this.mStatus = "READ";
804         else
805             this.mStatus = "UNREAD";
806     }
807 
setType(TYPE type)808     public void setType(TYPE type) {
809         this.mType = type;
810     }
811 
812     /**
813      * @return the type
814      */
getType()815     public TYPE getType() {
816         return mType;
817     }
818 
setCompleteFolder(String folder)819     public void setCompleteFolder(String folder) {
820         this.mFolder = folder;
821     }
822 
setFolder(String folder)823     public void setFolder(String folder) {
824         this.mFolder = "telecom/msg/" + folder;
825     }
826 
getFolder()827     public String getFolder() {
828         return mFolder;
829     }
830 
831 
setEncoding(String encoding)832     public void setEncoding(String encoding) {
833         this.mEncoding = encoding;
834     }
835 
getOriginators()836     public ArrayList<vCard> getOriginators() {
837         return mOriginator;
838     }
839 
addOriginator(vCard originator)840     public void addOriginator(vCard originator) {
841         if(this.mOriginator == null)
842             this.mOriginator = new ArrayList<vCard>();
843         this.mOriginator.add(originator);
844     }
845 
846     /**
847      * Add a version 3.0 vCard with a formatted name
848      * @param name e.g. Bonde;Casper
849      * @param formattedName e.g. "Casper Bonde"
850      * @param phoneNumbers
851      * @param emailAddresses
852      */
addOriginator(String name, String formattedName, String[] phoneNumbers, String[] emailAddresses, String[] btUids, String[] btUcis)853     public void addOriginator(String name, String formattedName,
854                               String[] phoneNumbers,
855                               String[] emailAddresses,
856                               String[] btUids,
857                               String[] btUcis) {
858         if(mOriginator == null)
859             mOriginator = new ArrayList<vCard>();
860         mOriginator.add(new vCard(name, formattedName, phoneNumbers,
861                     emailAddresses, btUids, btUcis));
862     }
863 
864 
addOriginator(String[] btUcis, String[] btUids)865     public void addOriginator(String[] btUcis, String[] btUids) {
866         if(mOriginator == null)
867             mOriginator = new ArrayList<vCard>();
868         mOriginator.add(new vCard(null,null,null,null,btUids, btUcis));
869     }
870 
871 
872     /** Add a version 2.1 vCard with only a name.
873      *
874      * @param name e.g. Bonde;Casper
875      * @param phoneNumbers
876      * @param emailAddresses
877      */
addOriginator(String name, String[] phoneNumbers, String[] emailAddresses)878     public void addOriginator(String name, String[] phoneNumbers, String[] emailAddresses) {
879         if(mOriginator == null)
880             mOriginator = new ArrayList<vCard>();
881         mOriginator.add(new vCard(name, phoneNumbers, emailAddresses));
882     }
883 
getRecipients()884     public ArrayList<vCard> getRecipients() {
885         return mRecipient;
886     }
887 
setRecipient(vCard recipient)888     public void setRecipient(vCard recipient) {
889         if(this.mRecipient == null)
890             this.mRecipient = new ArrayList<vCard>();
891         this.mRecipient.add(recipient);
892     }
addRecipient(String[] btUcis, String[] btUids)893     public void addRecipient(String[] btUcis, String[] btUids) {
894         if(mRecipient == null)
895             mRecipient = new ArrayList<vCard>();
896         mRecipient.add(new vCard(null,null,null,null,btUids, btUcis));
897     }
addRecipient(String name, String formattedName, String[] phoneNumbers, String[] emailAddresses, String[] btUids, String[] btUcis)898     public void addRecipient(String name, String formattedName,
899                              String[] phoneNumbers,
900                              String[] emailAddresses,
901                              String[] btUids,
902                              String[] btUcis) {
903         if(mRecipient == null)
904             mRecipient = new ArrayList<vCard>();
905         mRecipient.add(new vCard(name, formattedName, phoneNumbers,
906                     emailAddresses,btUids, btUcis));
907     }
908 
addRecipient(String name, String[] phoneNumbers, String[] emailAddresses)909     public void addRecipient(String name, String[] phoneNumbers, String[] emailAddresses) {
910         if(mRecipient == null)
911             mRecipient = new ArrayList<vCard>();
912         mRecipient.add(new vCard(name, phoneNumbers, emailAddresses));
913     }
914 
915     /**
916      * Convert a byte[] of data to a hex string representation, converting each nibble to the
917      * corresponding hex char.
918      * NOTE: There is not need to escape instances of "\r\nEND:MSG" in the binary data represented
919      * as a string as only the characters [0-9] and [a-f] is used.
920      * @param pduData the byte-array of data.
921      * @param scAddressData the byte-array of the encoded sc-Address.
922      * @return the resulting string.
923      */
encodeBinary(byte[] pduData, byte[] scAddressData)924     protected String encodeBinary(byte[] pduData, byte[] scAddressData) {
925         StringBuilder out = new StringBuilder((pduData.length + scAddressData.length)*2);
926         for(int i = 0; i < scAddressData.length; i++) {
927             out.append(Integer.toString((scAddressData[i] >> 4) & 0x0f,16)); // MS-nibble first
928             out.append(Integer.toString( scAddressData[i]       & 0x0f,16));
929         }
930         for(int i = 0; i < pduData.length; i++) {
931             out.append(Integer.toString((pduData[i] >> 4) & 0x0f,16)); // MS-nibble first
932             out.append(Integer.toString( pduData[i]       & 0x0f,16));
933             /*out.append(Integer.toHexString(data[i]));*/ /* This is the same as above, but does not
934                                                            * include the needed 0's
935                                                            * e.g. it converts the value 3 to "3"
936                                                            * and not "03" */
937         }
938         return out.toString();
939     }
940 
941     /**
942      * Decodes a binary hex-string encoded UTF-8 string to the represented binary data set.
943      * @param data The string representation of the data - must have an even number of characters.
944      * @return the byte[] represented in the data.
945      */
decodeBinary(String data)946     protected byte[] decodeBinary(String data) {
947         byte[] out = new byte[data.length()/2];
948         String value;
949         if(D) Log.d(TAG,"Decoding binary data: START:" + data + ":END");
950         for(int i = 0, j = 0, n = out.length; i < n; i++)
951         {
952             value = data.substring(j++, ++j);
953             // same as data.substring(2*i, 2*i+1+1) - substring() uses end-1 for last index
954             out[i] = (byte)(Integer.valueOf(value, 16) & 0xff);
955         }
956         if(D) {
957             StringBuilder sb = new StringBuilder(out.length);
958             for(int i = 0, n = out.length; i < n; i++)
959             {
960                 sb.append(String.format("%02X",out[i] & 0xff));
961             }
962             Log.d(TAG,"Decoded binary data: START:" + sb.toString() + ":END");
963         }
964         return out;
965     }
966 
encodeGeneric(ArrayList<byte[]> bodyFragments)967     public byte[] encodeGeneric(ArrayList<byte[]> bodyFragments) throws UnsupportedEncodingException
968     {
969         StringBuilder sb = new StringBuilder(256);
970         byte[] msgStart, msgEnd;
971         sb.append("BEGIN:BMSG").append("\r\n");
972 
973         sb.append(mVersionString).append("\r\n");
974         sb.append("STATUS:").append(mStatus).append("\r\n");
975         sb.append("TYPE:").append(mType.name()).append("\r\n");
976         if(mFolder.length() > 512)
977             sb.append("FOLDER:").append(
978                     mFolder.substring(mFolder.length()-512, mFolder.length())).append("\r\n");
979         else
980             sb.append("FOLDER:").append(mFolder).append("\r\n");
981         if(!mVersionString.contains("1.0")){
982             sb.append("EXTENDEDDATA:").append("\r\n");
983         }
984         if(mOriginator != null){
985             for(vCard element : mOriginator)
986                 element.encode(sb);
987         }
988         /* If we need the three levels of env. at some point - we do have a level in the
989          *  vCards that could be used to determine the levels of the envelope.
990          */
991 
992         sb.append("BEGIN:BENV").append("\r\n");
993         if(mRecipient != null){
994             for(vCard element : mRecipient) {
995                 if(V) Log.v(TAG, "encodeGeneric: recipient email" + element.getFirstEmail());
996                 element.encode(sb);
997             }
998         }
999         sb.append("BEGIN:BBODY").append("\r\n");
1000         if(mEncoding != null && mEncoding != "")
1001             sb.append("ENCODING:").append(mEncoding).append("\r\n");
1002         if(mCharset != null && mCharset != "")
1003             sb.append("CHARSET:").append(mCharset).append("\r\n");
1004 
1005 
1006         int length = 0;
1007         /* 22 is the length of the 'BEGIN:MSG' and 'END:MSG' + 3*CRLF */
1008         for (byte[] fragment : bodyFragments) {
1009             length += fragment.length + 22;
1010         }
1011         sb.append("LENGTH:").append(length).append("\r\n");
1012 
1013         // Extract the initial part of the bMessage string
1014         msgStart = sb.toString().getBytes("UTF-8");
1015 
1016         sb = new StringBuilder(31);
1017         sb.append("END:BBODY").append("\r\n");
1018         sb.append("END:BENV").append("\r\n");
1019         sb.append("END:BMSG").append("\r\n");
1020 
1021         msgEnd = sb.toString().getBytes("UTF-8");
1022 
1023         try {
1024 
1025             ByteArrayOutputStream stream = new ByteArrayOutputStream(
1026                                                        msgStart.length + msgEnd.length + length);
1027             stream.write(msgStart);
1028 
1029             for (byte[] fragment : bodyFragments) {
1030                 stream.write("BEGIN:MSG\r\n".getBytes("UTF-8"));
1031                 stream.write(fragment);
1032                 stream.write("\r\nEND:MSG\r\n".getBytes("UTF-8"));
1033             }
1034             stream.write(msgEnd);
1035 
1036             if(V) Log.v(TAG,stream.toString("UTF-8"));
1037             return stream.toByteArray();
1038         } catch (IOException e) {
1039             Log.w(TAG,e);
1040             return null;
1041         }
1042     }
1043 }
1044