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         while(!line.contains("END:")) {
    685             if(line.contains("PARTID:")) {
    686                 String arg[] = line.split(":");
    687                 if (arg != null && arg.length == 2) {
    688                     try {
    689                     mPartId = Long.parseLong(arg[1].trim());
    690                     } catch (NumberFormatException e) {
    691                         throw new IllegalArgumentException("Wrong value in 'PARTID': " + arg[1]);
    692                     }
    693                 } else {
    694                     throw new IllegalArgumentException("Missing value for 'PARTID': " + line);
    695                 }
    696             }
    697             else if(line.contains("ENCODING:")) {
    698                 String arg[] = line.split(":");
    699                 if (arg != null && arg.length == 2) {
    700                     mEncoding = arg[1].trim();
    701                     // If needed validation will be done when the value is used
    702                 } else {
    703                     throw new IllegalArgumentException("Missing value for 'ENCODING': " + line);
    704                 }
    705             }
    706             else if(line.contains("CHARSET:")) {
    707                 String arg[] = line.split(":");
    708                 if (arg != null && arg.length == 2) {
    709                     mCharset = arg[1].trim();
    710                     // If needed validation will be done when the value is used
    711                 } else {
    712                     throw new IllegalArgumentException("Missing value for 'CHARSET': " + line);
    713                 }
    714             }
    715             else if(line.contains("LANGUAGE:")) {
    716                 String arg[] = line.split(":");
    717                 if (arg != null && arg.length == 2) {
    718                     mLanguage = arg[1].trim();
    719                     // If needed validation will be done when the value is used
    720                 } else {
    721                     throw new IllegalArgumentException("Missing value for 'LANGUAGE': " + line);
    722                 }
    723             }
    724             else if(line.contains("LENGTH:")) {
    725                 String arg[] = line.split(":");
    726                 if (arg != null && arg.length == 2) {
    727                     try {
    728                         mBMsgLength = Integer.parseInt(arg[1].trim());
    729                     } catch (NumberFormatException e) {
    730                         throw new IllegalArgumentException("Wrong value in 'LENGTH': " + arg[1]);
    731                     }
    732                 } else {
    733                     throw new IllegalArgumentException("Missing value for 'LENGTH': " + line);
    734                 }
    735             }
    736             else if(line.contains("BEGIN:MSG")) {
    737                 if(mBMsgLength == INVALID_VALUE)
    738                     throw new IllegalArgumentException("Missing value for 'LENGTH'. " +
    739                             "Unable to read remaining part of the message");
    740                 /* For SMS: Encoding of MSG is always UTF-8 compliant, regardless of any properties,
    741                    since PDUs are encodes as hex-strings */
    742                 /* PTS has a bug regarding the message length, and sets it 2 bytes too short, hence
    743                  * using the length field to determine the amount of data to read, might not be the
    744                  * best solution.
    745                  * Since errata ???(bluetooth.org is down at the moment) introduced escaping of
    746                  * END:MSG in the actual message content, it is now safe to use the END:MSG tag
    747                  * as terminator, and simply ignore the length field.*/
    748 
    749                 /* 2 added to compensate for the removed \r\n */
    750                 byte[] rawData = reader.getDataBytes(mBMsgLength - (line.getBytes().length + 2));
    751                 String data;
    752                 try {
    753                     data = new String(rawData, "UTF-8");
    754                     if(V) {
    755                         Log.v(TAG,"MsgLength: " + mBMsgLength);
    756                         Log.v(TAG,"line.getBytes().length: " + line.getBytes().length);
    757                         String debug = line.replaceAll("\\n", "<LF>\n");
    758                         debug = debug.replaceAll("\\r", "<CR>");
    759                         Log.v(TAG,"The line: \"" + debug + "\"");
    760                         debug = data.replaceAll("\\n", "<LF>\n");
    761                         debug = debug.replaceAll("\\r", "<CR>");
    762                         Log.v(TAG,"The msgString: \"" + debug + "\"");
    763                     }
    764                 } catch (UnsupportedEncodingException e) {
    765                     Log.w(TAG,e);
    766                     throw new IllegalArgumentException("Unable to convert to UTF-8");
    767                 }
    768                 /* Decoding of MSG:
    769                  * 1) split on "\r\nEND:MSG\r\n"
    770                  * 2) delete "BEGIN:MSG\r\n" for each msg
    771                  * 3) replace any occurrence of "\END:MSG" with "END:MSG"
    772                  * 4) based on charset from application properties either store as String[] or
    773                  *    decode to raw PDUs
    774                  * */
    775                 String messages[] = data.split("\r\nEND:MSG\r\n");
    776                 parseMsgInit();
    777                 for(int i = 0; i < messages.length; i++) {
    778                     messages[i] = messages[i].replaceFirst("^BEGIN:MSG\r\n", "");
    779                     messages[i] = messages[i].replaceAll("\r\n([/]*)/END\\:MSG", "\r\n$1END:MSG");
    780                     messages[i] = messages[i].trim();
    781                     parseMsgPart(messages[i]);
    782                 }
    783             }
    784             line = reader.getLineEnforce();
    785         }
    786     }
    787 
    788     /**
    789      * Parse the 'message' part of <bmessage-body-content>"
    790      * @param msgPart
    791      */
    792     public abstract void parseMsgPart(String msgPart);
    793     /**
    794      * Set initial values before parsing - will be called is a message body is found
    795      * during parsing.
    796      */
    797     public abstract void parseMsgInit();
    798 
    799     public abstract byte[] encode() throws UnsupportedEncodingException;
    800 
    801     public void setStatus(boolean read) {
    802         if(read)
    803             this.mStatus = "READ";
    804         else
    805             this.mStatus = "UNREAD";
    806     }
    807 
    808     public void setType(TYPE type) {
    809         this.mType = type;
    810     }
    811 
    812     /**
    813      * @return the type
    814      */
    815     public TYPE getType() {
    816         return mType;
    817     }
    818 
    819     public void setCompleteFolder(String folder) {
    820         this.mFolder = folder;
    821     }
    822 
    823     public void setFolder(String folder) {
    824         this.mFolder = "telecom/msg/" + folder;
    825     }
    826 
    827     public String getFolder() {
    828         return mFolder;
    829     }
    830 
    831 
    832     public void setEncoding(String encoding) {
    833         this.mEncoding = encoding;
    834     }
    835 
    836     public ArrayList<vCard> getOriginators() {
    837         return mOriginator;
    838     }
    839 
    840     public void addOriginator(vCard originator) {
    841         if(this.mOriginator == null)
    842             this.mOriginator = new ArrayList<vCard>();
    843         this.mOriginator.add(originator);
    844     }
    845 
    846     /**
    847      * Add a version 3.0 vCard with a formatted name
    848      * @param name e.g. Bonde;Casper
    849      * @param formattedName e.g. "Casper Bonde"
    850      * @param phoneNumbers
    851      * @param emailAddresses
    852      */
    853     public void addOriginator(String name, String formattedName,
    854                               String[] phoneNumbers,
    855                               String[] emailAddresses,
    856                               String[] btUids,
    857                               String[] btUcis) {
    858         if(mOriginator == null)
    859             mOriginator = new ArrayList<vCard>();
    860         mOriginator.add(new vCard(name, formattedName, phoneNumbers,
    861                     emailAddresses, btUids, btUcis));
    862     }
    863 
    864 
    865     public void addOriginator(String[] btUcis, String[] btUids) {
    866         if(mOriginator == null)
    867             mOriginator = new ArrayList<vCard>();
    868         mOriginator.add(new vCard(null,null,null,null,btUids, btUcis));
    869     }
    870 
    871 
    872     /** Add a version 2.1 vCard with only a name.
    873      *
    874      * @param name e.g. Bonde;Casper
    875      * @param phoneNumbers
    876      * @param emailAddresses
    877      */
    878     public void addOriginator(String name, String[] phoneNumbers, String[] emailAddresses) {
    879         if(mOriginator == null)
    880             mOriginator = new ArrayList<vCard>();
    881         mOriginator.add(new vCard(name, phoneNumbers, emailAddresses));
    882     }
    883 
    884     public ArrayList<vCard> getRecipients() {
    885         return mRecipient;
    886     }
    887 
    888     public void setRecipient(vCard recipient) {
    889         if(this.mRecipient == null)
    890             this.mRecipient = new ArrayList<vCard>();
    891         this.mRecipient.add(recipient);
    892     }
    893     public void addRecipient(String[] btUcis, String[] btUids) {
    894         if(mRecipient == null)
    895             mRecipient = new ArrayList<vCard>();
    896         mRecipient.add(new vCard(null,null,null,null,btUids, btUcis));
    897     }
    898     public void addRecipient(String name, String formattedName,
    899                              String[] phoneNumbers,
    900                              String[] emailAddresses,
    901                              String[] btUids,
    902                              String[] btUcis) {
    903         if(mRecipient == null)
    904             mRecipient = new ArrayList<vCard>();
    905         mRecipient.add(new vCard(name, formattedName, phoneNumbers,
    906                     emailAddresses,btUids, btUcis));
    907     }
    908 
    909     public void addRecipient(String name, String[] phoneNumbers, String[] emailAddresses) {
    910         if(mRecipient == null)
    911             mRecipient = new ArrayList<vCard>();
    912         mRecipient.add(new vCard(name, phoneNumbers, emailAddresses));
    913     }
    914 
    915     /**
    916      * Convert a byte[] of data to a hex string representation, converting each nibble to the
    917      * corresponding hex char.
    918      * NOTE: There is not need to escape instances of "\r\nEND:MSG" in the binary data represented
    919      * as a string as only the characters [0-9] and [a-f] is used.
    920      * @param pduData the byte-array of data.
    921      * @param scAddressData the byte-array of the encoded sc-Address.
    922      * @return the resulting string.
    923      */
    924     protected String encodeBinary(byte[] pduData, byte[] scAddressData) {
    925         StringBuilder out = new StringBuilder((pduData.length + scAddressData.length)*2);
    926         for(int i = 0; i < scAddressData.length; i++) {
    927             out.append(Integer.toString((scAddressData[i] >> 4) & 0x0f,16)); // MS-nibble first
    928             out.append(Integer.toString( scAddressData[i]       & 0x0f,16));
    929         }
    930         for(int i = 0; i < pduData.length; i++) {
    931             out.append(Integer.toString((pduData[i] >> 4) & 0x0f,16)); // MS-nibble first
    932             out.append(Integer.toString( pduData[i]       & 0x0f,16));
    933             /*out.append(Integer.toHexString(data[i]));*/ /* This is the same as above, but does not
    934                                                            * include the needed 0's
    935                                                            * e.g. it converts the value 3 to "3"
    936                                                            * and not "03" */
    937         }
    938         return out.toString();
    939     }
    940 
    941     /**
    942      * Decodes a binary hex-string encoded UTF-8 string to the represented binary data set.
    943      * @param data The string representation of the data - must have an even number of characters.
    944      * @return the byte[] represented in the data.
    945      */
    946     protected byte[] decodeBinary(String data) {
    947         byte[] out = new byte[data.length()/2];
    948         String value;
    949         if(D) Log.d(TAG,"Decoding binary data: START:" + data + ":END");
    950         for(int i = 0, j = 0, n = out.length; i < n; i++)
    951         {
    952             value = data.substring(j++, ++j);
    953             // same as data.substring(2*i, 2*i+1+1) - substring() uses end-1 for last index
    954             out[i] = (byte)(Integer.valueOf(value, 16) & 0xff);
    955         }
    956         if(D) {
    957             StringBuilder sb = new StringBuilder(out.length);
    958             for(int i = 0, n = out.length; i < n; i++)
    959             {
    960                 sb.append(String.format("%02X",out[i] & 0xff));
    961             }
    962             Log.d(TAG,"Decoded binary data: START:" + sb.toString() + ":END");
    963         }
    964         return out;
    965     }
    966 
    967     public byte[] encodeGeneric(ArrayList<byte[]> bodyFragments) throws UnsupportedEncodingException
    968     {
    969         StringBuilder sb = new StringBuilder(256);
    970         byte[] msgStart, msgEnd;
    971         sb.append("BEGIN:BMSG").append("\r\n");
    972 
    973         sb.append(mVersionString).append("\r\n");
    974         sb.append("STATUS:").append(mStatus).append("\r\n");
    975         sb.append("TYPE:").append(mType.name()).append("\r\n");
    976         if(mFolder.length() > 512)
    977             sb.append("FOLDER:").append(
    978                     mFolder.substring(mFolder.length()-512, mFolder.length())).append("\r\n");
    979         else
    980             sb.append("FOLDER:").append(mFolder).append("\r\n");
    981         if(!mVersionString.contains("1.0")){
    982             sb.append("EXTENDEDDATA:").append("\r\n");
    983         }
    984         if(mOriginator != null){
    985             for(vCard element : mOriginator)
    986                 element.encode(sb);
    987         }
    988         /* If we need the three levels of env. at some point - we do have a level in the
    989          *  vCards that could be used to determine the levels of the envelope.
    990          */
    991 
    992         sb.append("BEGIN:BENV").append("\r\n");
    993         if(mRecipient != null){
    994             for(vCard element : mRecipient) {
    995                 if(V) Log.v(TAG, "encodeGeneric: recipient email" + element.getFirstEmail());
    996                 element.encode(sb);
    997             }
    998         }
    999         sb.append("BEGIN:BBODY").append("\r\n");
   1000         if(mEncoding != null && mEncoding != "")
   1001             sb.append("ENCODING:").append(mEncoding).append("\r\n");
   1002         if(mCharset != null && mCharset != "")
   1003             sb.append("CHARSET:").append(mCharset).append("\r\n");
   1004 
   1005 
   1006         int length = 0;
   1007         /* 22 is the length of the 'BEGIN:MSG' and 'END:MSG' + 3*CRLF */
   1008         for (byte[] fragment : bodyFragments) {
   1009             length += fragment.length + 22;
   1010         }
   1011         sb.append("LENGTH:").append(length).append("\r\n");
   1012 
   1013         // Extract the initial part of the bMessage string
   1014         msgStart = sb.toString().getBytes("UTF-8");
   1015 
   1016         sb = new StringBuilder(31);
   1017         sb.append("END:BBODY").append("\r\n");
   1018         sb.append("END:BENV").append("\r\n");
   1019         sb.append("END:BMSG").append("\r\n");
   1020 
   1021         msgEnd = sb.toString().getBytes("UTF-8");
   1022 
   1023         try {
   1024 
   1025             ByteArrayOutputStream stream = new ByteArrayOutputStream(
   1026                                                        msgStart.length + msgEnd.length + length);
   1027             stream.write(msgStart);
   1028 
   1029             for (byte[] fragment : bodyFragments) {
   1030                 stream.write("BEGIN:MSG\r\n".getBytes("UTF-8"));
   1031                 stream.write(fragment);
   1032                 stream.write("\r\nEND:MSG\r\n".getBytes("UTF-8"));
   1033             }
   1034             stream.write(msgEnd);
   1035 
   1036             if(V) Log.v(TAG,stream.toString("UTF-8"));
   1037             return stream.toByteArray();
   1038         } catch (IOException e) {
   1039             Log.w(TAG,e);
   1040             return null;
   1041         }
   1042     }
   1043 }
   1044