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