/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.bluetooth.client.map; import android.util.Log; import com.android.vcard.VCardEntry; import com.android.vcard.VCardEntryConstructor; import com.android.vcard.VCardEntryHandler; import com.android.vcard.VCardParser; import com.android.vcard.VCardParser_V21; import com.android.vcard.VCardParser_V30; import com.android.vcard.exception.VCardException; import com.android.vcard.exception.VCardVersionException; import android.bluetooth.client.map.BluetoothMapBmessage.Status; import android.bluetooth.client.map.BluetoothMapBmessage.Type; import android.bluetooth.client.map.utils.BmsgTokenizer; import android.bluetooth.client.map.utils.BmsgTokenizer.Property; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.text.ParseException; class BluetoothMapBmessageParser { private final static String TAG = "BluetoothMapBmessageParser"; private final static boolean DBG = false; private final static String CRLF = "\r\n"; private final static Property BEGIN_BMSG = new Property("BEGIN", "BMSG"); private final static Property END_BMSG = new Property("END", "BMSG"); private final static Property BEGIN_VCARD = new Property("BEGIN", "VCARD"); private final static Property END_VCARD = new Property("END", "VCARD"); private final static Property BEGIN_BENV = new Property("BEGIN", "BENV"); private final static Property END_BENV = new Property("END", "BENV"); private final static Property BEGIN_BBODY = new Property("BEGIN", "BBODY"); private final static Property END_BBODY = new Property("END", "BBODY"); private final static Property BEGIN_MSG = new Property("BEGIN", "MSG"); private final static Property END_MSG = new Property("END", "MSG"); private final static int CRLF_LEN = 2; /* * length of "container" for 'message' in bmessage-body-content: * BEGIN:MSG + + END:MSG */ private final static int MSG_CONTAINER_LEN = 22; private BmsgTokenizer mParser; private final BluetoothMapBmessage mBmsg; private BluetoothMapBmessageParser() { mBmsg = new BluetoothMapBmessage(); } static public BluetoothMapBmessage createBmessage(String str) { BluetoothMapBmessageParser p = new BluetoothMapBmessageParser(); if (DBG) { Log.d(TAG, "actual wired contents: " + str); } try { p.parse(str); } catch (IOException e) { Log.e(TAG, "I/O exception when parsing bMessage", e); return null; } catch (ParseException e) { Log.e(TAG, "Cannot parse bMessage", e); return null; } return p.mBmsg; } private ParseException expected(Property... props) { boolean first = true; StringBuilder sb = new StringBuilder(); for (Property prop : props) { if (!first) { sb.append(" or "); } sb.append(prop); first = false; } return new ParseException("Expected: " + sb.toString(), mParser.pos()); } private void parse(String str) throws IOException, ParseException { Property prop; /* * ::= { "BEGIN:BMSG" * []* "END:BMSG" } */ mParser = new BmsgTokenizer(str + CRLF); prop = mParser.next(); if (!prop.equals(BEGIN_BMSG)) { throw expected(BEGIN_BMSG); } prop = parseProperties(); while (prop.equals(BEGIN_VCARD)) { /* ::= */ StringBuilder vcard = new StringBuilder(); prop = extractVcard(vcard); VCardEntry entry = parseVcard(vcard.toString()); mBmsg.mOriginators.add(entry); } if (!prop.equals(BEGIN_BENV)) { throw expected(BEGIN_BENV); } prop = parseEnvelope(1); if (!prop.equals(END_BMSG)) { throw expected(END_BENV); } /* * there should be no meaningful data left in stream here so we just * ignore whatever is left */ mParser = null; } private Property parseProperties() throws ParseException { Property prop; /* * ::= * * ::="VERSION:" * *"."* * ::="STATUS:" 'readstatus' * ::="TYPE:" 'type' * ::="FOLDER:" 'foldername' */ do { prop = mParser.next(); if (prop.name.equals("VERSION")) { mBmsg.mBmsgVersion = prop.value; } else if (prop.name.equals("STATUS")) { for (Status s : Status.values()) { if (prop.value.equals(s.toString())) { mBmsg.mBmsgStatus = s; break; } } } else if (prop.name.equals("TYPE")) { for (Type t : Type.values()) { if (prop.value.equals(t.toString())) { mBmsg.mBmsgType = t; break; } } } else if (prop.name.equals("FOLDER")) { mBmsg.mBmsgFolder = prop.value; } } while (!prop.equals(BEGIN_VCARD) && !prop.equals(BEGIN_BENV)); return prop; } private Property parseEnvelope(int level) throws IOException, ParseException { Property prop; /* * we can support as many nesting level as we want, but MAP spec clearly * defines that there should be no more than 3 levels. so we verify it * here. */ if (level > 3) { throw new ParseException("bEnvelope is nested more than 3 times", mParser.pos()); } /* * ::= { "BEGIN:BENV" []* * | "END:BENV" } */ prop = mParser.next(); while (prop.equals(BEGIN_VCARD)) { /* ::= */ StringBuilder vcard = new StringBuilder(); prop = extractVcard(vcard); if (level == 1) { VCardEntry entry = parseVcard(vcard.toString()); mBmsg.mRecipients.add(entry); } } if (prop.equals(BEGIN_BENV)) { prop = parseEnvelope(level + 1); } else if (prop.equals(BEGIN_BBODY)) { prop = parseBody(); } else { throw expected(BEGIN_BENV, BEGIN_BBODY); } if (!prop.equals(END_BENV)) { throw expected(END_BENV); } return mParser.next(); } private Property parseBody() throws IOException, ParseException { Property prop; /* * ::= { "BEGIN:BBODY" [ * ] * * "END:BBODY" } ::="PARTID:" 'Part-ID' * ::=[] * [] * [] * * ::="ENCODING:"'encoding' * ::="CHARSET:"'charset' * ::="LANGUAGE:"'language' * ::= "LENGTH:" * * */ do { prop = mParser.next(); if (prop.name.equals("PARTID")) { } else if (prop.name.equals("ENCODING")) { mBmsg.mBbodyEncoding = prop.value; } else if (prop.name.equals("CHARSET")) { mBmsg.mBbodyCharset = prop.value; } else if (prop.name.equals("LANGUAGE")) { mBmsg.mBbodyLanguage = prop.value; } else if (prop.name.equals("LENGTH")) { try { mBmsg.mBbodyLength = Integer.parseInt(prop.value); } catch (NumberFormatException e) { throw new ParseException("Invalid LENGTH value", mParser.pos()); } } } while (!prop.equals(BEGIN_MSG)); /* * check that the charset is always set to UTF-8. We expect only text transfer (in lieu with * the MAPv12 specifying only RFC2822 (text only) for MMS/EMAIL and SMS do not support * non-text content. If the charset is not set to UTF-8, it is safe to set the message as * empty. We force the getMessage (see BluetoothMasClient) to only call getMessage with * UTF-8 as the MCE is not obliged to support native charset. */ if (!mBmsg.mBbodyCharset.equals("UTF-8")) { Log.e(TAG, "The charset was not set to charset UTF-8: " + mBmsg.mBbodyCharset); } /* * ::={ "BEGIN:MSG" 'message' * "END:MSG" } */ int messageLen = mBmsg.mBbodyLength - MSG_CONTAINER_LEN; int offset = messageLen + CRLF_LEN; int restartPos = mParser.pos() + offset; /* * length is specified in bytes so we need to convert from unicode * string back to bytes array */ String remng = mParser.remaining(); byte[] data = remng.getBytes(); /* restart parsing from after 'message' */ mParser = new BmsgTokenizer(new String(data, offset, data.length - offset), restartPos); prop = mParser.next(true); if (prop != null) { if (prop.equals(END_MSG)) { if (mBmsg.mBbodyCharset.equals("UTF-8")) { mBmsg.mMessage = new String(data, 0, messageLen, StandardCharsets.UTF_8); } else { mBmsg.mMessage = null; } } else { /* Handle possible exception for incorrect LENGTH value * from MSE while parsing GET Message response */ Log.e(TAG, "Prop Invalid: "+ prop.toString()); Log.e(TAG, "Possible Invalid LENGTH value"); throw expected(END_MSG); } } else { data = null; /* * now we check if bMessage can be parsed if LENGTH is handled as * number of characters instead of number of bytes */ if (offset < 0 || offset > remng.length()) { /* Handle possible exception for incorrect LENGTH value * from MSE while parsing GET Message response */ throw new ParseException("Invalid LENGTH value", mParser.pos()); } Log.w(TAG, "byte LENGTH seems to be invalid, trying with char length"); mParser = new BmsgTokenizer(remng.substring(offset)); prop = mParser.next(); if (!prop.equals(END_MSG)) { throw expected(END_MSG); } if (mBmsg.mBbodyCharset.equals("UTF-8")) { mBmsg.mMessage = remng.substring(0, messageLen); } else { mBmsg.mMessage = null; } } prop = mParser.next(); if (!prop.equals(END_BBODY)) { throw expected(END_BBODY); } return mParser.next(); } private Property extractVcard(StringBuilder out) throws IOException, ParseException { Property prop; out.append(BEGIN_VCARD).append(CRLF); do { prop = mParser.next(); out.append(prop).append(CRLF); } while (!prop.equals(END_VCARD)); return mParser.next(); } private class VcardHandler implements VCardEntryHandler { VCardEntry vcard; @Override public void onStart() { } @Override public void onEntryCreated(VCardEntry entry) { vcard = entry; } @Override public void onEnd() { } }; private VCardEntry parseVcard(String str) throws IOException, ParseException { VCardEntry vcard = null; try { VCardParser p = new VCardParser_V21(); VCardEntryConstructor c = new VCardEntryConstructor(); VcardHandler handler = new VcardHandler(); c.addEntryHandler(handler); p.addInterpreter(c); p.parse(new ByteArrayInputStream(str.getBytes())); vcard = handler.vcard; } catch (VCardVersionException e1) { try { VCardParser p = new VCardParser_V30(); VCardEntryConstructor c = new VCardEntryConstructor(); VcardHandler handler = new VcardHandler(); c.addEntryHandler(handler); p.addInterpreter(c); p.parse(new ByteArrayInputStream(str.getBytes())); vcard = handler.vcard; } catch (VCardVersionException e2) { // will throw below } catch (VCardException e2) { // will throw below } } catch (VCardException e1) { // will throw below } if (vcard == null) { throw new ParseException("Cannot parse vCard object (neither 2.1 nor 3.0?)", mParser.pos()); } return vcard; } }