BluetoothMapbMessage.java revision 326b5e610063ac24c0ba467ac585bd4c7f618a67
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 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         */
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         */
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         */
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         */
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
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
157        public String getFirstPhoneNumber() {
158            if(mPhoneNumbers.length > 0) {
159                return mPhoneNumbers[0];
160            } else
161                return null;
162        }
163
164        public int getEnvLevel() {
165            return mEnvLevel;
166        }
167
168        public String getName() {
169            return mName;
170        }
171
172        public String getFirstEmail() {
173            if(mEmailAddresses.length > 0) {
174                return mEmailAddresses[0];
175            } else
176                return null;
177        }
178
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         */
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;
261        public BMsgReader(InputStream is)
262        {
263            this.mInStream = is;
264        }
265
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         */
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         */
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         */
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         */
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         */
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
390    public BluetoothMapbMessage(){
391
392    }
393
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
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
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     */
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     */
692    public abstract void parseMsgInit();
693
694    public abstract byte[] encode() throws UnsupportedEncodingException;
695
696    public void setStatus(boolean read) {
697        if(read)
698            this.mStatus = "READ";
699        else
700            this.mStatus = "UNREAD";
701    }
702
703    public void setType(TYPE type) {
704        this.mType = type;
705    }
706
707    /**
708     * @return the type
709     */
710    public TYPE getType() {
711        return mType;
712    }
713
714    public void setCompleteFolder(String folder) {
715        this.mFolder = folder;
716    }
717
718    public void setFolder(String folder) {
719        this.mFolder = "telecom/msg/" + folder;
720    }
721
722    public String getFolder() {
723        return mFolder;
724    }
725
726
727    public void setEncoding(String encoding) {
728        this.mEncoding = encoding;
729    }
730
731    public ArrayList<vCard> getOriginators() {
732        return mOriginator;
733    }
734
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     */
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     */
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
766    public ArrayList<vCard> getRecipients() {
767        return mRecipient;
768    }
769
770    public void setRecipient(vCard recipient) {
771        if(this.mRecipient == null)
772            this.mRecipient = new ArrayList<vCard>();
773        this.mRecipient.add(recipient);
774    }
775
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
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     */
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     */
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
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