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