BluetoothMapbMessage.java revision 0612fada88b00a9896feeb9e7a16d5e3e41114f2
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*/
15package com.android.bluetooth.map;
16
17import java.io.ByteArrayOutputStream;
18import java.io.File;
19import java.io.FileInputStream;
20import java.io.FileNotFoundException;
21import java.io.FileOutputStream;
22import java.io.IOException;
23import java.io.InputStream;
24import java.io.UnsupportedEncodingException;
25import java.util.ArrayList;
26
27import android.os.Environment;
28import android.telephony.PhoneNumberUtils;
29import android.util.Log;
30
31import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
32
33public 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         */
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         */
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         */
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         */
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
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
174        public String getFirstPhoneNumber() {
175            if(mPhoneNumbers.length > 0) {
176                return mPhoneNumbers[0];
177            } else
178                return null;
179        }
180
181        public int getEnvLevel() {
182            return mEnvLevel;
183        }
184
185        public String getName() {
186            return mName;
187        }
188
189        public String getFirstEmail() {
190            if(mEmailAddresses.length > 0) {
191                return mEmailAddresses[0];
192            } else
193                return null;
194        }
195        public String getFirstBtUci() {
196            if(mBtUcis.length > 0) {
197                return mBtUcis[0];
198            } else
199                return null;
200        }
201
202        public String getFirstBtUid() {
203            if(mBtUids.length > 0) {
204                return mBtUids[0];
205            } else
206                return null;
207        }
208
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         */
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;
328        public BMsgReader(InputStream is)
329        {
330            this.mInStream = is;
331        }
332
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         */
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         */
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         */
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         */
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         */
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
461    public BluetoothMapbMessage(){
462
463    }
464
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     * */
472    public void setVersionString(String version) {
473        this.mVersionString = "VERSION:"+version;
474    }
475
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
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
681    private void parseBody(BMsgReader reader) {
682        String line;
683        line = reader.getLineEnforce();
684        parseMsgInit();
685        while(!line.contains("END:")) {
686            if(line.contains("PARTID:")) {
687                String[] arg = line.split(":");
688                if (arg != null && arg.length == 2) {
689                    try {
690                    mPartId = Long.parseLong(arg[1].trim());
691                    } catch (NumberFormatException e) {
692                        throw new IllegalArgumentException("Wrong value in 'PARTID': " + arg[1]);
693                    }
694                } else {
695                    throw new IllegalArgumentException("Missing value for 'PARTID': " + line);
696                }
697            }
698            else if(line.contains("ENCODING:")) {
699                String[] arg = line.split(":");
700                if (arg != null && arg.length == 2) {
701                    mEncoding = arg[1].trim();
702                    // If needed validation will be done when the value is used
703                } else {
704                    throw new IllegalArgumentException("Missing value for 'ENCODING': " + line);
705                }
706            }
707            else if(line.contains("CHARSET:")) {
708                String[] arg = line.split(":");
709                if (arg != null && arg.length == 2) {
710                    mCharset = arg[1].trim();
711                    // If needed validation will be done when the value is used
712                } else {
713                    throw new IllegalArgumentException("Missing value for 'CHARSET': " + line);
714                }
715            }
716            else if(line.contains("LANGUAGE:")) {
717                String[] arg = line.split(":");
718                if (arg != null && arg.length == 2) {
719                    mLanguage = arg[1].trim();
720                    // If needed validation will be done when the value is used
721                } else {
722                    throw new IllegalArgumentException("Missing value for 'LANGUAGE': " + line);
723                }
724            }
725            else if(line.contains("LENGTH:")) {
726                String[] arg = line.split(":");
727                if (arg != null && arg.length == 2) {
728                    try {
729                        mBMsgLength = Integer.parseInt(arg[1].trim());
730                    } catch (NumberFormatException e) {
731                        throw new IllegalArgumentException("Wrong value in 'LENGTH': " + arg[1]);
732                    }
733                } else {
734                    throw new IllegalArgumentException("Missing value for 'LENGTH': " + line);
735                }
736            }
737            else if(line.contains("BEGIN:MSG")) {
738                if (V) Log.v(TAG, "bMsgLength: " + mBMsgLength);
739                if(mBMsgLength == INVALID_VALUE)
740                    throw new IllegalArgumentException("Missing value for 'LENGTH'. " +
741                            "Unable to read remaining part of the message");
742
743                /* For SMS: Encoding of MSG is always UTF-8 compliant, regardless of any properties,
744                   since PDUs are encodes as hex-strings */
745                /* PTS has a bug regarding the message length, and sets it 2 bytes too short, hence
746                 * using the length field to determine the amount of data to read, might not be the
747                 * best solution.
748                 * Errata ESR06 section 5.8.12 introduced escaping of END:MSG in the actual message
749                 * content, it is now safe to use the END:MSG tag as terminator, and simply ignore
750                 * the length field.*/
751
752                // Read until we receive END:MSG as some carkits send bad message lengths
753                String data = "";
754                String message_line = "";
755                while (!message_line.equals("END:MSG")) {
756                    data += message_line;
757                    message_line = reader.getLineEnforce();
758                }
759
760                // The MAP spec says that all END:MSG strings in the body
761                // of the message must be escaped upon encoding and the
762                // escape removed upon decoding
763                data.replaceAll("([/]*)/END\\:MSG", "$1END:MSG");
764                data.trim();
765
766                parseMsgPart(data);
767            }
768            line = reader.getLineEnforce();
769        }
770    }
771
772    /**
773     * Parse the 'message' part of <bmessage-body-content>"
774     * @param msgPart
775     */
776    public abstract void parseMsgPart(String msgPart);
777    /**
778     * Set initial values before parsing - will be called is a message body is found
779     * during parsing.
780     */
781    public abstract void parseMsgInit();
782
783    public abstract byte[] encode() throws UnsupportedEncodingException;
784
785    public void setStatus(boolean read) {
786        if(read)
787            this.mStatus = "READ";
788        else
789            this.mStatus = "UNREAD";
790    }
791
792    public void setType(TYPE type) {
793        this.mType = type;
794    }
795
796    /**
797     * @return the type
798     */
799    public TYPE getType() {
800        return mType;
801    }
802
803    public void setCompleteFolder(String folder) {
804        this.mFolder = folder;
805    }
806
807    public void setFolder(String folder) {
808        this.mFolder = "telecom/msg/" + folder;
809    }
810
811    public String getFolder() {
812        return mFolder;
813    }
814
815
816    public void setEncoding(String encoding) {
817        this.mEncoding = encoding;
818    }
819
820    public ArrayList<vCard> getOriginators() {
821        return mOriginator;
822    }
823
824    public void addOriginator(vCard originator) {
825        if(this.mOriginator == null)
826            this.mOriginator = new ArrayList<vCard>();
827        this.mOriginator.add(originator);
828    }
829
830    /**
831     * Add a version 3.0 vCard with a formatted name
832     * @param name e.g. Bonde;Casper
833     * @param formattedName e.g. "Casper Bonde"
834     * @param phoneNumbers
835     * @param emailAddresses
836     */
837    public void addOriginator(String name, String formattedName,
838                              String[] phoneNumbers,
839                              String[] emailAddresses,
840                              String[] btUids,
841                              String[] btUcis) {
842        if(mOriginator == null)
843            mOriginator = new ArrayList<vCard>();
844        mOriginator.add(new vCard(name, formattedName, phoneNumbers,
845                    emailAddresses, btUids, btUcis));
846    }
847
848
849    public void addOriginator(String[] btUcis, String[] btUids) {
850        if(mOriginator == null)
851            mOriginator = new ArrayList<vCard>();
852        mOriginator.add(new vCard(null,null,null,null,btUids, btUcis));
853    }
854
855
856    /** Add a version 2.1 vCard with only a name.
857     *
858     * @param name e.g. Bonde;Casper
859     * @param phoneNumbers
860     * @param emailAddresses
861     */
862    public void addOriginator(String name, String[] phoneNumbers, String[] emailAddresses) {
863        if(mOriginator == null)
864            mOriginator = new ArrayList<vCard>();
865        mOriginator.add(new vCard(name, phoneNumbers, emailAddresses));
866    }
867
868    public ArrayList<vCard> getRecipients() {
869        return mRecipient;
870    }
871
872    public void setRecipient(vCard recipient) {
873        if(this.mRecipient == null)
874            this.mRecipient = new ArrayList<vCard>();
875        this.mRecipient.add(recipient);
876    }
877    public void addRecipient(String[] btUcis, String[] btUids) {
878        if(mRecipient == null)
879            mRecipient = new ArrayList<vCard>();
880        mRecipient.add(new vCard(null,null,null,null,btUids, btUcis));
881    }
882    public void addRecipient(String name, String formattedName,
883                             String[] phoneNumbers,
884                             String[] emailAddresses,
885                             String[] btUids,
886                             String[] btUcis) {
887        if(mRecipient == null)
888            mRecipient = new ArrayList<vCard>();
889        mRecipient.add(new vCard(name, formattedName, phoneNumbers,
890                    emailAddresses,btUids, btUcis));
891    }
892
893    public void addRecipient(String name, String[] phoneNumbers, String[] emailAddresses) {
894        if(mRecipient == null)
895            mRecipient = new ArrayList<vCard>();
896        mRecipient.add(new vCard(name, phoneNumbers, emailAddresses));
897    }
898
899    /**
900     * Convert a byte[] of data to a hex string representation, converting each nibble to the
901     * corresponding hex char.
902     * NOTE: There is not need to escape instances of "\r\nEND:MSG" in the binary data represented
903     * as a string as only the characters [0-9] and [a-f] is used.
904     * @param pduData the byte-array of data.
905     * @param scAddressData the byte-array of the encoded sc-Address.
906     * @return the resulting string.
907     */
908    protected String encodeBinary(byte[] pduData, byte[] scAddressData) {
909        StringBuilder out = new StringBuilder((pduData.length + scAddressData.length)*2);
910        for(int i = 0; i < scAddressData.length; i++) {
911            out.append(Integer.toString((scAddressData[i] >> 4) & 0x0f,16)); // MS-nibble first
912            out.append(Integer.toString( scAddressData[i]       & 0x0f,16));
913        }
914        for(int i = 0; i < pduData.length; i++) {
915            out.append(Integer.toString((pduData[i] >> 4) & 0x0f,16)); // MS-nibble first
916            out.append(Integer.toString( pduData[i]       & 0x0f,16));
917            /*out.append(Integer.toHexString(data[i]));*/ /* This is the same as above, but does not
918                                                           * include the needed 0's
919                                                           * e.g. it converts the value 3 to "3"
920                                                           * and not "03" */
921        }
922        return out.toString();
923    }
924
925    /**
926     * Decodes a binary hex-string encoded UTF-8 string to the represented binary data set.
927     * @param data The string representation of the data - must have an even number of characters.
928     * @return the byte[] represented in the data.
929     */
930    protected byte[] decodeBinary(String data) {
931        byte[] out = new byte[data.length()/2];
932        String value;
933        if(D) Log.d(TAG,"Decoding binary data: START:" + data + ":END");
934        for(int i = 0, j = 0, n = out.length; i < n; i++)
935        {
936            value = data.substring(j++, ++j);
937            // same as data.substring(2*i, 2*i+1+1) - substring() uses end-1 for last index
938            out[i] = (byte)(Integer.valueOf(value, 16) & 0xff);
939        }
940        if(D) {
941            StringBuilder sb = new StringBuilder(out.length);
942            for(int i = 0, n = out.length; i < n; i++)
943            {
944                sb.append(String.format("%02X",out[i] & 0xff));
945            }
946            Log.d(TAG,"Decoded binary data: START:" + sb.toString() + ":END");
947        }
948        return out;
949    }
950
951    public byte[] encodeGeneric(ArrayList<byte[]> bodyFragments) throws UnsupportedEncodingException
952    {
953        StringBuilder sb = new StringBuilder(256);
954        byte[] msgStart, msgEnd;
955        sb.append("BEGIN:BMSG").append("\r\n");
956
957        sb.append(mVersionString).append("\r\n");
958        sb.append("STATUS:").append(mStatus).append("\r\n");
959        sb.append("TYPE:").append(mType.name()).append("\r\n");
960        if(mFolder.length() > 512)
961            sb.append("FOLDER:").append(
962                    mFolder.substring(mFolder.length()-512, mFolder.length())).append("\r\n");
963        else
964            sb.append("FOLDER:").append(mFolder).append("\r\n");
965        if(!mVersionString.contains("1.0")){
966            sb.append("EXTENDEDDATA:").append("\r\n");
967        }
968        if(mOriginator != null){
969            for(vCard element : mOriginator)
970                element.encode(sb);
971        }
972        /* If we need the three levels of env. at some point - we do have a level in the
973         *  vCards that could be used to determine the levels of the envelope.
974         */
975
976        sb.append("BEGIN:BENV").append("\r\n");
977        if(mRecipient != null){
978            for(vCard element : mRecipient) {
979                if(V) Log.v(TAG, "encodeGeneric: recipient email" + element.getFirstEmail());
980                element.encode(sb);
981            }
982        }
983        sb.append("BEGIN:BBODY").append("\r\n");
984        if(mEncoding != null && mEncoding != "")
985            sb.append("ENCODING:").append(mEncoding).append("\r\n");
986        if(mCharset != null && mCharset != "")
987            sb.append("CHARSET:").append(mCharset).append("\r\n");
988
989
990        int length = 0;
991        /* 22 is the length of the 'BEGIN:MSG' and 'END:MSG' + 3*CRLF */
992        for (byte[] fragment : bodyFragments) {
993            length += fragment.length + 22;
994        }
995        sb.append("LENGTH:").append(length).append("\r\n");
996
997        // Extract the initial part of the bMessage string
998        msgStart = sb.toString().getBytes("UTF-8");
999
1000        sb = new StringBuilder(31);
1001        sb.append("END:BBODY").append("\r\n");
1002        sb.append("END:BENV").append("\r\n");
1003        sb.append("END:BMSG").append("\r\n");
1004
1005        msgEnd = sb.toString().getBytes("UTF-8");
1006
1007        try {
1008
1009            ByteArrayOutputStream stream = new ByteArrayOutputStream(
1010                                                       msgStart.length + msgEnd.length + length);
1011            stream.write(msgStart);
1012
1013            for (byte[] fragment : bodyFragments) {
1014                stream.write("BEGIN:MSG\r\n".getBytes("UTF-8"));
1015                stream.write(fragment);
1016                stream.write("\r\nEND:MSG\r\n".getBytes("UTF-8"));
1017            }
1018            stream.write(msgEnd);
1019
1020            if(V) Log.v(TAG,stream.toString("UTF-8"));
1021            return stream.toByteArray();
1022        } catch (IOException e) {
1023            Log.w(TAG,e);
1024            return null;
1025        }
1026    }
1027}
1028