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