1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.bluetooth.client.map; 18 19import android.util.Log; 20 21import com.android.vcard.VCardEntry; 22import com.android.vcard.VCardEntryConstructor; 23import com.android.vcard.VCardEntryHandler; 24import com.android.vcard.VCardParser; 25import com.android.vcard.VCardParser_V21; 26import com.android.vcard.VCardParser_V30; 27import com.android.vcard.exception.VCardException; 28import com.android.vcard.exception.VCardVersionException; 29import android.bluetooth.client.map.BluetoothMapBmessage.Status; 30import android.bluetooth.client.map.BluetoothMapBmessage.Type; 31import android.bluetooth.client.map.utils.BmsgTokenizer; 32import android.bluetooth.client.map.utils.BmsgTokenizer.Property; 33 34import java.io.ByteArrayInputStream; 35import java.io.IOException; 36import java.nio.charset.StandardCharsets; 37import java.text.ParseException; 38 39class BluetoothMapBmessageParser { 40 41 private final static String TAG = "BluetoothMapBmessageParser"; 42 private final static boolean DBG = false; 43 44 private final static String CRLF = "\r\n"; 45 46 private final static Property BEGIN_BMSG = new Property("BEGIN", "BMSG"); 47 private final static Property END_BMSG = new Property("END", "BMSG"); 48 49 private final static Property BEGIN_VCARD = new Property("BEGIN", "VCARD"); 50 private final static Property END_VCARD = new Property("END", "VCARD"); 51 52 private final static Property BEGIN_BENV = new Property("BEGIN", "BENV"); 53 private final static Property END_BENV = new Property("END", "BENV"); 54 55 private final static Property BEGIN_BBODY = new Property("BEGIN", "BBODY"); 56 private final static Property END_BBODY = new Property("END", "BBODY"); 57 58 private final static Property BEGIN_MSG = new Property("BEGIN", "MSG"); 59 private final static Property END_MSG = new Property("END", "MSG"); 60 61 private final static int CRLF_LEN = 2; 62 63 /* 64 * length of "container" for 'message' in bmessage-body-content: 65 * BEGIN:MSG<CRLF> + <CRLF> + END:MSG<CRFL> 66 */ 67 private final static int MSG_CONTAINER_LEN = 22; 68 69 private BmsgTokenizer mParser; 70 71 private final BluetoothMapBmessage mBmsg; 72 73 private BluetoothMapBmessageParser() { 74 mBmsg = new BluetoothMapBmessage(); 75 } 76 77 static public BluetoothMapBmessage createBmessage(String str) { 78 BluetoothMapBmessageParser p = new BluetoothMapBmessageParser(); 79 80 if (DBG) { 81 Log.d(TAG, "actual wired contents: " + str); 82 } 83 84 try { 85 p.parse(str); 86 } catch (IOException e) { 87 Log.e(TAG, "I/O exception when parsing bMessage", e); 88 return null; 89 } catch (ParseException e) { 90 Log.e(TAG, "Cannot parse bMessage", e); 91 return null; 92 } 93 94 return p.mBmsg; 95 } 96 97 private ParseException expected(Property... props) { 98 boolean first = true; 99 StringBuilder sb = new StringBuilder(); 100 101 for (Property prop : props) { 102 if (!first) { 103 sb.append(" or "); 104 } 105 sb.append(prop); 106 first = false; 107 } 108 109 return new ParseException("Expected: " + sb.toString(), mParser.pos()); 110 } 111 112 private void parse(String str) throws IOException, ParseException { 113 114 Property prop; 115 116 /* 117 * <bmessage-object>::= { "BEGIN:BMSG" <CRLF> <bmessage-property> 118 * [<bmessage-originator>]* <bmessage-envelope> "END:BMSG" <CRLF> } 119 */ 120 121 mParser = new BmsgTokenizer(str + CRLF); 122 123 prop = mParser.next(); 124 if (!prop.equals(BEGIN_BMSG)) { 125 throw expected(BEGIN_BMSG); 126 } 127 128 prop = parseProperties(); 129 130 while (prop.equals(BEGIN_VCARD)) { 131 132 /* <bmessage-originator>::= <vcard> <CRLF> */ 133 134 StringBuilder vcard = new StringBuilder(); 135 prop = extractVcard(vcard); 136 137 VCardEntry entry = parseVcard(vcard.toString()); 138 mBmsg.mOriginators.add(entry); 139 } 140 141 if (!prop.equals(BEGIN_BENV)) { 142 throw expected(BEGIN_BENV); 143 } 144 145 prop = parseEnvelope(1); 146 147 if (!prop.equals(END_BMSG)) { 148 throw expected(END_BENV); 149 } 150 151 /* 152 * there should be no meaningful data left in stream here so we just 153 * ignore whatever is left 154 */ 155 156 mParser = null; 157 } 158 159 private Property parseProperties() throws ParseException { 160 161 Property prop; 162 163 /* 164 * <bmessage-property>::=<bmessage-version-property> 165 * <bmessage-readstatus-property> <bmessage-type-property> 166 * <bmessage-folder-property> <bmessage-version-property>::="VERSION:" 167 * <common-digit>*"."<common-digit>* <CRLF> 168 * <bmessage-readstatus-property>::="STATUS:" 'readstatus' <CRLF> 169 * <bmessage-type-property>::="TYPE:" 'type' <CRLF> 170 * <bmessage-folder-property>::="FOLDER:" 'foldername' <CRLF> 171 */ 172 173 do { 174 prop = mParser.next(); 175 176 if (prop.name.equals("VERSION")) { 177 mBmsg.mBmsgVersion = prop.value; 178 179 } else if (prop.name.equals("STATUS")) { 180 for (Status s : Status.values()) { 181 if (prop.value.equals(s.toString())) { 182 mBmsg.mBmsgStatus = s; 183 break; 184 } 185 } 186 187 } else if (prop.name.equals("TYPE")) { 188 for (Type t : Type.values()) { 189 if (prop.value.equals(t.toString())) { 190 mBmsg.mBmsgType = t; 191 break; 192 } 193 } 194 195 } else if (prop.name.equals("FOLDER")) { 196 mBmsg.mBmsgFolder = prop.value; 197 198 } 199 200 } while (!prop.equals(BEGIN_VCARD) && !prop.equals(BEGIN_BENV)); 201 202 return prop; 203 } 204 205 private Property parseEnvelope(int level) throws IOException, ParseException { 206 207 Property prop; 208 209 /* 210 * we can support as many nesting level as we want, but MAP spec clearly 211 * defines that there should be no more than 3 levels. so we verify it 212 * here. 213 */ 214 215 if (level > 3) { 216 throw new ParseException("bEnvelope is nested more than 3 times", mParser.pos()); 217 } 218 219 /* 220 * <bmessage-envelope> ::= { "BEGIN:BENV" <CRLF> [<bmessage-recipient>]* 221 * <bmessage-envelope> | <bmessage-content> "END:BENV" <CRLF> } 222 */ 223 224 prop = mParser.next(); 225 226 while (prop.equals(BEGIN_VCARD)) { 227 228 /* <bmessage-originator>::= <vcard> <CRLF> */ 229 230 StringBuilder vcard = new StringBuilder(); 231 prop = extractVcard(vcard); 232 233 if (level == 1) { 234 VCardEntry entry = parseVcard(vcard.toString()); 235 mBmsg.mRecipients.add(entry); 236 } 237 } 238 239 if (prop.equals(BEGIN_BENV)) { 240 prop = parseEnvelope(level + 1); 241 242 } else if (prop.equals(BEGIN_BBODY)) { 243 prop = parseBody(); 244 245 } else { 246 throw expected(BEGIN_BENV, BEGIN_BBODY); 247 } 248 249 if (!prop.equals(END_BENV)) { 250 throw expected(END_BENV); 251 } 252 253 return mParser.next(); 254 } 255 256 private Property parseBody() throws IOException, ParseException { 257 258 Property prop; 259 260 /* 261 * <bmessage-content>::= { "BEGIN:BBODY"<CRLF> [<bmessage-body-part-ID> 262 * <CRLF>] <bmessage-body-property> <bmessage-body-content>* <CRLF> 263 * "END:BBODY"<CRLF> } <bmessage-body-part-ID>::="PARTID:" 'Part-ID' 264 * <bmessage-body-property>::=[<bmessage-body-encoding-property>] 265 * [<bmessage-body-charset-property>] 266 * [<bmessage-body-language-property>] 267 * <bmessage-body-content-length-property> 268 * <bmessage-body-encoding-property>::="ENCODING:"'encoding' <CRLF> 269 * <bmessage-body-charset-property>::="CHARSET:"'charset' <CRLF> 270 * <bmessage-body-language-property>::="LANGUAGE:"'language' <CRLF> 271 * <bmessage-body-content-length-property>::= "LENGTH:" <common-digit>* 272 * <CRLF> 273 */ 274 275 do { 276 prop = mParser.next(); 277 278 if (prop.name.equals("PARTID")) { 279 } else if (prop.name.equals("ENCODING")) { 280 mBmsg.mBbodyEncoding = prop.value; 281 282 } else if (prop.name.equals("CHARSET")) { 283 mBmsg.mBbodyCharset = prop.value; 284 285 } else if (prop.name.equals("LANGUAGE")) { 286 mBmsg.mBbodyLanguage = prop.value; 287 288 } else if (prop.name.equals("LENGTH")) { 289 try { 290 mBmsg.mBbodyLength = Integer.parseInt(prop.value); 291 } catch (NumberFormatException e) { 292 throw new ParseException("Invalid LENGTH value", mParser.pos()); 293 } 294 295 } 296 297 } while (!prop.equals(BEGIN_MSG)); 298 299 /* 300 * check that the charset is always set to UTF-8. We expect only text transfer (in lieu with 301 * the MAPv12 specifying only RFC2822 (text only) for MMS/EMAIL and SMS do not support 302 * non-text content. If the charset is not set to UTF-8, it is safe to set the message as 303 * empty. We force the getMessage (see BluetoothMasClient) to only call getMessage with 304 * UTF-8 as the MCE is not obliged to support native charset. 305 */ 306 if (!mBmsg.mBbodyCharset.equals("UTF-8")) { 307 Log.e(TAG, "The charset was not set to charset UTF-8: " + mBmsg.mBbodyCharset); 308 } 309 310 /* 311 * <bmessage-body-content>::={ "BEGIN:MSG"<CRLF> 'message'<CRLF> 312 * "END:MSG"<CRLF> } 313 */ 314 315 int messageLen = mBmsg.mBbodyLength - MSG_CONTAINER_LEN; 316 int offset = messageLen + CRLF_LEN; 317 int restartPos = mParser.pos() + offset; 318 319 /* 320 * length is specified in bytes so we need to convert from unicode 321 * string back to bytes array 322 */ 323 324 String remng = mParser.remaining(); 325 byte[] data = remng.getBytes(); 326 327 /* restart parsing from after 'message'<CRLF> */ 328 mParser = new BmsgTokenizer(new String(data, offset, data.length - offset), restartPos); 329 330 prop = mParser.next(true); 331 332 if (prop != null) { 333 if (prop.equals(END_MSG)) { 334 if (mBmsg.mBbodyCharset.equals("UTF-8")) { 335 mBmsg.mMessage = new String(data, 0, messageLen, StandardCharsets.UTF_8); 336 } else { 337 mBmsg.mMessage = null; 338 } 339 } else { 340 /* Handle possible exception for incorrect LENGTH value 341 * from MSE while parsing GET Message response */ 342 Log.e(TAG, "Prop Invalid: "+ prop.toString()); 343 Log.e(TAG, "Possible Invalid LENGTH value"); 344 throw expected(END_MSG); 345 } 346 } else { 347 348 data = null; 349 350 /* 351 * now we check if bMessage can be parsed if LENGTH is handled as 352 * number of characters instead of number of bytes 353 */ 354 if (offset < 0 || offset > remng.length()) { 355 /* Handle possible exception for incorrect LENGTH value 356 * from MSE while parsing GET Message response */ 357 throw new ParseException("Invalid LENGTH value", mParser.pos()); 358 } 359 360 Log.w(TAG, "byte LENGTH seems to be invalid, trying with char length"); 361 362 mParser = new BmsgTokenizer(remng.substring(offset)); 363 364 prop = mParser.next(); 365 366 if (!prop.equals(END_MSG)) { 367 throw expected(END_MSG); 368 } 369 370 if (mBmsg.mBbodyCharset.equals("UTF-8")) { 371 mBmsg.mMessage = remng.substring(0, messageLen); 372 } else { 373 mBmsg.mMessage = null; 374 } 375 } 376 377 prop = mParser.next(); 378 379 if (!prop.equals(END_BBODY)) { 380 throw expected(END_BBODY); 381 } 382 383 return mParser.next(); 384 } 385 386 private Property extractVcard(StringBuilder out) throws IOException, ParseException { 387 Property prop; 388 389 out.append(BEGIN_VCARD).append(CRLF); 390 391 do { 392 prop = mParser.next(); 393 out.append(prop).append(CRLF); 394 } while (!prop.equals(END_VCARD)); 395 396 return mParser.next(); 397 } 398 399 private class VcardHandler implements VCardEntryHandler { 400 401 VCardEntry vcard; 402 403 @Override 404 public void onStart() { 405 } 406 407 @Override 408 public void onEntryCreated(VCardEntry entry) { 409 vcard = entry; 410 } 411 412 @Override 413 public void onEnd() { 414 } 415 }; 416 417 private VCardEntry parseVcard(String str) throws IOException, ParseException { 418 VCardEntry vcard = null; 419 420 try { 421 VCardParser p = new VCardParser_V21(); 422 VCardEntryConstructor c = new VCardEntryConstructor(); 423 VcardHandler handler = new VcardHandler(); 424 c.addEntryHandler(handler); 425 p.addInterpreter(c); 426 p.parse(new ByteArrayInputStream(str.getBytes())); 427 428 vcard = handler.vcard; 429 430 } catch (VCardVersionException e1) { 431 432 try { 433 VCardParser p = new VCardParser_V30(); 434 VCardEntryConstructor c = new VCardEntryConstructor(); 435 VcardHandler handler = new VcardHandler(); 436 c.addEntryHandler(handler); 437 p.addInterpreter(c); 438 p.parse(new ByteArrayInputStream(str.getBytes())); 439 440 vcard = handler.vcard; 441 442 } catch (VCardVersionException e2) { 443 // will throw below 444 } catch (VCardException e2) { 445 // will throw below 446 } 447 448 } catch (VCardException e1) { 449 // will throw below 450 } 451 452 if (vcard == null) { 453 throw new ParseException("Cannot parse vCard object (neither 2.1 nor 3.0?)", 454 mParser.pos()); 455 } 456 457 return vcard; 458 } 459} 460