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