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