Home | History | Annotate | Download | only in map
      1 /*
      2 * Copyright (C) 2013 Samsung System LSI
      3 * Licensed under the Apache License, Version 2.0 (the "License");
      4 * you may not use this file except in compliance with the License.
      5 * You may obtain a copy of the License at
      6 *
      7 *      http://www.apache.org/licenses/LICENSE-2.0
      8 *
      9 * Unless required by applicable law or agreed to in writing, software
     10 * distributed under the License is distributed on an "AS IS" BASIS,
     11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 * See the License for the specific language governing permissions and
     13 * limitations under the License.
     14 */
     15 package com.android.bluetooth.map;
     16 
     17 import java.io.ByteArrayOutputStream;
     18 import java.io.File;
     19 import java.io.FileInputStream;
     20 import java.io.FileNotFoundException;
     21 import java.io.FileOutputStream;
     22 import java.io.IOException;
     23 import java.io.InputStream;
     24 import java.io.UnsupportedEncodingException;
     25 import java.util.ArrayList;
     26 
     27 import android.os.Environment;
     28 import android.telephony.PhoneNumberUtils;
     29 import android.util.Log;
     30 
     31 import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
     32 
     33 public abstract class BluetoothMapbMessage {
     34 
     35     protected static String TAG = "BluetoothMapbMessage";
     36     protected static final boolean D = false;
     37     protected static final boolean V = false;
     38     private static final String VERSION = "VERSION:1.0";
     39 
     40     public static int INVALID_VALUE = -1;
     41 
     42     protected int appParamCharset = BluetoothMapAppParams.INVALID_VALUE_PARAMETER;
     43 
     44     // TODO: Reevaluate if strings are the best types for the members.
     45 
     46     /* BMSG attributes */
     47     private String status = null; // READ/UNREAD
     48     protected TYPE type = null;   // SMS/MMS/EMAIL
     49 
     50     private String folder = null;
     51 
     52     /* BBODY attributes */
     53     private long partId = INVALID_VALUE;
     54     protected String encoding = null;
     55     protected String charset = null;
     56     private String language = null;
     57 
     58     private int bMsgLength = INVALID_VALUE;
     59 
     60     private ArrayList<vCard> originator = null;
     61     private ArrayList<vCard> recipient = null;
     62 
     63 
     64     public static class vCard {
     65         /* VCARD attributes */
     66         private String version;
     67         private String name = null;
     68         private String formattedName = null;
     69         private String[] phoneNumbers = {};
     70         private String[] emailAddresses = {};
     71         private int envLevel = 0;
     72 
     73         /**
     74          * Construct a version 3.0 vCard
     75          * @param name Structured
     76          * @param formattedName Formatted name
     77          * @param phoneNumbers a String[] of phone numbers
     78          * @param emailAddresses a String[] of email addresses
     79          * @param the bmessage envelope level (0 is the top/most outer level)
     80          */
     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