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