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