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