Bluetooth: Support MAP and PBAP Client role on Bluedroid.

Implementation of android.bluetooth.client.pbap and
android.bluetooth.client.map STATIC JAVA lib for PBAP and MAP
client role(s). These static libraries can be used by application
for PBAP and MAP Client role support on Bluedroid.

Change-Id: I173d2c095661704e2efb39516837c6b681193e9a
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..68acd2a
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,35 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+src_dirs:= src/android/bluetooth/client/pbap
+
+LOCAL_SRC_FILES := \
+        $(call all-java-files-under, $(src_dirs))
+
+LOCAL_MODULE:= android.bluetooth.client.pbap
+LOCAL_JAVA_LIBRARIES := javax.obex
+LOCAL_STATIC_JAVA_LIBRARIES := com.android.vcard
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+include $(CLEAR_VARS)
+
+src_dirs:= src/android/bluetooth/client/map
+
+LOCAL_SRC_FILES := \
+        $(call all-java-files-under, $(src_dirs))
+
+LOCAL_MODULE:= android.bluetooth.client.map
+LOCAL_JAVA_LIBRARIES := javax.obex
+LOCAL_STATIC_JAVA_LIBRARIES := com.android.vcard
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/src/android/bluetooth/client/map/BluetoothMapBmessage.java b/src/android/bluetooth/client/map/BluetoothMapBmessage.java
new file mode 100644
index 0000000..84e4c75
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMapBmessage.java
@@ -0,0 +1,169 @@
+/*
+ * 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 com.android.vcard.VCardEntry;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+/**
+ * Object representation of message in bMessage format
+ * <p>
+ * This object will be received in {@link BluetoothMasClient#EVENT_GET_MESSAGE}
+ * callback message.
+ */
+public class BluetoothMapBmessage {
+
+    String mBmsgVersion;
+    Status mBmsgStatus;
+    Type mBmsgType;
+    String mBmsgFolder;
+
+    String mBbodyEncoding;
+    String mBbodyCharset;
+    String mBbodyLanguage;
+    int mBbodyLength;
+
+    String mMessage;
+
+    ArrayList<VCardEntry> mOriginators;
+    ArrayList<VCardEntry> mRecipients;
+
+    public enum Status {
+        READ, UNREAD
+    }
+
+    public enum Type {
+        EMAIL, SMS_GSM, SMS_CDMA, MMS
+    }
+
+    /**
+     * Constructs empty message object
+     */
+    public BluetoothMapBmessage() {
+        mOriginators = new ArrayList<VCardEntry>();
+        mRecipients = new ArrayList<VCardEntry>();
+    }
+
+    public VCardEntry getOriginator() {
+        if (mOriginators.size() > 0) {
+            return mOriginators.get(0);
+        } else {
+            return null;
+        }
+    }
+
+    public ArrayList<VCardEntry> getOriginators() {
+        return mOriginators;
+    }
+
+    public BluetoothMapBmessage addOriginator(VCardEntry vcard) {
+        mOriginators.add(vcard);
+        return this;
+    }
+
+    public ArrayList<VCardEntry> getRecipients() {
+        return mRecipients;
+    }
+
+    public BluetoothMapBmessage addRecipient(VCardEntry vcard) {
+        mRecipients.add(vcard);
+        return this;
+    }
+
+    public Status getStatus() {
+        return mBmsgStatus;
+    }
+
+    public BluetoothMapBmessage setStatus(Status status) {
+        mBmsgStatus = status;
+        return this;
+    }
+
+    public Type getType() {
+        return mBmsgType;
+    }
+
+    public BluetoothMapBmessage setType(Type type) {
+        mBmsgType = type;
+        return this;
+    }
+
+    public String getFolder() {
+        return mBmsgFolder;
+    }
+
+    public BluetoothMapBmessage setFolder(String folder) {
+        mBmsgFolder = folder;
+        return this;
+    }
+
+    public String getEncoding() {
+        return mBbodyEncoding;
+    }
+
+    public BluetoothMapBmessage setEncoding(String encoding) {
+        mBbodyEncoding = encoding;
+        return this;
+    }
+
+    public String getCharset() {
+        return mBbodyCharset;
+    }
+
+    public BluetoothMapBmessage setCharset(String charset) {
+        mBbodyCharset = charset;
+        return this;
+    }
+
+    public String getLanguage() {
+        return mBbodyLanguage;
+    }
+
+    public BluetoothMapBmessage setLanguage(String language) {
+        mBbodyLanguage = language;
+        return this;
+    }
+
+    public String getBodyContent() {
+        return mMessage;
+    }
+
+    public BluetoothMapBmessage setBodyContent(String body) {
+        mMessage = body;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        JSONObject json = new JSONObject();
+
+        try {
+            json.put("status", mBmsgStatus);
+            json.put("type", mBmsgType);
+            json.put("folder", mBmsgFolder);
+            json.put("message", mMessage);
+        } catch (JSONException e) {
+            // do nothing
+        }
+
+        return json.toString();
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMapBmessageBuilder.java b/src/android/bluetooth/client/map/BluetoothMapBmessageBuilder.java
new file mode 100644
index 0000000..8629423
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMapBmessageBuilder.java
@@ -0,0 +1,160 @@
+/*
+ * 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 com.android.vcard.VCardEntry;
+import com.android.vcard.VCardEntry.EmailData;
+import com.android.vcard.VCardEntry.NameData;
+import com.android.vcard.VCardEntry.PhoneData;
+
+import java.util.List;
+
+class BluetoothMapBmessageBuilder {
+
+    private final static String CRLF = "\r\n";
+
+    private final static String BMSG_BEGIN = "BEGIN:BMSG";
+    private final static String BMSG_VERSION = "VERSION:1.0";
+    private final static String BMSG_STATUS = "STATUS:";
+    private final static String BMSG_TYPE = "TYPE:";
+    private final static String BMSG_FOLDER = "FOLDER:";
+    private final static String BMSG_END = "END:BMSG";
+
+    private final static String BENV_BEGIN = "BEGIN:BENV";
+    private final static String BENV_END = "END:BENV";
+
+    private final static String BBODY_BEGIN = "BEGIN:BBODY";
+    private final static String BBODY_ENCODING = "ENCODING:";
+    private final static String BBODY_CHARSET = "CHARSET:";
+    private final static String BBODY_LANGUAGE = "LANGUAGE:";
+    private final static String BBODY_LENGTH = "LENGTH:";
+    private final static String BBODY_END = "END:BBODY";
+
+    private final static String MSG_BEGIN = "BEGIN:MSG";
+    private final static String MSG_END = "END:MSG";
+
+    private final static String VCARD_BEGIN = "BEGIN:VCARD";
+    private final static String VCARD_VERSION = "VERSION:2.1";
+    private final static String VCARD_N = "N:";
+    private final static String VCARD_EMAIL = "EMAIL:";
+    private final static String VCARD_TEL = "TEL:";
+    private final static String VCARD_END = "END:VCARD";
+
+    private final StringBuilder mBmsg;
+
+    private BluetoothMapBmessageBuilder() {
+        mBmsg = new StringBuilder();
+    }
+
+    static public String createBmessage(BluetoothMapBmessage bmsg) {
+        BluetoothMapBmessageBuilder b = new BluetoothMapBmessageBuilder();
+
+        b.build(bmsg);
+
+        return b.mBmsg.toString();
+    }
+
+    private void build(BluetoothMapBmessage bmsg) {
+        int bodyLen = MSG_BEGIN.length() + MSG_END.length() + 3 * CRLF.length()
+                + bmsg.mMessage.getBytes().length;
+
+        mBmsg.append(BMSG_BEGIN).append(CRLF);
+
+        mBmsg.append(BMSG_VERSION).append(CRLF);
+        mBmsg.append(BMSG_STATUS).append(bmsg.mBmsgStatus).append(CRLF);
+        mBmsg.append(BMSG_TYPE).append(bmsg.mBmsgType).append(CRLF);
+        mBmsg.append(BMSG_FOLDER).append(bmsg.mBmsgFolder).append(CRLF);
+
+        for (VCardEntry vcard : bmsg.mOriginators) {
+            buildVcard(vcard);
+        }
+
+        {
+            mBmsg.append(BENV_BEGIN).append(CRLF);
+
+            for (VCardEntry vcard : bmsg.mRecipients) {
+                buildVcard(vcard);
+            }
+
+            {
+                mBmsg.append(BBODY_BEGIN).append(CRLF);
+
+                if (bmsg.mBbodyEncoding != null) {
+                    mBmsg.append(BBODY_ENCODING).append(bmsg.mBbodyEncoding).append(CRLF);
+                }
+
+                if (bmsg.mBbodyCharset != null) {
+                    mBmsg.append(BBODY_CHARSET).append(bmsg.mBbodyCharset).append(CRLF);
+                }
+
+                if (bmsg.mBbodyLanguage != null) {
+                    mBmsg.append(BBODY_LANGUAGE).append(bmsg.mBbodyLanguage).append(CRLF);
+                }
+
+                mBmsg.append(BBODY_LENGTH).append(bodyLen).append(CRLF);
+
+                {
+                    mBmsg.append(MSG_BEGIN).append(CRLF);
+
+                    mBmsg.append(bmsg.mMessage).append(CRLF);
+
+                    mBmsg.append(MSG_END).append(CRLF);
+                }
+
+                mBmsg.append(BBODY_END).append(CRLF);
+            }
+
+            mBmsg.append(BENV_END).append(CRLF);
+        }
+
+        mBmsg.append(BMSG_END).append(CRLF);
+    }
+
+    private void buildVcard(VCardEntry vcard) {
+        String n = buildVcardN(vcard);
+        List<PhoneData> tel = vcard.getPhoneList();
+        List<EmailData> email = vcard.getEmailList();
+
+        mBmsg.append(VCARD_BEGIN).append(CRLF);
+
+        mBmsg.append(VCARD_VERSION).append(CRLF);
+
+        mBmsg.append(VCARD_N).append(n).append(CRLF);
+
+        if (tel != null && tel.size() > 0) {
+            mBmsg.append(VCARD_TEL).append(tel.get(0).getNumber()).append(CRLF);
+        }
+
+        if (email != null && email.size() > 0) {
+            mBmsg.append(VCARD_EMAIL).append(email.get(0).getAddress()).append(CRLF);
+        }
+
+        mBmsg.append(VCARD_END).append(CRLF);
+    }
+
+    private String buildVcardN(VCardEntry vcard) {
+        NameData nd = vcard.getNameData();
+        StringBuilder sb = new StringBuilder();
+
+        sb.append(nd.getFamily()).append(";");
+        sb.append(nd.getGiven() == null ? "" : nd.getGiven()).append(";");
+        sb.append(nd.getMiddle() == null ? "" : nd.getMiddle()).append(";");
+        sb.append(nd.getPrefix() == null ? "" : nd.getPrefix()).append(";");
+        sb.append(nd.getSuffix() == null ? "" : nd.getSuffix());
+
+        return sb.toString();
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMapBmessageParser.java b/src/android/bluetooth/client/map/BluetoothMapBmessageParser.java
new file mode 100644
index 0000000..fa3d817
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMapBmessageParser.java
@@ -0,0 +1,421 @@
+/*
+ * 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.text.ParseException;
+
+class BluetoothMapBmessageParser {
+
+    private final static String TAG = "BluetoothMapBmessageParser";
+
+    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<CRLF> + <CRLF> + END:MSG<CRFL>
+     */
+    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();
+
+        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;
+
+        /*
+         * <bmessage-object>::= { "BEGIN:BMSG" <CRLF> <bmessage-property>
+         * [<bmessage-originator>]* <bmessage-envelope> "END:BMSG" <CRLF> }
+         */
+
+        mParser = new BmsgTokenizer(str + CRLF);
+
+        prop = mParser.next();
+        if (!prop.equals(BEGIN_BMSG)) {
+            throw expected(BEGIN_BMSG);
+        }
+
+        prop = parseProperties();
+
+        while (prop.equals(BEGIN_VCARD)) {
+
+            /* <bmessage-originator>::= <vcard> <CRLF> */
+
+            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;
+
+        /*
+         * <bmessage-property>::=<bmessage-version-property>
+         * <bmessage-readstatus-property> <bmessage-type-property>
+         * <bmessage-folder-property> <bmessage-version-property>::="VERSION:"
+         * <common-digit>*"."<common-digit>* <CRLF>
+         * <bmessage-readstatus-property>::="STATUS:" 'readstatus' <CRLF>
+         * <bmessage-type-property>::="TYPE:" 'type' <CRLF>
+         * <bmessage-folder-property>::="FOLDER:" 'foldername' <CRLF>
+         */
+
+        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());
+        }
+
+        /*
+         * <bmessage-envelope> ::= { "BEGIN:BENV" <CRLF> [<bmessage-recipient>]*
+         * <bmessage-envelope> | <bmessage-content> "END:BENV" <CRLF> }
+         */
+
+        prop = mParser.next();
+
+        while (prop.equals(BEGIN_VCARD)) {
+
+            /* <bmessage-originator>::= <vcard> <CRLF> */
+
+            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;
+
+        /*
+         * <bmessage-content>::= { "BEGIN:BBODY"<CRLF> [<bmessage-body-part-ID>
+         * <CRLF>] <bmessage-body-property> <bmessage-body-content>* <CRLF>
+         * "END:BBODY"<CRLF> } <bmessage-body-part-ID>::="PARTID:" 'Part-ID'
+         * <bmessage-body-property>::=[<bmessage-body-encoding-property>]
+         * [<bmessage-body-charset-property>]
+         * [<bmessage-body-language-property>]
+         * <bmessage-body-content-length-property>
+         * <bmessage-body-encoding-property>::="ENCODING:"'encoding' <CRLF>
+         * <bmessage-body-charset-property>::="CHARSET:"'charset' <CRLF>
+         * <bmessage-body-language-property>::="LANGUAGE:"'language' <CRLF>
+         * <bmessage-body-content-length-property>::= "LENGTH:" <common-digit>*
+         * <CRLF>
+         */
+
+        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.valueOf(prop.value);
+                } catch (NumberFormatException e) {
+                    throw new ParseException("Invalid LENGTH value", mParser.pos());
+                }
+
+            }
+
+        } while (!prop.equals(BEGIN_MSG));
+
+        /*
+         * <bmessage-body-content>::={ "BEGIN:MSG"<CRLF> 'message'<CRLF>
+         * "END:MSG"<CRLF> }
+         */
+
+        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'<CRLF> */
+        mParser = new BmsgTokenizer(new String(data, offset, data.length - offset), restartPos);
+
+        prop = mParser.next(true);
+
+        if (prop != null && prop.equals(END_MSG)) {
+            mBmsg.mMessage = new String(data, 0, messageLen);
+        } else {
+
+            data = null;
+
+            /*
+             * now we check if bMessage can be parsed if LENGTH is handled as
+             * number of characters instead of number of bytes
+             */
+
+            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);
+            }
+
+            mBmsg.mMessage = remng.substring(0, messageLen);
+        }
+
+        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;
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMapEventReport.java b/src/android/bluetooth/client/map/BluetoothMapEventReport.java
new file mode 100644
index 0000000..5963db4
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMapEventReport.java
@@ -0,0 +1,223 @@
+/*
+ * 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 org.json.JSONException;
+import org.json.JSONObject;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.HashMap;
+
+/**
+ * Object representation of event report received by MNS
+ * <p>
+ * This object will be received in {@link BluetoothMasClient#EVENT_EVENT_REPORT}
+ * callback message.
+ */
+public class BluetoothMapEventReport {
+
+    private final static String TAG = "BluetoothMapEventReport";
+
+    public enum Type {
+        NEW_MESSAGE("NewMessage"), DELIVERY_SUCCESS("DeliverySuccess"),
+        SENDING_SUCCESS("SendingSuccess"), DELIVERY_FAILURE("DeliveryFailure"),
+        SENDING_FAILURE("SendingFailure"), MEMORY_FULL("MemoryFull"),
+        MEMORY_AVAILABLE("MemoryAvailable"), MESSAGE_DELETED("MessageDeleted"),
+        MESSAGE_SHIFT("MessageShift");
+
+        private final String mSpecName;
+
+        private Type(String specName) {
+            mSpecName = specName;
+        }
+
+        @Override
+        public String toString() {
+            return mSpecName;
+        }
+    }
+
+    private final Type mType;
+
+    private final String mHandle;
+
+    private final String mFolder;
+
+    private final String mOldFolder;
+
+    private final BluetoothMapBmessage.Type mMsgType;
+
+    private BluetoothMapEventReport(HashMap<String, String> attrs) throws IllegalArgumentException {
+        mType = parseType(attrs.get("type"));
+
+        if (mType != Type.MEMORY_FULL && mType != Type.MEMORY_AVAILABLE) {
+            String handle = attrs.get("handle");
+            try {
+                /* just to validate */
+                new BigInteger(attrs.get("handle"), 16);
+
+                mHandle = attrs.get("handle");
+            } catch (NumberFormatException e) {
+                throw new IllegalArgumentException("Invalid value for handle:" + handle);
+            }
+        } else {
+            mHandle = null;
+        }
+
+        mFolder = attrs.get("folder");
+
+        mOldFolder = attrs.get("old_folder");
+
+        if (mType != Type.MEMORY_FULL && mType != Type.MEMORY_AVAILABLE) {
+            String s = attrs.get("msg_type");
+
+            if ("".equals(s)) {
+                // Some phones (e.g. SGS3 for MessageDeleted) send empty
+                // msg_type, in such case leave it as null rather than throw
+                // parse exception
+                mMsgType = null;
+            } else {
+                mMsgType = parseMsgType(s);
+            }
+        } else {
+            mMsgType = null;
+        }
+    }
+
+    private Type parseType(String type) throws IllegalArgumentException {
+        for (Type t : Type.values()) {
+            if (t.toString().equals(type)) {
+                return t;
+            }
+        }
+
+        throw new IllegalArgumentException("Invalid value for type: " + type);
+    }
+
+    private BluetoothMapBmessage.Type parseMsgType(String msgType) throws IllegalArgumentException {
+        for (BluetoothMapBmessage.Type t : BluetoothMapBmessage.Type.values()) {
+            if (t.name().equals(msgType)) {
+                return t;
+            }
+        }
+
+        throw new IllegalArgumentException("Invalid value for msg_type: " + msgType);
+    }
+
+    /**
+     * @return {@link BluetoothMapEventReport.Type} object corresponding to
+     *         <code>type</code> application parameter in MAP specification
+     */
+    public Type getType() {
+        return mType;
+    }
+
+    /**
+     * @return value corresponding to <code>handle</code> parameter in MAP
+     *         specification
+     */
+    public String getHandle() {
+        return mHandle;
+    }
+
+    /**
+     * @return value corresponding to <code>folder</code> parameter in MAP
+     *         specification
+     */
+    public String getFolder() {
+        return mFolder;
+    }
+
+    /**
+     * @return value corresponding to <code>old_folder</code> parameter in MAP
+     *         specification
+     */
+    public String getOldFolder() {
+        return mOldFolder;
+    }
+
+    /**
+     * @return {@link BluetoothMapBmessage.Type} object corresponding to
+     *         <code>msg_type</code> application parameter in MAP specification
+     */
+    public BluetoothMapBmessage.Type getMsgType() {
+        return mMsgType;
+    }
+
+    @Override
+    public String toString() {
+        JSONObject json = new JSONObject();
+
+        try {
+            json.put("type", mType);
+            json.put("handle", mHandle);
+            json.put("folder", mFolder);
+            json.put("old_folder", mOldFolder);
+            json.put("msg_type", mMsgType);
+        } catch (JSONException e) {
+            // do nothing
+        }
+
+        return json.toString();
+    }
+
+    static BluetoothMapEventReport fromStream(DataInputStream in) {
+        BluetoothMapEventReport ev = null;
+
+        try {
+            XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();
+            xpp.setInput(in, "utf-8");
+
+            int event = xpp.getEventType();
+            while (event != XmlPullParser.END_DOCUMENT) {
+                switch (event) {
+                    case XmlPullParser.START_TAG:
+                        if (xpp.getName().equals("event")) {
+                            HashMap<String, String> attrs = new HashMap<String, String>();
+
+                            for (int i = 0; i < xpp.getAttributeCount(); i++) {
+                                attrs.put(xpp.getAttributeName(i), xpp.getAttributeValue(i));
+                            }
+
+                            ev = new BluetoothMapEventReport(attrs);
+
+                            // return immediately, only one event should be here
+                            return ev;
+                        }
+                        break;
+                }
+
+                event = xpp.next();
+            }
+
+        } catch (XmlPullParserException e) {
+            Log.e(TAG, "XML parser error when parsing XML", e);
+        } catch (IOException e) {
+            Log.e(TAG, "I/O error when parsing XML", e);
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "Invalid event received", e);
+        }
+
+        return ev;
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMapFolderListing.java b/src/android/bluetooth/client/map/BluetoothMapFolderListing.java
new file mode 100644
index 0000000..f0494b3
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMapFolderListing.java
@@ -0,0 +1,69 @@
+/*
+ * 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 org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+class BluetoothMapFolderListing {
+
+    private static final String TAG = "BluetoothMasFolderListing";
+
+    private final ArrayList<String> mFolders;
+
+    public BluetoothMapFolderListing(InputStream in) {
+        mFolders = new ArrayList<String>();
+
+        parse(in);
+    }
+
+    public void parse(InputStream in) {
+
+        try {
+            XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();
+            xpp.setInput(in, "utf-8");
+
+            int event = xpp.getEventType();
+            while (event != XmlPullParser.END_DOCUMENT) {
+                switch (event) {
+                    case XmlPullParser.START_TAG:
+                        if (xpp.getName().equals("folder")) {
+                            mFolders.add(xpp.getAttributeValue(null, "name"));
+                        }
+                        break;
+                }
+
+                event = xpp.next();
+            }
+
+        } catch (XmlPullParserException e) {
+            Log.e(TAG, "XML parser error when parsing XML", e);
+        } catch (IOException e) {
+            Log.e(TAG, "I/O error when parsing XML", e);
+        }
+    }
+
+    public ArrayList<String> getList() {
+        return mFolders;
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMapMessage.java b/src/android/bluetooth/client/map/BluetoothMapMessage.java
new file mode 100644
index 0000000..6c76bbe
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMapMessage.java
@@ -0,0 +1,332 @@
+/*
+ * 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.bluetooth.client.map.utils.ObexTime;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.math.BigInteger;
+import java.util.Date;
+import java.util.HashMap;
+
+/**
+ * Object representation of message received in messages listing
+ * <p>
+ * This object will be received in
+ * {@link BluetoothMasClient#EVENT_GET_MESSAGES_LISTING} callback message.
+ */
+public class BluetoothMapMessage {
+
+    private final String mHandle;
+
+    private final String mSubject;
+
+    private final Date mDateTime;
+
+    private final String mSenderName;
+
+    private final String mSenderAddressing;
+
+    private final String mReplytoAddressing;
+
+    private final String mRecipientName;
+
+    private final String mRecipientAddressing;
+
+    private final Type mType;
+
+    private final int mSize;
+
+    private final boolean mText;
+
+    private final ReceptionStatus mReceptionStatus;
+
+    private final int mAttachmentSize;
+
+    private final boolean mPriority;
+
+    private final boolean mRead;
+
+    private final boolean mSent;
+
+    private final boolean mProtected;
+
+    public enum Type {
+        UNKNOWN, EMAIL, SMS_GSM, SMS_CDMA, MMS
+    };
+
+    public enum ReceptionStatus {
+        UNKNOWN, COMPLETE, FRACTIONED, NOTIFICATION
+    }
+
+    BluetoothMapMessage(HashMap<String, String> attrs) throws IllegalArgumentException {
+        int size;
+
+        try {
+            /* just to validate */
+            new BigInteger(attrs.get("handle"), 16);
+
+            mHandle = attrs.get("handle");
+        } catch (NumberFormatException e) {
+            /*
+             * handle MUST have proper value, if it does not then throw
+             * something here
+             */
+            throw new IllegalArgumentException(e);
+        }
+
+        mSubject = attrs.get("subject");
+
+        mDateTime = (new ObexTime(attrs.get("datetime"))).getTime();
+
+        mSenderName = attrs.get("sender_name");
+
+        mSenderAddressing = attrs.get("sender_addressing");
+
+        mReplytoAddressing = attrs.get("replyto_addressing");
+
+        mRecipientName = attrs.get("recipient_name");
+
+        mRecipientAddressing = attrs.get("recipient_addressing");
+
+        mType = strToType(attrs.get("type"));
+
+        try {
+            size = Integer.parseInt(attrs.get("size"));
+        } catch (NumberFormatException e) {
+            size = 0;
+        }
+
+        mSize = size;
+
+        mText = yesnoToBoolean(attrs.get("text"));
+
+        mReceptionStatus = strToReceptionStatus(attrs.get("reception_status"));
+
+        try {
+            size = Integer.parseInt(attrs.get("attachment_size"));
+        } catch (NumberFormatException e) {
+            size = 0;
+        }
+
+        mAttachmentSize = size;
+
+        mPriority = yesnoToBoolean(attrs.get("priority"));
+
+        mRead = yesnoToBoolean(attrs.get("read"));
+
+        mSent = yesnoToBoolean(attrs.get("sent"));
+
+        mProtected = yesnoToBoolean(attrs.get("protected"));
+    }
+
+    private boolean yesnoToBoolean(String yesno) {
+        return "yes".equals(yesno);
+    }
+
+    private Type strToType(String s) {
+        if ("EMAIL".equals(s)) {
+            return Type.EMAIL;
+        } else if ("SMS_GSM".equals(s)) {
+            return Type.SMS_GSM;
+        } else if ("SMS_CDMA".equals(s)) {
+            return Type.SMS_CDMA;
+        } else if ("MMS".equals(s)) {
+            return Type.MMS;
+        }
+
+        return Type.UNKNOWN;
+    }
+
+    private ReceptionStatus strToReceptionStatus(String s) {
+        if ("complete".equals(s)) {
+            return ReceptionStatus.COMPLETE;
+        } else if ("fractioned".equals(s)) {
+            return ReceptionStatus.FRACTIONED;
+        } else if ("notification".equals(s)) {
+            return ReceptionStatus.NOTIFICATION;
+        }
+
+        return ReceptionStatus.UNKNOWN;
+    }
+
+    @Override
+    public String toString() {
+        JSONObject json = new JSONObject();
+
+        try {
+            json.put("handle", mHandle);
+            json.put("subject", mSubject);
+            json.put("datetime", mDateTime);
+            json.put("sender_name", mSenderName);
+            json.put("sender_addressing", mSenderAddressing);
+            json.put("replyto_addressing", mReplytoAddressing);
+            json.put("recipient_name", mRecipientName);
+            json.put("recipient_addressing", mRecipientAddressing);
+            json.put("type", mType);
+            json.put("size", mSize);
+            json.put("text", mText);
+            json.put("reception_status", mReceptionStatus);
+            json.put("attachment_size", mAttachmentSize);
+            json.put("priority", mPriority);
+            json.put("read", mRead);
+            json.put("sent", mSent);
+            json.put("protected", mProtected);
+        } catch (JSONException e) {
+            // do nothing
+        }
+
+        return json.toString();
+    }
+
+    /**
+     * @return value corresponding to <code>handle</code> parameter in MAP
+     *         specification
+     */
+    public String getHandle() {
+        return mHandle;
+    }
+
+    /**
+     * @return value corresponding to <code>subject</code> parameter in MAP
+     *         specification
+     */
+    public String getSubject() {
+        return mSubject;
+    }
+
+    /**
+     * @return <code>Date</code> object corresponding to <code>datetime</code>
+     *         parameter in MAP specification
+     */
+    public Date getDateTime() {
+        return mDateTime;
+    }
+
+    /**
+     * @return value corresponding to <code>sender_name</code> parameter in MAP
+     *         specification
+     */
+    public String getSenderName() {
+        return mSenderName;
+    }
+
+    /**
+     * @return value corresponding to <code>sender_addressing</code> parameter
+     *         in MAP specification
+     */
+    public String getSenderAddressing() {
+        return mSenderAddressing;
+    }
+
+    /**
+     * @return value corresponding to <code>replyto_addressing</code> parameter
+     *         in MAP specification
+     */
+    public String getReplytoAddressing() {
+        return mReplytoAddressing;
+    }
+
+    /**
+     * @return value corresponding to <code>recipient_name</code> parameter in
+     *         MAP specification
+     */
+    public String getRecipientName() {
+        return mRecipientName;
+    }
+
+    /**
+     * @return value corresponding to <code>recipient_addressing</code>
+     *         parameter in MAP specification
+     */
+    public String getRecipientAddressing() {
+        return mRecipientAddressing;
+    }
+
+    /**
+     * @return {@link Type} object corresponding to <code>type</code> parameter
+     *         in MAP specification
+     */
+    public Type getType() {
+        return mType;
+    }
+
+    /**
+     * @return value corresponding to <code>size</code> parameter in MAP
+     *         specification
+     */
+    public int getSize() {
+        return mSize;
+    }
+
+    /**
+     * @return {@link .ReceptionStatus} object corresponding to
+     *         <code>reception_status</code> parameter in MAP specification
+     */
+    public ReceptionStatus getReceptionStatus() {
+        return mReceptionStatus;
+    }
+
+    /**
+     * @return value corresponding to <code>attachment_size</code> parameter in
+     *         MAP specification
+     */
+    public int getAttachmentSize() {
+        return mAttachmentSize;
+    }
+
+    /**
+     * @return value corresponding to <code>text</code> parameter in MAP
+     *         specification
+     */
+    public boolean isText() {
+        return mText;
+    }
+
+    /**
+     * @return value corresponding to <code>priority</code> parameter in MAP
+     *         specification
+     */
+    public boolean isPriority() {
+        return mPriority;
+    }
+
+    /**
+     * @return value corresponding to <code>read</code> parameter in MAP
+     *         specification
+     */
+    public boolean isRead() {
+        return mRead;
+    }
+
+    /**
+     * @return value corresponding to <code>sent</code> parameter in MAP
+     *         specification
+     */
+    public boolean isSent() {
+        return mSent;
+    }
+
+    /**
+     * @return value corresponding to <code>protected</code> parameter in MAP
+     *         specification
+     */
+    public boolean isProtected() {
+        return mProtected;
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMapMessagesListing.java b/src/android/bluetooth/client/map/BluetoothMapMessagesListing.java
new file mode 100644
index 0000000..2fb3dea
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMapMessagesListing.java
@@ -0,0 +1,84 @@
+/*
+ * 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 org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+class BluetoothMapMessagesListing {
+
+    private static final String TAG = "BluetoothMapMessagesListing";
+
+    private final ArrayList<BluetoothMapMessage> mMessages;
+
+    public BluetoothMapMessagesListing(InputStream in) {
+        mMessages = new ArrayList<BluetoothMapMessage>();
+
+        parse(in);
+    }
+
+    public void parse(InputStream in) {
+
+        try {
+            XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();
+            xpp.setInput(in, "utf-8");
+
+            int event = xpp.getEventType();
+            while (event != XmlPullParser.END_DOCUMENT) {
+                switch (event) {
+                    case XmlPullParser.START_TAG:
+                        if (xpp.getName().equals("msg")) {
+
+                            HashMap<String, String> attrs = new HashMap<String, String>();
+
+                            for (int i = 0; i < xpp.getAttributeCount(); i++) {
+                                attrs.put(xpp.getAttributeName(i), xpp.getAttributeValue(i));
+                            }
+
+                            try {
+                                BluetoothMapMessage msg = new BluetoothMapMessage(attrs);
+                                mMessages.add(msg);
+                            } catch (IllegalArgumentException e) {
+                                /* TODO: provide something more useful here */
+                                Log.w(TAG, "Invalid <msg/>");
+                            }
+                        }
+                        break;
+                }
+
+                event = xpp.next();
+            }
+
+        } catch (XmlPullParserException e) {
+            Log.e(TAG, "XML parser error when parsing XML", e);
+        } catch (IOException e) {
+            Log.e(TAG, "I/O error when parsing XML", e);
+        }
+    }
+
+    public ArrayList<BluetoothMapMessage> getList() {
+        return mMessages;
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMapRfcommTransport.java b/src/android/bluetooth/client/map/BluetoothMapRfcommTransport.java
new file mode 100644
index 0000000..0b1b624
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMapRfcommTransport.java
@@ -0,0 +1,77 @@
+/*
+ * 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.bluetooth.BluetoothSocket;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.obex.ObexTransport;
+
+class BluetoothMapRfcommTransport implements ObexTransport {
+    private final BluetoothSocket mSocket;
+
+    public BluetoothMapRfcommTransport(BluetoothSocket socket) {
+        super();
+        mSocket = socket;
+    }
+
+    @Override
+    public void create() throws IOException {
+    }
+
+    @Override
+    public void listen() throws IOException {
+    }
+
+    @Override
+    public void close() throws IOException {
+        mSocket.close();
+    }
+
+    @Override
+    public void connect() throws IOException {
+    }
+
+    @Override
+    public void disconnect() throws IOException {
+    }
+
+    @Override
+    public InputStream openInputStream() throws IOException {
+        return mSocket.getInputStream();
+    }
+
+    @Override
+    public OutputStream openOutputStream() throws IOException {
+        return mSocket.getOutputStream();
+    }
+
+    @Override
+    public DataInputStream openDataInputStream() throws IOException {
+        return new DataInputStream(openInputStream());
+    }
+
+    @Override
+    public DataOutputStream openDataOutputStream() throws IOException {
+        return new DataOutputStream(openOutputStream());
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMasClient.java b/src/android/bluetooth/client/map/BluetoothMasClient.java
new file mode 100644
index 0000000..7d50e5b
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMasClient.java
@@ -0,0 +1,1102 @@
+/*
+ * 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.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothMasInstance;
+import android.bluetooth.BluetoothSocket;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import android.bluetooth.client.map.BluetoothMasRequestSetMessageStatus.StatusIndicator;
+import android.bluetooth.client.map.utils.ObexTime;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.math.BigInteger;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+
+import javax.obex.ObexTransport;
+
+public class BluetoothMasClient {
+
+    private final static String TAG = "BluetoothMasClient";
+
+    private static final int SOCKET_CONNECTED = 10;
+
+    private static final int SOCKET_ERROR = 11;
+
+    /**
+     * Callback message sent when connection state changes
+     * <p>
+     * <code>arg1</code> is set to {@link #STATUS_OK} when connection is
+     * established successfully and {@link #STATUS_FAILED} when connection
+     * either failed or was disconnected (depends on request from application)
+     *
+     * @see #connect()
+     * @see #disconnect()
+     */
+    public static final int EVENT_CONNECT = 1;
+
+    /**
+     * Callback message sent when MSE accepted update inbox request
+     *
+     * @see #updateInbox()
+     */
+    public static final int EVENT_UPDATE_INBOX = 2;
+
+    /**
+     * Callback message sent when path is changed
+     * <p>
+     * <code>obj</code> is set to path currently set on MSE
+     *
+     * @see #setFolderRoot()
+     * @see #setFolderUp()
+     * @see #setFolderDown(String)
+     */
+    public static final int EVENT_SET_PATH = 3;
+
+    /**
+     * Callback message sent when folder listing is received
+     * <p>
+     * <code>obj</code> contains ArrayList of sub-folder names
+     *
+     * @see #getFolderListing()
+     * @see #getFolderListing(int, int)
+     */
+    public static final int EVENT_GET_FOLDER_LISTING = 4;
+
+    /**
+     * Callback message sent when folder listing size is received
+     * <p>
+     * <code>obj</code> contains number of items in folder listing
+     *
+     * @see #getFolderListingSize()
+     */
+    public static final int EVENT_GET_FOLDER_LISTING_SIZE = 5;
+
+    /**
+     * Callback message sent when messages listing is received
+     * <p>
+     * <code>obj</code> contains ArrayList of {@link BluetoothMapBmessage}
+     *
+     * @see #getMessagesListing(String, int)
+     * @see #getMessagesListing(String, int, MessagesFilter, int)
+     * @see #getMessagesListing(String, int, MessagesFilter, int, int, int)
+     */
+    public static final int EVENT_GET_MESSAGES_LISTING = 6;
+
+    /**
+     * Callback message sent when message is received
+     * <p>
+     * <code>obj</code> contains {@link BluetoothMapBmessage}
+     *
+     * @see #getMessage(String, CharsetType, boolean)
+     */
+    public static final int EVENT_GET_MESSAGE = 7;
+
+    /**
+     * Callback message sent when message status is changed
+     *
+     * @see #setMessageDeletedStatus(String, boolean)
+     * @see #setMessageReadStatus(String, boolean)
+     */
+    public static final int EVENT_SET_MESSAGE_STATUS = 8;
+
+    /**
+     * Callback message sent when message is pushed to MSE
+     * <p>
+     * <code>obj</code> contains handle of message as allocated by MSE
+     *
+     * @see #pushMessage(String, BluetoothMapBmessage, CharsetType)
+     * @see #pushMessage(String, BluetoothMapBmessage, CharsetType, boolean,
+     *      boolean)
+     */
+    public static final int EVENT_PUSH_MESSAGE = 9;
+
+    /**
+     * Callback message sent when notification status is changed
+     * <p>
+     * <code>obj</code> contains <code>1</code> if notifications are enabled and
+     * <code>0</code> otherwise
+     *
+     * @see #setNotificationRegistration(boolean)
+     */
+    public static final int EVENT_SET_NOTIFICATION_REGISTRATION = 10;
+
+    /**
+     * Callback message sent when event report is received from MSE to MNS
+     * <p>
+     * <code>obj</code> contains {@link BluetoothMapEventReport}
+     *
+     * @see #setNotificationRegistration(boolean)
+     */
+    public static final int EVENT_EVENT_REPORT = 11;
+
+    /**
+     * Callback message sent when messages listing size is received
+     * <p>
+     * <code>obj</code> contains number of items in messages listing
+     *
+     * @see #getMessagesListingSize()
+     */
+    public static final int EVENT_GET_MESSAGES_LISTING_SIZE = 12;
+
+    /**
+     * Status for callback message when request is successful
+     */
+    public static final int STATUS_OK = 0;
+
+    /**
+     * Status for callback message when request is not successful
+     */
+    public static final int STATUS_FAILED = 1;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_DEFAULT = 0x00000000;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_SUBJECT = 0x00000001;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_DATETIME = 0x00000002;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_SENDER_NAME = 0x00000004;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_SENDER_ADDRESSING = 0x00000008;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_RECIPIENT_NAME = 0x00000010;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_RECIPIENT_ADDRESSING = 0x00000020;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_TYPE = 0x00000040;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_SIZE = 0x00000080;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_RECEPTION_STATUS = 0x00000100;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_TEXT = 0x00000200;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_ATTACHMENT_SIZE = 0x00000400;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_PRIORITY = 0x00000800;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_READ = 0x00001000;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_SENT = 0x00002000;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_PROTECTED = 0x00004000;
+
+    /**
+     * Constant corresponding to <code>ParameterMask</code> application
+     * parameter value in MAP specification
+     */
+    public static final int PARAMETER_REPLYTO_ADDRESSING = 0x00008000;
+
+    public enum ConnectionState {
+        DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING;
+    }
+
+    public enum CharsetType {
+        NATIVE, UTF_8;
+    }
+
+    /** device associated with client */
+    private final BluetoothDevice mDevice;
+
+    /** MAS instance associated with client */
+    private final BluetoothMasInstance mMas;
+
+    /** callback handler to application */
+    private final Handler mCallback;
+
+    private ConnectionState mConnectionState = ConnectionState.DISCONNECTED;
+
+    private boolean mNotificationEnabled = false;
+
+    private SocketConnectThread mConnectThread = null;
+
+    private ObexTransport mObexTransport = null;
+
+    private BluetoothMasObexClientSession mObexSession = null;
+
+    private SessionHandler mSessionHandler = null;
+
+    private BluetoothMnsService mMnsService = null;
+
+    private ArrayDeque<String> mPath = null;
+
+    private static class SessionHandler extends Handler {
+
+        private final WeakReference<BluetoothMasClient> mClient;
+
+        public SessionHandler(BluetoothMasClient client) {
+            super();
+
+            mClient = new WeakReference<BluetoothMasClient>(client);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+
+            BluetoothMasClient client = mClient.get();
+            if (client == null) {
+                return;
+            }
+            Log.v(TAG, "handleMessage  "+msg.what);
+
+            switch (msg.what) {
+                case SOCKET_ERROR:
+                    client.mConnectThread = null;
+                    client.sendToClient(EVENT_CONNECT, false);
+                    break;
+
+                case SOCKET_CONNECTED:
+                    client.mConnectThread = null;
+
+                    client.mObexTransport = (ObexTransport) msg.obj;
+
+                    client.mObexSession = new BluetoothMasObexClientSession(client.mObexTransport,
+                            client.mSessionHandler);
+                    client.mObexSession.start();
+                    break;
+
+                case BluetoothMasObexClientSession.MSG_OBEX_CONNECTED:
+                    client.mPath.clear(); // we're in root after connected
+                    client.mConnectionState = ConnectionState.CONNECTED;
+                    client.sendToClient(EVENT_CONNECT, true);
+                    break;
+
+                case BluetoothMasObexClientSession.MSG_OBEX_DISCONNECTED:
+                    client.mConnectionState = ConnectionState.DISCONNECTED;
+                    client.mNotificationEnabled = false;
+                    client.mObexSession = null;
+                    client.sendToClient(EVENT_CONNECT, false);
+                    break;
+
+                case BluetoothMasObexClientSession.MSG_REQUEST_COMPLETED:
+                    BluetoothMasRequest request = (BluetoothMasRequest) msg.obj;
+                    int status = request.isSuccess() ? STATUS_OK : STATUS_FAILED;
+
+                    Log.v(TAG, "MSG_REQUEST_COMPLETED (" + status + ") for "
+                            + request.getClass().getName());
+
+                    if (request instanceof BluetoothMasRequestUpdateInbox) {
+                        client.sendToClient(EVENT_UPDATE_INBOX, request.isSuccess());
+
+                    } else if (request instanceof BluetoothMasRequestSetPath) {
+                        if (request.isSuccess()) {
+                            BluetoothMasRequestSetPath req = (BluetoothMasRequestSetPath) request;
+                            switch (req.mDir) {
+                                case UP:
+                                    if (client.mPath.size() > 0) {
+                                        client.mPath.removeLast();
+                                    }
+                                    break;
+
+                                case ROOT:
+                                    client.mPath.clear();
+                                    break;
+
+                                case DOWN:
+                                    client.mPath.addLast(req.mName);
+                                    break;
+                            }
+                        }
+
+                        client.sendToClient(EVENT_SET_PATH, request.isSuccess(),
+                                client.getCurrentPath());
+
+                    } else if (request instanceof BluetoothMasRequestGetFolderListing) {
+                        BluetoothMasRequestGetFolderListing req = (BluetoothMasRequestGetFolderListing) request;
+                        ArrayList<String> folders = req.getList();
+
+                        client.sendToClient(EVENT_GET_FOLDER_LISTING, request.isSuccess(), folders);
+
+                    } else if (request instanceof BluetoothMasRequestGetFolderListingSize) {
+                        int size = ((BluetoothMasRequestGetFolderListingSize) request).getSize();
+
+                        client.sendToClient(EVENT_GET_FOLDER_LISTING_SIZE, request.isSuccess(),
+                                size);
+
+                    } else if (request instanceof BluetoothMasRequestGetMessagesListing) {
+                        BluetoothMasRequestGetMessagesListing req = (BluetoothMasRequestGetMessagesListing) request;
+                        ArrayList<BluetoothMapMessage> msgs = req.getList();
+
+                        client.sendToClient(EVENT_GET_MESSAGES_LISTING, request.isSuccess(), msgs);
+
+                    } else if (request instanceof BluetoothMasRequestGetMessage) {
+                        BluetoothMasRequestGetMessage req = (BluetoothMasRequestGetMessage) request;
+                        BluetoothMapBmessage bmsg = req.getMessage();
+
+                        client.sendToClient(EVENT_GET_MESSAGE, request.isSuccess(), bmsg);
+
+                    } else if (request instanceof BluetoothMasRequestSetMessageStatus) {
+                        client.sendToClient(EVENT_SET_MESSAGE_STATUS, request.isSuccess());
+
+                    } else if (request instanceof BluetoothMasRequestPushMessage) {
+                        BluetoothMasRequestPushMessage req = (BluetoothMasRequestPushMessage) request;
+                        String handle = req.getMsgHandle();
+
+                        client.sendToClient(EVENT_PUSH_MESSAGE, request.isSuccess(), handle);
+
+                    } else if (request instanceof BluetoothMasRequestSetNotificationRegistration) {
+                        BluetoothMasRequestSetNotificationRegistration req = (BluetoothMasRequestSetNotificationRegistration) request;
+
+                        client.mNotificationEnabled = req.isSuccess() ? req.getStatus()
+                                : client.mNotificationEnabled;
+
+                        client.sendToClient(EVENT_SET_NOTIFICATION_REGISTRATION,
+                                request.isSuccess(),
+                                client.mNotificationEnabled ? 1 : 0);
+                    } else if (request instanceof BluetoothMasRequestGetMessagesListingSize) {
+                        int size = ((BluetoothMasRequestGetMessagesListingSize) request).getSize();
+                        client.sendToClient(EVENT_GET_MESSAGES_LISTING_SIZE, request.isSuccess(),
+                                size);
+                    }
+                    break;
+
+                case BluetoothMnsService.EVENT_REPORT:
+                    /* pass event report directly to app */
+                    client.sendToClient(EVENT_EVENT_REPORT, true, msg.obj);
+                    break;
+            }
+        }
+    }
+
+    private void sendToClient(int event, boolean success) {
+        sendToClient(event, success, null);
+    }
+
+    private void sendToClient(int event, boolean success, int param) {
+        sendToClient(event, success, Integer.valueOf(param));
+    }
+
+    private void sendToClient(int event, boolean success, Object param) {
+        if (success) {
+            mCallback.obtainMessage(event, STATUS_OK, mMas.getId(), param).sendToTarget();
+        } else {
+            mCallback.obtainMessage(event, STATUS_FAILED, mMas.getId(), null).sendToTarget();
+        }
+    }
+
+    private class SocketConnectThread extends Thread {
+        private BluetoothSocket socket = null;
+
+        public SocketConnectThread() {
+            super("SocketConnectThread");
+        }
+
+        @Override
+        public void run() {
+            try {
+                socket = mDevice.createRfcommSocket(mMas.getChannel());
+                socket.connect();
+
+                BluetoothMapRfcommTransport transport;
+                transport = new BluetoothMapRfcommTransport(socket);
+
+                mSessionHandler.obtainMessage(SOCKET_CONNECTED, transport).sendToTarget();
+            } catch (IOException e) {
+                Log.e(TAG, "Error when creating/connecting socket", e);
+
+                closeSocket();
+                mSessionHandler.obtainMessage(SOCKET_ERROR).sendToTarget();
+            }
+        }
+
+        @Override
+        public void interrupt() {
+            closeSocket();
+        }
+
+        private void closeSocket() {
+            try {
+                if (socket != null) {
+                    socket.close();
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Error when closing socket", e);
+            }
+        }
+    }
+
+    /**
+     * Object representation of filters to be applied on message listing
+     *
+     * @see #getMessagesListing(String, int, MessagesFilter, int)
+     * @see #getMessagesListing(String, int, MessagesFilter, int, int, int)
+     */
+    public static final class MessagesFilter {
+
+        public final static byte MESSAGE_TYPE_ALL = 0x00;
+        public final static byte MESSAGE_TYPE_SMS_GSM = 0x01;
+        public final static byte MESSAGE_TYPE_SMS_CDMA = 0x02;
+        public final static byte MESSAGE_TYPE_EMAIL = 0x04;
+        public final static byte MESSAGE_TYPE_MMS = 0x08;
+
+        public final static byte READ_STATUS_ANY = 0x00;
+        public final static byte READ_STATUS_UNREAD = 0x01;
+        public final static byte READ_STATUS_READ = 0x02;
+
+        public final static byte PRIORITY_ANY = 0x00;
+        public final static byte PRIORITY_HIGH = 0x01;
+        public final static byte PRIORITY_NON_HIGH = 0x02;
+
+        byte messageType = MESSAGE_TYPE_ALL;
+
+        String periodBegin = null;
+
+        String periodEnd = null;
+
+        byte readStatus = READ_STATUS_ANY;
+
+        String recipient = null;
+
+        String originator = null;
+
+        byte priority = PRIORITY_ANY;
+
+        public MessagesFilter() {
+        }
+
+        public void setMessageType(byte filter) {
+            messageType = filter;
+        }
+
+        public void setPeriod(Date filterBegin, Date filterEnd) {
+            periodBegin = (new ObexTime(filterBegin)).toString();
+            periodEnd = (new ObexTime(filterEnd)).toString();
+        }
+
+        public void setReadStatus(byte readfilter) {
+            readStatus = readfilter;
+        }
+
+        public void setRecipient(String filter) {
+            if ("".equals(filter)) {
+                recipient = null;
+            } else {
+                recipient = filter;
+            }
+        }
+
+        public void setOriginator(String filter) {
+            if ("".equals(filter)) {
+                originator = null;
+            } else {
+                originator = filter;
+            }
+        }
+
+        public void setPriority(byte filter) {
+            priority = filter;
+        }
+    }
+
+    /**
+     * Constructs client object to communicate with single MAS instance on MSE
+     *
+     * @param device {@link BluetoothDevice} corresponding to remote device
+     *            acting as MSE
+     * @param mas {@link BluetoothMasInstance} object describing MAS instance on
+     *            remote device
+     * @param callback {@link Handler} object to which callback messages will be
+     *            sent Each message will have <code>arg1</code> set to either
+     *            {@link #STATUS_OK} or {@link #STATUS_FAILED} and
+     *            <code>arg2</code> to MAS instance ID. <code>obj</code> in
+     *            message is event specific.
+     */
+    public BluetoothMasClient(BluetoothDevice device, BluetoothMasInstance mas,
+            Handler callback) {
+        mDevice = device;
+        mMas = mas;
+        mCallback = callback;
+
+        mPath = new ArrayDeque<String>();
+    }
+
+    /**
+     * Retrieves MAS instance data associated with client
+     *
+     * @return instance data object
+     */
+    public BluetoothMasInstance getInstanceData() {
+        return mMas;
+    }
+
+    /**
+     * Connects to MAS instance
+     * <p>
+     * Upon completion callback handler will receive {@link #EVENT_CONNECT}
+     */
+    public void connect() {
+        if (mSessionHandler == null) {
+            mSessionHandler = new SessionHandler(this);
+        }
+
+        if (mConnectThread == null && mObexSession == null) {
+            mConnectionState = ConnectionState.CONNECTING;
+
+            mConnectThread = new SocketConnectThread();
+            mConnectThread.start();
+        }
+    }
+
+    /**
+     * Disconnects from MAS instance
+     * <p>
+     * Upon completion callback handler will receive {@link #EVENT_CONNECT}
+     */
+    public void disconnect() {
+        if (mConnectThread == null && mObexSession == null) {
+            return;
+        }
+
+        mConnectionState = ConnectionState.DISCONNECTING;
+
+        if (mConnectThread != null) {
+            mConnectThread.interrupt();
+        }
+
+        if (mObexSession != null) {
+            mObexSession.stop();
+        }
+    }
+
+    @Override
+    public void finalize() {
+        disconnect();
+    }
+
+    /**
+     * Gets current connection state
+     *
+     * @return current connection state
+     * @see ConnectionState
+     */
+    public ConnectionState getState() {
+        return mConnectionState;
+    }
+
+    private boolean enableNotifications() {
+        Log.v(TAG, "enableNotifications()");
+
+        if (mMnsService == null) {
+            mMnsService = new BluetoothMnsService();
+        }
+
+        mMnsService.registerCallback(mMas.getId(), mSessionHandler);
+
+        BluetoothMasRequest request = new BluetoothMasRequestSetNotificationRegistration(true);
+        return mObexSession.makeRequest(request);
+    }
+
+    private boolean disableNotifications() {
+        Log.v(TAG, "enableNotifications()");
+
+        if (mMnsService != null) {
+            mMnsService.unregisterCallback(mMas.getId());
+        }
+
+        mMnsService = null;
+
+        BluetoothMasRequest request = new BluetoothMasRequestSetNotificationRegistration(false);
+        return mObexSession.makeRequest(request);
+    }
+
+    /**
+     * Sets state of notifications for MAS instance
+     * <p>
+     * Once notifications are enabled, callback handler will receive
+     * {@link #EVENT_EVENT_REPORT} when new notification is received
+     * <p>
+     * Upon completion callback handler will receive
+     * {@link #EVENT_SET_NOTIFICATION_REGISTRATION}
+     *
+     * @param status <code>true</code> if notifications shall be enabled,
+     *            <code>false</code> otherwise
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     */
+    public boolean setNotificationRegistration(boolean status) {
+        if (mObexSession == null) {
+            return false;
+        }
+
+        if (status) {
+            return enableNotifications();
+        } else {
+            return disableNotifications();
+        }
+    }
+
+    /**
+     * Gets current state of notifications for MAS instance
+     *
+     * @return <code>true</code> if notifications are enabled,
+     *         <code>false</code> otherwise
+     */
+    public boolean getNotificationRegistration() {
+        return mNotificationEnabled;
+    }
+
+    /**
+     * Goes back to root of folder hierarchy
+     * <p>
+     * Upon completion callback handler will receive {@link #EVENT_SET_PATH}
+     *
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     */
+    public boolean setFolderRoot() {
+        if (mObexSession == null) {
+            return false;
+        }
+
+        BluetoothMasRequest request = new BluetoothMasRequestSetPath(true);
+        return mObexSession.makeRequest(request);
+    }
+
+    /**
+     * Goes back to parent folder in folder hierarchy
+     * <p>
+     * Upon completion callback handler will receive {@link #EVENT_SET_PATH}
+     *
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     */
+    public boolean setFolderUp() {
+        if (mObexSession == null) {
+            return false;
+        }
+
+        BluetoothMasRequest request = new BluetoothMasRequestSetPath(false);
+        return mObexSession.makeRequest(request);
+    }
+
+    /**
+     * Goes down to specified sub-folder in folder hierarchy
+     * <p>
+     * Upon completion callback handler will receive {@link #EVENT_SET_PATH}
+     *
+     * @param name name of sub-folder
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     */
+    public boolean setFolderDown(String name) {
+        if (mObexSession == null) {
+            return false;
+        }
+
+        if (name == null || name.isEmpty() || name.contains("/")) {
+            return false;
+        }
+
+        BluetoothMasRequest request = new BluetoothMasRequestSetPath(name);
+        return mObexSession.makeRequest(request);
+    }
+
+    /**
+     * Gets current path in folder hierarchy
+     *
+     * @return current path
+     */
+    public String getCurrentPath() {
+        if (mPath.size() == 0) {
+            return "";
+        }
+
+        Iterator<String> iter = mPath.iterator();
+
+        StringBuilder sb = new StringBuilder(iter.next());
+
+        while (iter.hasNext()) {
+            sb.append("/").append(iter.next());
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Gets list of sub-folders in current folder
+     * <p>
+     * Upon completion callback handler will receive
+     * {@link #EVENT_GET_FOLDER_LISTING}
+     *
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     */
+    public boolean getFolderListing() {
+        return getFolderListing((short) 0, (short) 0);
+    }
+
+    /**
+     * Gets list of sub-folders in current folder
+     * <p>
+     * Upon completion callback handler will receive
+     * {@link #EVENT_GET_FOLDER_LISTING}
+     *
+     * @param maxListCount maximum number of items returned or <code>0</code>
+     *            for default value
+     * @param listStartOffset index of first item returned or <code>0</code> for
+     *            default value
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     * @throws IllegalArgumentException if either maxListCount or
+     *             listStartOffset are outside allowed range [0..65535]
+     */
+    public boolean getFolderListing(int maxListCount, int listStartOffset) {
+        if (mObexSession == null) {
+            return false;
+        }
+
+        BluetoothMasRequest request = new BluetoothMasRequestGetFolderListing(maxListCount,
+                listStartOffset);
+        return mObexSession.makeRequest(request);
+    }
+
+    /**
+     * Gets number of sub-folders in current folder
+     * <p>
+     * Upon completion callback handler will receive
+     * {@link #EVENT_GET_FOLDER_LISTING_SIZE}
+     *
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     */
+    public boolean getFolderListingSize() {
+        if (mObexSession == null) {
+            return false;
+        }
+
+        BluetoothMasRequest request = new BluetoothMasRequestGetFolderListingSize();
+        return mObexSession.makeRequest(request);
+    }
+
+    /**
+     * Gets list of messages in specified sub-folder
+     * <p>
+     * Upon completion callback handler will receive
+     * {@link #EVENT_GET_MESSAGES_LISTING}
+     *
+     * @param folder name of sub-folder or <code>null</code> for current folder
+     * @param parameters bit-mask specifying requested parameters in listing or
+     *            <code>0</code> for default value
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     */
+    public boolean getMessagesListing(String folder, int parameters) {
+        return getMessagesListing(folder, parameters, null, (byte) 0, 0, 0);
+    }
+
+    /**
+     * Gets list of messages in specified sub-folder
+     * <p>
+     * Upon completion callback handler will receive
+     * {@link #EVENT_GET_MESSAGES_LISTING}
+     *
+     * @param folder name of sub-folder or <code>null</code> for current folder
+     * @param parameters corresponds to <code>ParameterMask</code> application
+     *            parameter in MAP specification
+     * @param filter {@link MessagesFilter} object describing filters to be
+     *            applied on listing by MSE
+     * @param subjectLength maximum length of message subject in returned
+     *            listing or <code>0</code> for default value
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     * @throws IllegalArgumentException if subjectLength is outside allowed
+     *             range [0..255]
+     */
+    public boolean getMessagesListing(String folder, int parameters, MessagesFilter filter,
+            int subjectLength) {
+
+        return getMessagesListing(folder, parameters, filter, subjectLength, 0, 0);
+    }
+
+    /**
+     * Gets list of messages in specified sub-folder
+     * <p>
+     * Upon completion callback handler will receive
+     * {@link #EVENT_GET_MESSAGES_LISTING}
+     *
+     * @param folder name of sub-folder or <code>null</code> for current folder
+     * @param parameters corresponds to <code>ParameterMask</code> application
+     *            parameter in MAP specification
+     * @param filter {@link MessagesFilter} object describing filters to be
+     *            applied on listing by MSE
+     * @param subjectLength maximum length of message subject in returned
+     *            listing or <code>0</code> for default value
+     * @param maxListCount maximum number of items returned or <code>0</code>
+     *            for default value
+     * @param listStartOffset index of first item returned or <code>0</code> for
+     *            default value
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     * @throws IllegalArgumentException if subjectLength is outside allowed
+     *             range [0..255] or either maxListCount or listStartOffset are
+     *             outside allowed range [0..65535]
+     */
+    public boolean getMessagesListing(String folder, int parameters, MessagesFilter filter,
+            int subjectLength, int maxListCount, int listStartOffset) {
+
+        if (mObexSession == null) {
+            return false;
+        }
+
+        BluetoothMasRequest request = new BluetoothMasRequestGetMessagesListing(folder,
+                parameters, filter, subjectLength, maxListCount, listStartOffset);
+        return mObexSession.makeRequest(request);
+    }
+
+    /**
+     * Gets number of messages in current folder
+     * <p>
+     * Upon completion callback handler will receive
+     * {@link #EVENT_GET_MESSAGES_LISTING_SIZE}
+     *
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     */
+    public boolean getMessagesListingSize() {
+        if (mObexSession == null) {
+            return false;
+        }
+
+        BluetoothMasRequest request = new BluetoothMasRequestGetMessagesListingSize();
+        return mObexSession.makeRequest(request);
+    }
+
+    /**
+     * Retrieves message from MSE
+     * <p>
+     * Upon completion callback handler will receive {@link #EVENT_GET_MESSAGE}
+     *
+     * @param handle handle of message to retrieve
+     * @param charset {@link CharsetType} object corresponding to
+     *            <code>Charset</code> application parameter in MAP
+     *            specification
+     * @param attachment corresponds to <code>Attachment</code> application
+     *            parameter in MAP specification
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     */
+    public boolean getMessage(String handle, CharsetType charset, boolean attachment) {
+        if (mObexSession == null) {
+            return false;
+        }
+
+        try {
+            /* just to validate */
+            new BigInteger(handle, 16);
+        } catch (NumberFormatException e) {
+            return false;
+        }
+
+        BluetoothMasRequest request = new BluetoothMasRequestGetMessage(handle, charset,
+                attachment);
+        return mObexSession.makeRequest(request);
+    }
+
+    /**
+     * Sets read status of message on MSE
+     * <p>
+     * Upon completion callback handler will receive
+     * {@link #EVENT_SET_MESSAGE_STATUS}
+     *
+     * @param handle handle of message
+     * @param read <code>true</code> for "read", <code>false</code> for "unread"
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     */
+    public boolean setMessageReadStatus(String handle, boolean read) {
+        if (mObexSession == null) {
+            return false;
+        }
+
+        try {
+            /* just to validate */
+            new BigInteger(handle, 16);
+        } catch (NumberFormatException e) {
+            return false;
+        }
+
+        BluetoothMasRequest request = new BluetoothMasRequestSetMessageStatus(handle,
+                StatusIndicator.READ, read);
+        return mObexSession.makeRequest(request);
+    }
+
+    /**
+     * Sets deleted status of message on MSE
+     * <p>
+     * Upon completion callback handler will receive
+     * {@link #EVENT_SET_MESSAGE_STATUS}
+     *
+     * @param handle handle of message
+     * @param deleted <code>true</code> for "deleted", <code>false</code> for
+     *            "undeleted"
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     */
+    public boolean setMessageDeletedStatus(String handle, boolean deleted) {
+        if (mObexSession == null) {
+            return false;
+        }
+
+        try {
+            /* just to validate */
+            new BigInteger(handle, 16);
+        } catch (NumberFormatException e) {
+            return false;
+        }
+
+        BluetoothMasRequest request = new BluetoothMasRequestSetMessageStatus(handle,
+                StatusIndicator.DELETED, deleted);
+        return mObexSession.makeRequest(request);
+    }
+
+    /**
+     * Pushes new message to MSE
+     * <p>
+     * Upon completion callback handler will receive {@link #EVENT_PUSH_MESSAGE}
+     *
+     * @param folder name of sub-folder to push to or <code>null</code> for
+     *            current folder
+     * @param charset {@link CharsetType} object corresponding to
+     *            <code>Charset</code> application parameter in MAP
+     *            specification
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     */
+    public boolean pushMessage(String folder, BluetoothMapBmessage bmsg, CharsetType charset) {
+        return pushMessage(folder, bmsg, charset, false, false);
+    }
+
+    /**
+     * Pushes new message to MSE
+     * <p>
+     * Upon completion callback handler will receive {@link #EVENT_PUSH_MESSAGE}
+     *
+     * @param folder name of sub-folder to push to or <code>null</code> for
+     *            current folder
+     * @param bmsg {@link BluetoothMapBmessage} object representing message to
+     *            be pushed
+     * @param charset {@link CharsetType} object corresponding to
+     *            <code>Charset</code> application parameter in MAP
+     *            specification
+     * @param transparent corresponds to <code>Transparent</code> application
+     *            parameter in MAP specification
+     * @param retry corresponds to <code>Transparent</code> application
+     *            parameter in MAP specification
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     */
+    public boolean pushMessage(String folder, BluetoothMapBmessage bmsg, CharsetType charset,
+            boolean transparent, boolean retry) {
+        if (mObexSession == null) {
+            return false;
+        }
+
+        String bmsgString = BluetoothMapBmessageBuilder.createBmessage(bmsg);
+
+        BluetoothMasRequest request =
+                new BluetoothMasRequestPushMessage(folder, bmsgString, charset, transparent, retry);
+        return mObexSession.makeRequest(request);
+    }
+
+    /**
+     * Requests MSE to initiate ubdate of inbox
+     * <p>
+     * Upon completion callback handler will receive {@link #EVENT_UPDATE_INBOX}
+     *
+     * @return <code>true</code> if request has been sent, <code>false</code>
+     *         otherwise
+     */
+    public boolean updateInbox() {
+        if (mObexSession == null) {
+            return false;
+        }
+
+        BluetoothMasRequest request = new BluetoothMasRequestUpdateInbox();
+        return mObexSession.makeRequest(request);
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMasObexClientSession.java b/src/android/bluetooth/client/map/BluetoothMasObexClientSession.java
new file mode 100644
index 0000000..f949b8d
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMasObexClientSession.java
@@ -0,0 +1,192 @@
+/*
+ * 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.os.Handler;
+import android.os.Process;
+import android.util.Log;
+
+import java.io.IOException;
+
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+import javax.obex.ObexTransport;
+import javax.obex.ResponseCodes;
+
+class BluetoothMasObexClientSession {
+    private static final String TAG = "BluetoothMasObexClientSession";
+
+    private static final byte[] MAS_TARGET = new byte[] {
+            (byte) 0xbb, 0x58, 0x2b, 0x40, 0x42, 0x0c, 0x11, (byte) 0xdb, (byte) 0xb0, (byte) 0xde,
+            0x08, 0x00, 0x20, 0x0c, (byte) 0x9a, 0x66
+    };
+
+    static final int MSG_OBEX_CONNECTED = 100;
+    static final int MSG_OBEX_DISCONNECTED = 101;
+    static final int MSG_REQUEST_COMPLETED = 102;
+
+    private final ObexTransport mTransport;
+
+    private final Handler mSessionHandler;
+
+    private ClientThread mClientThread;
+
+    private volatile boolean mInterrupted;
+
+    private class ClientThread extends Thread {
+        private final ObexTransport mTransport;
+
+        private ClientSession mSession;
+
+        private BluetoothMasRequest mRequest;
+
+        private boolean mConnected;
+
+        public ClientThread(ObexTransport transport) {
+            super("MAS ClientThread");
+
+            mTransport = transport;
+            mConnected = false;
+        }
+
+        @Override
+        public void run() {
+            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+            connect();
+
+            if (mConnected) {
+                mSessionHandler.obtainMessage(MSG_OBEX_CONNECTED).sendToTarget();
+            } else {
+                mSessionHandler.obtainMessage(MSG_OBEX_DISCONNECTED).sendToTarget();
+                return;
+            }
+
+            while (!mInterrupted) {
+                synchronized (this) {
+                    if (mRequest == null) {
+                        try {
+                            this.wait();
+                        } catch (InterruptedException e) {
+                            mInterrupted = true;
+                        }
+                    }
+                }
+
+                if (!mInterrupted && mRequest != null) {
+                    try {
+                        mRequest.execute(mSession);
+                    } catch (IOException e) {
+                        // this will "disconnect" to cleanup
+                        mInterrupted = true;
+                    }
+
+                    BluetoothMasRequest oldReq = mRequest;
+                    mRequest = null;
+
+                    mSessionHandler.obtainMessage(MSG_REQUEST_COMPLETED, oldReq).sendToTarget();
+                }
+            }
+
+            disconnect();
+
+            mSessionHandler.obtainMessage(MSG_OBEX_DISCONNECTED).sendToTarget();
+        }
+
+        private void connect() {
+            try {
+                mSession = new ClientSession(mTransport);
+
+                HeaderSet headerset = new HeaderSet();
+                headerset.setHeader(HeaderSet.TARGET, MAS_TARGET);
+
+                headerset = mSession.connect(headerset);
+
+                if (headerset.getResponseCode() == ResponseCodes.OBEX_HTTP_OK) {
+                    mConnected = true;
+                } else {
+                    disconnect();
+                }
+            } catch (IOException e) {
+            }
+        }
+
+        private void disconnect() {
+            try {
+                mSession.disconnect(null);
+            } catch (IOException e) {
+            }
+
+            try {
+                mSession.close();
+            } catch (IOException e) {
+            }
+
+            mConnected = false;
+        }
+
+        public synchronized boolean schedule(BluetoothMasRequest request) {
+            if (mRequest != null) {
+                return false;
+            }
+
+            mRequest = request;
+            notify();
+
+            return true;
+        }
+    }
+
+    public BluetoothMasObexClientSession(ObexTransport transport, Handler handler) {
+        mTransport = transport;
+        mSessionHandler = handler;
+    }
+
+    public void start() {
+        if (mClientThread == null) {
+            mClientThread = new ClientThread(mTransport);
+            mClientThread.start();
+        }
+
+    }
+
+    public void stop() {
+        if (mClientThread != null) {
+            mClientThread.interrupt();
+
+            (new Thread() {
+                @Override
+                public void run() {
+                    try {
+                        mClientThread.join();
+                        mClientThread = null;
+                    } catch (InterruptedException e) {
+                        Log.w(TAG, "Interrupted while waiting for thread to join");
+                    }
+                }
+            }).run();
+        }
+    }
+
+    public boolean makeRequest(BluetoothMasRequest request) {
+        if (mClientThread == null) {
+            return false;
+        }
+
+        return mClientThread.schedule(request);
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMasRequest.java b/src/android/bluetooth/client/map/BluetoothMasRequest.java
new file mode 100644
index 0000000..658a344
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMasRequest.java
@@ -0,0 +1,161 @@
+/*
+ * 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 java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.obex.ClientOperation;
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+import javax.obex.Operation;
+import javax.obex.ResponseCodes;
+
+abstract class BluetoothMasRequest {
+
+    protected static final byte OAP_TAGID_MAX_LIST_COUNT = 0x01;
+    protected static final byte OAP_TAGID_START_OFFSET = 0x02;
+    protected static final byte OAP_TAGID_FILTER_MESSAGE_TYPE = 0x03;
+    protected static final byte OAP_TAGID_FILTER_PERIOD_BEGIN = 0x04;
+    protected static final byte OAP_TAGID_FILTER_PERIOD_END = 0x05;
+    protected static final byte OAP_TAGID_FILTER_READ_STATUS = 0x06;
+    protected static final byte OAP_TAGID_FILTER_RECIPIENT = 0x07;
+    protected static final byte OAP_TAGID_FILTER_ORIGINATOR = 0x08;
+    protected static final byte OAP_TAGID_FILTER_PRIORITY = 0x09;
+    protected static final byte OAP_TAGID_ATTACHMENT = 0x0a;
+    protected static final byte OAP_TAGID_TRANSPARENT = 0xb;
+    protected static final byte OAP_TAGID_RETRY = 0xc;
+    protected static final byte OAP_TAGID_NEW_MESSAGE = 0x0d;
+    protected static final byte OAP_TAGID_NOTIFICATION_STATUS = 0x0e;
+    protected static final byte OAP_TAGID_MAS_INSTANCE_ID = 0x0f;
+    protected static final byte OAP_TAGID_FOLDER_LISTING_SIZE = 0x11;
+    protected static final byte OAP_TAGID_MESSAGES_LISTING_SIZE = 0x12;
+    protected static final byte OAP_TAGID_SUBJECT_LENGTH = 0x13;
+    protected static final byte OAP_TAGID_CHARSET = 0x14;
+    protected static final byte OAP_TAGID_STATUS_INDICATOR = 0x17;
+    protected static final byte OAP_TAGID_STATUS_VALUE = 0x18;
+    protected static final byte OAP_TAGID_MSE_TIME = 0x19;
+
+    protected static byte NOTIFICATION_ON = 0x01;
+    protected static byte NOTIFICATION_OFF = 0x00;
+
+    protected static byte ATTACHMENT_ON = 0x01;
+    protected static byte ATTACHMENT_OFF = 0x00;
+
+    protected static byte CHARSET_NATIVE = 0x00;
+    protected static byte CHARSET_UTF8 = 0x01;
+
+    protected static byte STATUS_INDICATOR_READ = 0x00;
+    protected static byte STATUS_INDICATOR_DELETED = 0x01;
+
+    protected static byte STATUS_NO = 0x00;
+    protected static byte STATUS_YES = 0x01;
+
+    protected static byte TRANSPARENT_OFF = 0x00;
+    protected static byte TRANSPARENT_ON = 0x01;
+
+    protected static byte RETRY_OFF = 0x00;
+    protected static byte RETRY_ON = 0x01;
+
+    /* used for PUT requests which require filler byte */
+    protected static final byte[] FILLER_BYTE = {
+        0x30
+    };
+
+    protected HeaderSet mHeaderSet;
+
+    protected int mResponseCode;
+
+    public BluetoothMasRequest() {
+        mHeaderSet = new HeaderSet();
+    }
+
+    abstract public void execute(ClientSession session) throws IOException;
+
+    protected void executeGet(ClientSession session) throws IOException {
+        ClientOperation op = null;
+
+        try {
+            op = (ClientOperation) session.get(mHeaderSet);
+
+            /*
+             * MAP spec does not explicitly require that GET request should be
+             * sent in single packet but for some reason PTS complains when
+             * final GET packet with no headers follows non-final GET with all
+             * headers. So this is workaround, at least temporary. TODO: check
+             * with PTS
+             */
+            op.setGetFinalFlag(true);
+
+            /*
+             * this will trigger ClientOperation to use non-buffered stream so
+             * we can abort operation
+             */
+            op.continueOperation(true, false);
+
+            readResponseHeaders(op.getReceivedHeader());
+
+            InputStream is = op.openInputStream();
+            readResponse(is);
+            is.close();
+
+            op.close();
+
+            mResponseCode = op.getResponseCode();
+        } catch (IOException e) {
+            mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+
+            throw e;
+        }
+    }
+
+    protected void executePut(ClientSession session, byte[] body) throws IOException {
+        Operation op = null;
+
+        mHeaderSet.setHeader(HeaderSet.LENGTH, Long.valueOf(body.length));
+
+        try {
+            op = session.put(mHeaderSet);
+
+            DataOutputStream out = op.openDataOutputStream();
+            out.write(body);
+            out.close();
+
+            readResponseHeaders(op.getReceivedHeader());
+
+            op.close();
+            mResponseCode = op.getResponseCode();
+        } catch (IOException e) {
+            mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+
+            throw e;
+        }
+    }
+
+    final public boolean isSuccess() {
+        return (mResponseCode == ResponseCodes.OBEX_HTTP_OK);
+    }
+
+    protected void readResponse(InputStream stream) throws IOException {
+        /* nothing here by default */
+    }
+
+    protected void readResponseHeaders(HeaderSet headerset) {
+        /* nothing here by default */
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMasRequestGetFolderListing.java b/src/android/bluetooth/client/map/BluetoothMasRequestGetFolderListing.java
new file mode 100644
index 0000000..bd5a2dd
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMasRequestGetFolderListing.java
@@ -0,0 +1,75 @@
+/*
+ * 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.bluetooth.client.map.utils.ObexAppParameters;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+
+final class BluetoothMasRequestGetFolderListing extends BluetoothMasRequest {
+
+    private static final String TYPE = "x-obex/folder-listing";
+
+    private BluetoothMapFolderListing mResponse = null;
+
+    public BluetoothMasRequestGetFolderListing(int maxListCount, int listStartOffset) {
+
+        if (maxListCount < 0 || maxListCount > 65535) {
+            throw new IllegalArgumentException("maxListCount should be [0..65535]");
+        }
+
+        if (listStartOffset < 0 || listStartOffset > 65535) {
+            throw new IllegalArgumentException("listStartOffset should be [0..65535]");
+        }
+
+        mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
+
+        ObexAppParameters oap = new ObexAppParameters();
+
+        if (maxListCount > 0) {
+            oap.add(OAP_TAGID_MAX_LIST_COUNT, (short) maxListCount);
+        }
+
+        if (listStartOffset > 0) {
+            oap.add(OAP_TAGID_START_OFFSET, (short) listStartOffset);
+        }
+
+        oap.addToHeaderSet(mHeaderSet);
+    }
+
+    @Override
+    protected void readResponse(InputStream stream) {
+        mResponse = new BluetoothMapFolderListing(stream);
+    }
+
+    public ArrayList<String> getList() {
+        if (mResponse == null) {
+            return null;
+        }
+
+        return mResponse.getList();
+    }
+
+    @Override
+    public void execute(ClientSession session) throws IOException {
+        executeGet(session);
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMasRequestGetFolderListingSize.java b/src/android/bluetooth/client/map/BluetoothMasRequestGetFolderListingSize.java
new file mode 100644
index 0000000..910c036
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMasRequestGetFolderListingSize.java
@@ -0,0 +1,56 @@
+/*
+ * 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.bluetooth.client.map.utils.ObexAppParameters;
+
+import java.io.IOException;
+
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+
+final class BluetoothMasRequestGetFolderListingSize extends BluetoothMasRequest {
+
+    private static final String TYPE = "x-obex/folder-listing";
+
+    private int mSize;
+
+    public BluetoothMasRequestGetFolderListingSize() {
+        mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
+
+        ObexAppParameters oap = new ObexAppParameters();
+        oap.add(OAP_TAGID_MAX_LIST_COUNT, 0);
+
+        oap.addToHeaderSet(mHeaderSet);
+    }
+
+    @Override
+    protected void readResponseHeaders(HeaderSet headerset) {
+        ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
+
+        mSize = oap.getShort(OAP_TAGID_FOLDER_LISTING_SIZE);
+    }
+
+    public int getSize() {
+        return mSize;
+    }
+
+    @Override
+    public void execute(ClientSession session) throws IOException {
+        executeGet(session);
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMasRequestGetMessage.java b/src/android/bluetooth/client/map/BluetoothMasRequestGetMessage.java
new file mode 100644
index 0000000..b50fd0f
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMasRequestGetMessage.java
@@ -0,0 +1,89 @@
+/*
+ * 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 android.bluetooth.client.map.BluetoothMasClient.CharsetType;
+import android.bluetooth.client.map.utils.ObexAppParameters;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+import javax.obex.ResponseCodes;
+
+final class BluetoothMasRequestGetMessage extends BluetoothMasRequest {
+
+    private static final String TAG = "BluetoothMasRequestGetMessage";
+
+    private static final String TYPE = "x-bt/message";
+
+    private BluetoothMapBmessage mBmessage;
+
+    public BluetoothMasRequestGetMessage(String handle, CharsetType charset, boolean attachment) {
+
+        mHeaderSet.setHeader(HeaderSet.NAME, handle);
+
+        mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
+
+        ObexAppParameters oap = new ObexAppParameters();
+
+        oap.add(OAP_TAGID_CHARSET, CharsetType.UTF_8.equals(charset) ? CHARSET_UTF8
+                : CHARSET_NATIVE);
+
+        oap.add(OAP_TAGID_ATTACHMENT, attachment ? ATTACHMENT_ON : ATTACHMENT_OFF);
+
+        oap.addToHeaderSet(mHeaderSet);
+    }
+
+    @Override
+    protected void readResponse(InputStream stream) {
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        byte[] buf = new byte[1024];
+
+        try {
+            int len;
+            while ((len = stream.read(buf)) != -1) {
+                baos.write(buf, 0, len);
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "I/O exception while reading response", e);
+        }
+
+        String bmsg = baos.toString();
+
+        mBmessage = BluetoothMapBmessageParser.createBmessage(bmsg);
+
+        if (mBmessage == null) {
+            mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+        }
+    }
+
+    public BluetoothMapBmessage getMessage() {
+        return mBmessage;
+    }
+
+    @Override
+    public void execute(ClientSession session) throws IOException {
+        executeGet(session);
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListing.java b/src/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListing.java
new file mode 100644
index 0000000..d5460f9
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListing.java
@@ -0,0 +1,150 @@
+/*
+ * 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.bluetooth.client.map.BluetoothMasClient.MessagesFilter;
+import android.bluetooth.client.map.utils.ObexAppParameters;
+import android.bluetooth.client.map.utils.ObexTime;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Date;
+
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+
+final class BluetoothMasRequestGetMessagesListing extends BluetoothMasRequest {
+
+    private static final String TYPE = "x-bt/MAP-msg-listing";
+
+    private BluetoothMapMessagesListing mResponse = null;
+
+    private boolean mNewMessage = false;
+
+    private Date mServerTime = null;
+
+    public BluetoothMasRequestGetMessagesListing(String folderName, int parameters,
+            BluetoothMasClient.MessagesFilter filter, int subjectLength, int maxListCount,
+            int listStartOffset) {
+
+        if (subjectLength < 0 || subjectLength > 255) {
+            throw new IllegalArgumentException("subjectLength should be [0..255]");
+        }
+
+        if (maxListCount < 0 || maxListCount > 65535) {
+            throw new IllegalArgumentException("maxListCount should be [0..65535]");
+        }
+
+        if (listStartOffset < 0 || listStartOffset > 65535) {
+            throw new IllegalArgumentException("listStartOffset should be [0..65535]");
+        }
+
+        mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
+
+        if (folderName == null) {
+            mHeaderSet.setHeader(HeaderSet.NAME, "");
+        } else {
+            mHeaderSet.setHeader(HeaderSet.NAME, folderName);
+        }
+
+        ObexAppParameters oap = new ObexAppParameters();
+
+        if (filter != null) {
+            if (filter.messageType != MessagesFilter.MESSAGE_TYPE_ALL) {
+                oap.add(OAP_TAGID_FILTER_MESSAGE_TYPE, filter.messageType);
+            }
+
+            if (filter.periodBegin != null) {
+                oap.add(OAP_TAGID_FILTER_PERIOD_BEGIN, filter.periodBegin);
+            }
+
+            if (filter.periodEnd != null) {
+                oap.add(OAP_TAGID_FILTER_PERIOD_END, filter.periodEnd);
+            }
+
+            if (filter.readStatus != MessagesFilter.READ_STATUS_ANY) {
+                oap.add(OAP_TAGID_FILTER_READ_STATUS, filter.readStatus);
+            }
+
+            if (filter.recipient != null) {
+                oap.add(OAP_TAGID_FILTER_RECIPIENT, filter.recipient);
+            }
+
+            if (filter.originator != null) {
+                oap.add(OAP_TAGID_FILTER_ORIGINATOR, filter.originator);
+            }
+
+            if (filter.priority != MessagesFilter.PRIORITY_ANY) {
+                oap.add(OAP_TAGID_FILTER_PRIORITY, filter.priority);
+            }
+        }
+
+        if (subjectLength != 0) {
+            oap.add(OAP_TAGID_SUBJECT_LENGTH, (byte) subjectLength);
+        }
+
+        if (maxListCount != 0) {
+            oap.add(OAP_TAGID_MAX_LIST_COUNT, (short) maxListCount);
+        }
+
+        if (listStartOffset != 0) {
+            oap.add(OAP_TAGID_START_OFFSET, (short) listStartOffset);
+        }
+
+        oap.addToHeaderSet(mHeaderSet);
+    }
+
+    @Override
+    protected void readResponse(InputStream stream) {
+        mResponse = new BluetoothMapMessagesListing(stream);
+    }
+
+    @Override
+    protected void readResponseHeaders(HeaderSet headerset) {
+        ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
+
+        mNewMessage = ((oap.getByte(OAP_TAGID_NEW_MESSAGE) & 0x01) == 1);
+
+        if (oap.exists(OAP_TAGID_MSE_TIME)) {
+            String mseTime = oap.getString(OAP_TAGID_MSE_TIME);
+
+            mServerTime = (new ObexTime(mseTime)).getTime();
+        }
+    }
+
+    public ArrayList<BluetoothMapMessage> getList() {
+        if (mResponse == null) {
+            return null;
+        }
+
+        return mResponse.getList();
+    }
+
+    public boolean getNewMessageStatus() {
+        return mNewMessage;
+    }
+
+    public Date getMseTime() {
+        return mServerTime;
+    }
+
+    @Override
+    public void execute(ClientSession session) throws IOException {
+        executeGet(session);
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListingSize.java b/src/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListingSize.java
new file mode 100644
index 0000000..cdadb2e
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListingSize.java
@@ -0,0 +1,57 @@
+/*
+ * 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.bluetooth.client.map.utils.ObexAppParameters;
+
+import java.io.IOException;
+
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+
+final class BluetoothMasRequestGetMessagesListingSize extends BluetoothMasRequest {
+
+    private static final String TYPE = "x-bt/MAP-msg-listing";
+
+    private int mSize;
+
+    public BluetoothMasRequestGetMessagesListingSize() {
+        mHeaderSet.setHeader(HeaderSet.NAME, "");
+        mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
+
+        ObexAppParameters oap = new ObexAppParameters();
+        oap.add(OAP_TAGID_MAX_LIST_COUNT, (short) 0);
+
+        oap.addToHeaderSet(mHeaderSet);
+    }
+
+    @Override
+    protected void readResponseHeaders(HeaderSet headerset) {
+        ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
+
+        mSize = oap.getShort(OAP_TAGID_MESSAGES_LISTING_SIZE);
+    }
+
+    public int getSize() {
+        return mSize;
+    }
+
+    @Override
+    public void execute(ClientSession session) throws IOException {
+        executeGet(session);
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMasRequestPushMessage.java b/src/android/bluetooth/client/map/BluetoothMasRequestPushMessage.java
new file mode 100644
index 0000000..8fc9bd4
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMasRequestPushMessage.java
@@ -0,0 +1,79 @@
+/*
+ * 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 java.io.IOException;
+import java.math.BigInteger;
+
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+import javax.obex.ResponseCodes;
+
+import android.bluetooth.client.map.BluetoothMasClient.CharsetType;
+import android.bluetooth.client.map.utils.ObexAppParameters;
+
+final class BluetoothMasRequestPushMessage extends BluetoothMasRequest {
+
+    private static final String TYPE = "x-bt/message";
+    private String mMsg;
+    private String mMsgHandle;
+
+    private BluetoothMasRequestPushMessage(String folder) {
+        mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
+        if (folder == null) {
+            folder = "";
+        }
+        mHeaderSet.setHeader(HeaderSet.NAME, folder);
+    }
+
+    public BluetoothMasRequestPushMessage(String folder, String msg, CharsetType charset,
+            boolean transparent, boolean retry) {
+        this(folder);
+        mMsg = msg;
+        ObexAppParameters oap = new ObexAppParameters();
+        oap.add(OAP_TAGID_TRANSPARENT, transparent ? TRANSPARENT_ON : TRANSPARENT_OFF);
+        oap.add(OAP_TAGID_RETRY, retry ? RETRY_ON : RETRY_OFF);
+        oap.add(OAP_TAGID_CHARSET, charset == CharsetType.NATIVE ? CHARSET_NATIVE : CHARSET_UTF8);
+        oap.addToHeaderSet(mHeaderSet);
+    }
+
+    @Override
+    protected void readResponseHeaders(HeaderSet headerset) {
+        try {
+            String handle = (String) headerset.getHeader(HeaderSet.NAME);
+            if (handle != null) {
+                /* just to validate */
+                new BigInteger(handle, 16);
+
+                mMsgHandle = handle;
+            }
+        } catch (NumberFormatException e) {
+            mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+        } catch (IOException e) {
+            mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+        }
+    }
+
+    public String getMsgHandle() {
+        return mMsgHandle;
+    }
+
+    @Override
+    public void execute(ClientSession session) throws IOException {
+        executePut(session, mMsg.getBytes());
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMasRequestSetMessageStatus.java b/src/android/bluetooth/client/map/BluetoothMasRequestSetMessageStatus.java
new file mode 100644
index 0000000..140312e
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMasRequestSetMessageStatus.java
@@ -0,0 +1,52 @@
+/*
+ * 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.bluetooth.client.map.utils.ObexAppParameters;
+
+import java.io.IOException;
+
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+
+final class BluetoothMasRequestSetMessageStatus extends BluetoothMasRequest {
+
+    public enum StatusIndicator {
+        READ, DELETED;
+    }
+
+    private static final String TYPE = "x-bt/messageStatus";
+
+    public BluetoothMasRequestSetMessageStatus(String handle, StatusIndicator statusInd,
+            boolean statusValue) {
+
+        mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
+        mHeaderSet.setHeader(HeaderSet.NAME, handle);
+
+        ObexAppParameters oap = new ObexAppParameters();
+        oap.add(OAP_TAGID_STATUS_INDICATOR,
+                statusInd == StatusIndicator.READ ? STATUS_INDICATOR_READ
+                        : STATUS_INDICATOR_DELETED);
+        oap.add(OAP_TAGID_STATUS_VALUE, statusValue ? STATUS_YES : STATUS_NO);
+        oap.addToHeaderSet(mHeaderSet);
+    }
+
+    @Override
+    public void execute(ClientSession session) throws IOException {
+        executePut(session, FILLER_BYTE);
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMasRequestSetNotificationRegistration.java b/src/android/bluetooth/client/map/BluetoothMasRequestSetNotificationRegistration.java
new file mode 100644
index 0000000..debb508
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMasRequestSetNotificationRegistration.java
@@ -0,0 +1,52 @@
+/*
+ * 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.bluetooth.client.map.utils.ObexAppParameters;
+
+import java.io.IOException;
+
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+
+final class BluetoothMasRequestSetNotificationRegistration extends BluetoothMasRequest {
+
+    private static final String TYPE = "x-bt/MAP-NotificationRegistration";
+
+    private final boolean mStatus;
+
+    public BluetoothMasRequestSetNotificationRegistration(boolean status) {
+        mStatus = status;
+
+        mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
+
+        ObexAppParameters oap = new ObexAppParameters();
+
+        oap.add(OAP_TAGID_NOTIFICATION_STATUS, status ? NOTIFICATION_ON : NOTIFICATION_OFF);
+
+        oap.addToHeaderSet(mHeaderSet);
+    }
+
+    @Override
+    public void execute(ClientSession session) throws IOException {
+        executePut(session, FILLER_BYTE);
+    }
+
+    public boolean getStatus() {
+        return mStatus;
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMasRequestSetPath.java b/src/android/bluetooth/client/map/BluetoothMasRequestSetPath.java
new file mode 100644
index 0000000..71e2dbe
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMasRequestSetPath.java
@@ -0,0 +1,71 @@
+/*
+ * 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 java.io.IOException;
+
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+import javax.obex.ResponseCodes;
+
+class BluetoothMasRequestSetPath extends BluetoothMasRequest {
+
+    enum SetPathDir {
+        ROOT, UP, DOWN
+    };
+
+    SetPathDir mDir;
+
+    String mName;
+
+    public BluetoothMasRequestSetPath(String name) {
+        mDir = SetPathDir.DOWN;
+        mName = name;
+
+        mHeaderSet.setHeader(HeaderSet.NAME, name);
+    }
+
+    public BluetoothMasRequestSetPath(boolean goRoot) {
+        mHeaderSet.setEmptyNameHeader();
+        if (goRoot) {
+            mDir = SetPathDir.ROOT;
+        } else {
+            mDir = SetPathDir.UP;
+        }
+    }
+
+    @Override
+    public void execute(ClientSession session) {
+        HeaderSet hs = null;
+
+        try {
+            switch (mDir) {
+                case ROOT:
+                case DOWN:
+                    hs = session.setPath(mHeaderSet, false, false);
+                    break;
+                case UP:
+                    hs = session.setPath(mHeaderSet, true, false);
+                    break;
+            }
+
+            mResponseCode = hs.getResponseCode();
+        } catch (IOException e) {
+            mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+        }
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMasRequestUpdateInbox.java b/src/android/bluetooth/client/map/BluetoothMasRequestUpdateInbox.java
new file mode 100644
index 0000000..aeec632
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMasRequestUpdateInbox.java
@@ -0,0 +1,36 @@
+/*
+ * 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 java.io.IOException;
+
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+
+final class BluetoothMasRequestUpdateInbox extends BluetoothMasRequest {
+
+    private static final String TYPE = "x-bt/MAP-messageUpdate";
+
+    public BluetoothMasRequestUpdateInbox() {
+        mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
+    }
+
+    @Override
+    public void execute(ClientSession session) throws IOException {
+        executePut(session, FILLER_BYTE);
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMnsObexServer.java b/src/android/bluetooth/client/map/BluetoothMnsObexServer.java
new file mode 100644
index 0000000..672e9cf
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMnsObexServer.java
@@ -0,0 +1,136 @@
+/*
+ * 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.os.Handler;
+import android.util.Log;
+
+import android.bluetooth.client.map.utils.ObexAppParameters;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import javax.obex.HeaderSet;
+import javax.obex.Operation;
+import javax.obex.ResponseCodes;
+import javax.obex.ServerRequestHandler;
+
+class BluetoothMnsObexServer extends ServerRequestHandler {
+
+    private final static String TAG = "BluetoothMnsObexServer";
+
+    private static final byte[] MNS_TARGET = new byte[] {
+            (byte) 0xbb, 0x58, 0x2b, 0x41, 0x42, 0x0c, 0x11, (byte) 0xdb, (byte) 0xb0, (byte) 0xde,
+            0x08, 0x00, 0x20, 0x0c, (byte) 0x9a, 0x66
+    };
+
+    private final static String TYPE = "x-bt/MAP-event-report";
+
+    private final Handler mCallback;
+
+    public BluetoothMnsObexServer(Handler callback) {
+        super();
+
+        mCallback = callback;
+    }
+
+    @Override
+    public int onConnect(final HeaderSet request, HeaderSet reply) {
+        Log.v(TAG, "onConnect");
+
+        try {
+            byte[] uuid = (byte[]) request.getHeader(HeaderSet.TARGET);
+
+            if (!Arrays.equals(uuid, MNS_TARGET)) {
+                return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
+            }
+
+        } catch (IOException e) {
+            // this should never happen since getHeader won't throw exception it
+            // declares to throw
+            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+        }
+
+        reply.setHeader(HeaderSet.WHO, MNS_TARGET);
+        return ResponseCodes.OBEX_HTTP_OK;
+    }
+
+    @Override
+    public void onDisconnect(final HeaderSet request, HeaderSet reply) {
+        Log.v(TAG, "onDisconnect");
+    }
+
+    @Override
+    public int onGet(final Operation op) {
+        Log.v(TAG, "onGet");
+
+        return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+    }
+
+    @Override
+    public int onPut(final Operation op) {
+        Log.v(TAG, "onPut");
+
+        try {
+            HeaderSet headerset;
+            headerset = op.getReceivedHeader();
+
+            String type = (String) headerset.getHeader(HeaderSet.TYPE);
+            ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
+
+            if (!TYPE.equals(type) || !oap.exists(BluetoothMasRequest.OAP_TAGID_MAS_INSTANCE_ID)) {
+                return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+            }
+
+            Byte inst = oap.getByte(BluetoothMasRequest.OAP_TAGID_MAS_INSTANCE_ID);
+
+            BluetoothMapEventReport ev = BluetoothMapEventReport.fromStream(op
+                    .openDataInputStream());
+
+            op.close();
+
+            mCallback.obtainMessage(BluetoothMnsService.MSG_EVENT, inst, 0, ev).sendToTarget();
+        } catch (IOException e) {
+            Log.e(TAG, "I/O exception when handling PUT request", e);
+            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+        }
+
+        return ResponseCodes.OBEX_HTTP_OK;
+    }
+
+    @Override
+    public int onAbort(final HeaderSet request, HeaderSet reply) {
+        Log.v(TAG, "onAbort");
+
+        return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED;
+    }
+
+    @Override
+    public int onSetPath(final HeaderSet request, HeaderSet reply,
+            final boolean backup, final boolean create) {
+        Log.v(TAG, "onSetPath");
+
+        return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+    }
+
+    @Override
+    public void onClose() {
+        Log.v(TAG, "onClose");
+
+        // TODO: call session handler so it can disconnect
+    }
+}
diff --git a/src/android/bluetooth/client/map/BluetoothMnsService.java b/src/android/bluetooth/client/map/BluetoothMnsService.java
new file mode 100644
index 0000000..42175e0
--- /dev/null
+++ b/src/android/bluetooth/client/map/BluetoothMnsService.java
@@ -0,0 +1,195 @@
+/*
+ * 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.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
+import android.os.Handler;
+import android.os.Message;
+import android.os.ParcelUuid;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.lang.ref.WeakReference;
+
+import javax.obex.ServerSession;
+
+class BluetoothMnsService {
+
+    private static final String TAG = "BluetoothMnsService";
+
+    private static final ParcelUuid MAP_MNS =
+            ParcelUuid.fromString("00001133-0000-1000-8000-00805F9B34FB");
+
+    static final int MSG_EVENT = 1;
+
+    /* for BluetoothMasClient */
+    static final int EVENT_REPORT = 1001;
+
+    /* these are shared across instances */
+    static private SparseArray<Handler> mCallbacks = null;
+    static private SocketAcceptThread mAcceptThread = null;
+    static private Handler mSessionHandler = null;
+    static private BluetoothServerSocket mServerSocket = null;
+
+    private static class SessionHandler extends Handler {
+
+        private final WeakReference<BluetoothMnsService> mService;
+
+        SessionHandler(BluetoothMnsService service) {
+            mService = new WeakReference<BluetoothMnsService>(service);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            Log.d(TAG, "Handler: msg: " + msg.what);
+
+            switch (msg.what) {
+                case MSG_EVENT:
+                    int instanceId = msg.arg1;
+
+                    synchronized (mCallbacks) {
+                        Handler cb = mCallbacks.get(instanceId);
+
+                        if (cb != null) {
+                            BluetoothMapEventReport ev = (BluetoothMapEventReport) msg.obj;
+                            cb.obtainMessage(EVENT_REPORT, ev).sendToTarget();
+                        } else {
+                            Log.w(TAG, "Got event for instance which is not registered: "
+                                    + instanceId);
+                        }
+                    }
+                    break;
+            }
+        }
+    }
+
+    private static class SocketAcceptThread extends Thread {
+
+        private boolean mInterrupted = false;
+
+        @Override
+        public void run() {
+
+            if (mServerSocket != null) {
+                Log.w(TAG, "Socket already created, exiting");
+                return;
+            }
+
+            try {
+                BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+                mServerSocket = adapter.listenUsingEncryptedRfcommWithServiceRecord(
+                        "MAP Message Notification Service", MAP_MNS.getUuid());
+            } catch (IOException e) {
+                mInterrupted = true;
+                Log.e(TAG, "I/O exception when trying to create server socket", e);
+            }
+
+            while (!mInterrupted) {
+                try {
+                    Log.v(TAG, "waiting to accept connection...");
+
+                    BluetoothSocket sock = mServerSocket.accept();
+
+                    Log.v(TAG, "new incoming connection from "
+                            + sock.getRemoteDevice().getName());
+
+                    // session will live until closed by remote
+                    BluetoothMnsObexServer srv = new BluetoothMnsObexServer(mSessionHandler);
+                    BluetoothMapRfcommTransport transport = new BluetoothMapRfcommTransport(
+                            sock);
+                    new ServerSession(transport, srv, null);
+                } catch (IOException ex) {
+                    Log.v(TAG, "I/O exception when waiting to accept (aborted?)");
+                    mInterrupted = true;
+                }
+            }
+
+            if (mServerSocket != null) {
+                try {
+                    mServerSocket.close();
+                } catch (IOException e) {
+                    // do nothing
+                }
+
+                mServerSocket = null;
+            }
+        }
+    }
+
+    BluetoothMnsService() {
+        Log.v(TAG, "BluetoothMnsService()");
+
+        if (mCallbacks == null) {
+            Log.v(TAG, "BluetoothMnsService(): allocating callbacks");
+            mCallbacks = new SparseArray<Handler>();
+        }
+
+        if (mSessionHandler == null) {
+            Log.v(TAG, "BluetoothMnsService(): allocating session handler");
+            mSessionHandler = new SessionHandler(this);
+        }
+    }
+
+    public void registerCallback(int instanceId, Handler callback) {
+        Log.v(TAG, "registerCallback()");
+
+        synchronized (mCallbacks) {
+            mCallbacks.put(instanceId, callback);
+
+            if (mAcceptThread == null) {
+                Log.v(TAG, "registerCallback(): starting MNS server");
+                mAcceptThread = new SocketAcceptThread();
+                mAcceptThread.setName("BluetoothMnsAcceptThread");
+                mAcceptThread.start();
+            }
+        }
+    }
+
+    public void unregisterCallback(int instanceId) {
+        Log.v(TAG, "unregisterCallback()");
+
+        synchronized (mCallbacks) {
+            mCallbacks.remove(instanceId);
+
+            if (mCallbacks.size() == 0) {
+                Log.v(TAG, "unregisterCallback(): shutting down MNS server");
+
+                if (mServerSocket != null) {
+                    try {
+                        mServerSocket.close();
+                    } catch (IOException e) {
+                    }
+
+                    mServerSocket = null;
+                }
+
+                mAcceptThread.interrupt();
+
+                try {
+                    mAcceptThread.join(5000);
+                } catch (InterruptedException e) {
+                }
+
+                mAcceptThread = null;
+            }
+        }
+    }
+}
diff --git a/src/android/bluetooth/client/map/utils/BmsgTokenizer.java b/src/android/bluetooth/client/map/utils/BmsgTokenizer.java
new file mode 100644
index 0000000..9f23961
--- /dev/null
+++ b/src/android/bluetooth/client/map/utils/BmsgTokenizer.java
@@ -0,0 +1,108 @@
+/*
+ * 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.utils;
+
+import android.util.Log;
+
+import java.text.ParseException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class BmsgTokenizer {
+
+    private final String mStr;
+
+    private final Matcher mMatcher;
+
+    private int mPos = 0;
+
+    private final int mOffset;
+
+    static public class Property {
+        public final String name;
+        public final String value;
+
+        public Property(String name, String value) {
+            if (name == null || value == null) {
+                throw new IllegalArgumentException();
+            }
+
+            this.name = name;
+            this.value = value;
+
+            Log.v("BMSG >> ", toString());
+        }
+
+        @Override
+        public String toString() {
+            return name + ":" + value;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            return ((o instanceof Property) && ((Property) o).name.equals(name) && ((Property) o).value
+                    .equals(value));
+        }
+    };
+
+    public BmsgTokenizer(String str) {
+        this(str, 0);
+    }
+
+    public BmsgTokenizer(String str, int offset) {
+        mStr = str;
+        mOffset = offset;
+        mMatcher = Pattern.compile("(([^:]*):(.*))?\r\n").matcher(str);
+        mPos = mMatcher.regionStart();
+    }
+
+    public Property next(boolean alwaysReturn) throws ParseException {
+        boolean found = false;
+
+        do {
+            mMatcher.region(mPos, mMatcher.regionEnd());
+
+            if (!mMatcher.lookingAt()) {
+                if (alwaysReturn) {
+                    return null;
+                }
+
+                throw new ParseException("Property or empty line expected", pos());
+            }
+
+            mPos = mMatcher.end();
+
+            if (mMatcher.group(1) != null) {
+                found = true;
+            }
+        } while (!found);
+
+        return new Property(mMatcher.group(2), mMatcher.group(3));
+    }
+
+    public Property next() throws ParseException {
+        return next(false);
+    }
+
+    public String remaining() {
+        return mStr.substring(mPos);
+    }
+
+    public int pos() {
+        return mPos + mOffset;
+    }
+}
diff --git a/src/android/bluetooth/client/map/utils/ObexAppParameters.java b/src/android/bluetooth/client/map/utils/ObexAppParameters.java
new file mode 100644
index 0000000..cae379b
--- /dev/null
+++ b/src/android/bluetooth/client/map/utils/ObexAppParameters.java
@@ -0,0 +1,182 @@
+/*
+ * 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.utils;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.obex.HeaderSet;
+
+public final class ObexAppParameters {
+
+    private final HashMap<Byte, byte[]> mParams;
+
+    public ObexAppParameters() {
+        mParams = new HashMap<Byte, byte[]>();
+    }
+
+    public ObexAppParameters(byte[] raw) {
+        mParams = new HashMap<Byte, byte[]>();
+
+        if (raw != null) {
+            for (int i = 0; i < raw.length;) {
+                if (raw.length - i < 2) {
+                    break;
+                }
+
+                byte tag = raw[i++];
+                byte len = raw[i++];
+
+                if (raw.length - i - len < 0) {
+                    break;
+                }
+
+                byte[] val = new byte[len];
+
+                System.arraycopy(raw, i, val, 0, len);
+                this.add(tag, val);
+
+                i += len;
+            }
+        }
+    }
+
+    public static ObexAppParameters fromHeaderSet(HeaderSet headerset) {
+        try {
+            byte[] raw = (byte[]) headerset.getHeader(HeaderSet.APPLICATION_PARAMETER);
+            return new ObexAppParameters(raw);
+        } catch (IOException e) {
+            // won't happen
+        }
+
+        return null;
+    }
+
+    public byte[] getHeader() {
+        int length = 0;
+
+        for (Map.Entry<Byte, byte[]> entry : mParams.entrySet()) {
+            length += (entry.getValue().length + 2);
+        }
+
+        byte[] ret = new byte[length];
+
+        int idx = 0;
+        for (Map.Entry<Byte, byte[]> entry : mParams.entrySet()) {
+            length = entry.getValue().length;
+
+            ret[idx++] = entry.getKey();
+            ret[idx++] = (byte) length;
+            System.arraycopy(entry.getValue(), 0, ret, idx, length);
+            idx += length;
+        }
+
+        return ret;
+    }
+
+    public void addToHeaderSet(HeaderSet headerset) {
+        if (mParams.size() > 0) {
+            headerset.setHeader(HeaderSet.APPLICATION_PARAMETER, getHeader());
+        }
+    }
+
+    public boolean exists(byte tag) {
+        return mParams.containsKey(tag);
+    }
+
+    public void add(byte tag, byte val) {
+        byte[] bval = ByteBuffer.allocate(1).put(val).array();
+        mParams.put(tag, bval);
+    }
+
+    public void add(byte tag, short val) {
+        byte[] bval = ByteBuffer.allocate(2).putShort(val).array();
+        mParams.put(tag, bval);
+    }
+
+    public void add(byte tag, int val) {
+        byte[] bval = ByteBuffer.allocate(4).putInt(val).array();
+        mParams.put(tag, bval);
+    }
+
+    public void add(byte tag, long val) {
+        byte[] bval = ByteBuffer.allocate(8).putLong(val).array();
+        mParams.put(tag, bval);
+    }
+
+    public void add(byte tag, String val) {
+        byte[] bval = val.getBytes();
+        mParams.put(tag, bval);
+    }
+
+    public void add(byte tag, byte[] bval) {
+        mParams.put(tag, bval);
+    }
+
+    public byte getByte(byte tag) {
+        byte[] bval = mParams.get(tag);
+
+        if (bval == null || bval.length < 1) {
+            return 0;
+        }
+
+        return ByteBuffer.wrap(bval).get();
+    }
+
+    public short getShort(byte tag) {
+        byte[] bval = mParams.get(tag);
+
+        if (bval == null || bval.length < 2) {
+            return 0;
+        }
+
+        return ByteBuffer.wrap(bval).getShort();
+    }
+
+    public int getInt(byte tag) {
+        byte[] bval = mParams.get(tag);
+
+        if (bval == null || bval.length < 4) {
+            return 0;
+        }
+
+        return ByteBuffer.wrap(bval).getInt();
+    }
+
+    public String getString(byte tag) {
+        byte[] bval = mParams.get(tag);
+
+        if (bval == null) {
+            return null;
+        }
+
+        return new String(bval);
+    }
+
+    public byte[] getByteArray(byte tag) {
+        byte[] bval = mParams.get(tag);
+
+        return bval;
+    }
+
+    @Override
+    public String toString() {
+        return mParams.toString();
+    }
+}
diff --git a/src/android/bluetooth/client/map/utils/ObexTime.java b/src/android/bluetooth/client/map/utils/ObexTime.java
new file mode 100644
index 0000000..b35ce81
--- /dev/null
+++ b/src/android/bluetooth/client/map/utils/ObexTime.java
@@ -0,0 +1,101 @@
+/*
+ * 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.utils;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class ObexTime {
+
+    private Date mDate;
+
+    public ObexTime(String time) {
+        /*
+         * match OBEX time string: YYYYMMDDTHHMMSS with optional UTF offset
+         * +/-hhmm
+         */
+        Pattern p = Pattern
+                .compile("(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})(\\d{2})(([+-])(\\d{2})(\\d{2}))?");
+        Matcher m = p.matcher(time);
+
+        if (m.matches()) {
+
+            /*
+             * matched groups are numberes as follows: YYYY MM DD T HH MM SS +
+             * hh mm ^^^^ ^^ ^^ ^^ ^^ ^^ ^ ^^ ^^ 1 2 3 4 5 6 8 9 10 all groups
+             * are guaranteed to be numeric so conversion will always succeed
+             * (except group 8 which is either + or -)
+             */
+
+            Calendar cal = Calendar.getInstance();
+            cal.set(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)) - 1,
+                    Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)),
+                    Integer.parseInt(m.group(5)), Integer.parseInt(m.group(6)));
+
+            /*
+             * if 7th group is matched then we have UTC offset information
+             * included
+             */
+            if (m.group(7) != null) {
+                int ohh = Integer.parseInt(m.group(9));
+                int omm = Integer.parseInt(m.group(10));
+
+                /* time zone offset is specified in miliseconds */
+                int offset = (ohh * 60 + omm) * 60 * 1000;
+
+                if (m.group(8).equals("-")) {
+                    offset = -offset;
+                }
+
+                TimeZone tz = TimeZone.getTimeZone("UTC");
+                tz.setRawOffset(offset);
+
+                cal.setTimeZone(tz);
+            }
+
+            mDate = cal.getTime();
+        }
+    }
+
+    public ObexTime(Date date) {
+        mDate = date;
+    }
+
+    public Date getTime() {
+        return mDate;
+    }
+
+    @Override
+    public String toString() {
+        if (mDate == null) {
+            return null;
+        }
+
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(mDate);
+
+        /* note that months are numbered stating from 0 */
+        return String.format(Locale.US, "%04d%02d%02dT%02d%02d%02d",
+                cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1,
+                cal.get(Calendar.DATE), cal.get(Calendar.HOUR_OF_DAY),
+                cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND));
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/BluetoothPbapCard.java b/src/android/bluetooth/client/pbap/BluetoothPbapCard.java
new file mode 100644
index 0000000..6c4fadc
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/BluetoothPbapCard.java
@@ -0,0 +1,142 @@
+/*
+ * 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.pbap;
+
+import com.android.vcard.VCardEntry;
+import com.android.vcard.VCardEntry.EmailData;
+import com.android.vcard.VCardEntry.NameData;
+import com.android.vcard.VCardEntry.PhoneData;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.List;
+
+/**
+ * Entry representation of folder listing
+ */
+public class BluetoothPbapCard {
+
+    public final String handle;
+
+    public final String N;
+    public final String lastName;
+    public final String firstName;
+    public final String middleName;
+    public final String prefix;
+    public final String suffix;
+
+    public BluetoothPbapCard(String handle, String name) {
+        this.handle = handle;
+
+        N = name;
+
+        /*
+         * format is as for vCard N field, so we have up to 5 tokens: LastName;
+         * FirstName; MiddleName; Prefix; Suffix
+         */
+        String[] parsedName = name.split(";", 5);
+
+        lastName = parsedName.length < 1 ? null : parsedName[0];
+        firstName = parsedName.length < 2 ? null : parsedName[1];
+        middleName = parsedName.length < 3 ? null : parsedName[2];
+        prefix = parsedName.length < 4 ? null : parsedName[3];
+        suffix = parsedName.length < 5 ? null : parsedName[4];
+    }
+
+    @Override
+    public String toString() {
+        JSONObject json = new JSONObject();
+
+        try {
+            json.put("handle", handle);
+            json.put("N", N);
+            json.put("lastName", lastName);
+            json.put("firstName", firstName);
+            json.put("middleName", middleName);
+            json.put("prefix", prefix);
+            json.put("suffix", suffix);
+        } catch (JSONException e) {
+            // do nothing
+        }
+
+        return json.toString();
+    }
+
+    static public String jsonifyVcardEntry(VCardEntry vcard) {
+        JSONObject json = new JSONObject();
+
+        try {
+            NameData name = vcard.getNameData();
+            json.put("formatted", name.getFormatted());
+            json.put("family", name.getFamily());
+            json.put("given", name.getGiven());
+            json.put("middle", name.getMiddle());
+            json.put("prefix", name.getPrefix());
+            json.put("suffix", name.getSuffix());
+        } catch (JSONException e) {
+            // do nothing
+        }
+
+        try {
+            JSONArray jsonPhones = new JSONArray();
+
+            List<PhoneData> phones = vcard.getPhoneList();
+
+            if (phones != null) {
+                for (PhoneData phone : phones) {
+                    JSONObject jsonPhone = new JSONObject();
+                    jsonPhone.put("type", phone.getType());
+                    jsonPhone.put("number", phone.getNumber());
+                    jsonPhone.put("label", phone.getLabel());
+                    jsonPhone.put("is_primary", phone.isPrimary());
+
+                    jsonPhones.put(jsonPhone);
+                }
+
+                json.put("phones", jsonPhones);
+            }
+        } catch (JSONException e) {
+            // do nothing
+        }
+
+        try {
+            JSONArray jsonEmails = new JSONArray();
+
+            List<EmailData> emails = vcard.getEmailList();
+
+            if (emails != null) {
+                for (EmailData email : emails) {
+                    JSONObject jsonEmail = new JSONObject();
+                    jsonEmail.put("type", email.getType());
+                    jsonEmail.put("address", email.getAddress());
+                    jsonEmail.put("label", email.getLabel());
+                    jsonEmail.put("is_primary", email.isPrimary());
+
+                    jsonEmails.put(jsonEmail);
+                }
+
+                json.put("emails", jsonEmails);
+            }
+        } catch (JSONException e) {
+            // do nothing
+        }
+
+        return json.toString();
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/BluetoothPbapClient.java b/src/android/bluetooth/client/pbap/BluetoothPbapClient.java
new file mode 100644
index 0000000..5e212e8
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/BluetoothPbapClient.java
@@ -0,0 +1,846 @@
+/*
+ * 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.pbap;
+
+import android.bluetooth.BluetoothDevice;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Public API to control Phone Book Profile (PCE role only).
+ * <p>
+ * This class defines methods that shall be used by application for the
+ * retrieval of phone book objects from remote device.
+ * <p>
+ * How to connect to remote device which is acting in PSE role:
+ * <ul>
+ * <li>Create a <code>BluetoothDevice</code> object which corresponds to remote
+ * device in PSE role;
+ * <li>Create an instance of <code>BluetoothPbapClient</code> class, passing
+ * <code>BluetothDevice</code> object along with a <code>Handler</code> to it;
+ * <li>Use {@link #setPhoneBookFolderRoot}, {@link #setPhoneBookFolderUp} and
+ * {@link #setPhoneBookFolderDown} to navigate in virtual phone book folder
+ * structure
+ * <li>Use {@link #pullPhoneBookSize} or {@link #pullVcardListingSize} to
+ * retrieve the size of selected phone book
+ * <li>Use {@link #pullPhoneBook} to retrieve phone book entries
+ * <li>Use {@link #pullVcardListing} to retrieve list of entries in the phone
+ * book
+ * <li>Use {@link #pullVcardEntry} to pull single entry from the phone book
+ * </ul>
+ * Upon completion of each call above PCE will notify application if operation
+ * completed successfully (along with results) or failed.
+ * <p>
+ * Therefore, application should handle following events in its message queue
+ * handler:
+ * <ul>
+ * <li><code>EVENT_PULL_PHONE_BOOK_SIZE_DONE</code>
+ * <li><code>EVENT_PULL_VCARD_LISTING_SIZE_DONE</code>
+ * <li><code>EVENT_PULL_PHONE_BOOK_DONE</code>
+ * <li><code>EVENT_PULL_VCARD_LISTING_DONE</code>
+ * <li><code>EVENT_PULL_VCARD_ENTRY_DONE</code>
+ * <li><code>EVENT_SET_PHONE_BOOK_DONE</code>
+ * </ul>
+ * and
+ * <ul>
+ * <li><code>EVENT_PULL_PHONE_BOOK_SIZE_ERROR</code>
+ * <li><code>EVENT_PULL_VCARD_LISTING_SIZE_ERROR</code>
+ * <li><code>EVENT_PULL_PHONE_BOOK_ERROR</code>
+ * <li><code>EVENT_PULL_VCARD_LISTING_ERROR</code>
+ * <li><code>EVENT_PULL_VCARD_ENTRY_ERROR</code>
+ * <li><code>EVENT_SET_PHONE_BOOK_ERROR</code>
+ * </ul>
+ * <code>connect</code> and <code>disconnect</code> methods are introduced for
+ * testing purposes. An application does not need to use them as the session
+ * connection and disconnection happens automatically internally.
+ */
+public class BluetoothPbapClient {
+    private static final String TAG = "BluetoothPbapClient";
+
+    /**
+     * Path to local incoming calls history object
+     */
+    public static final String ICH_PATH = "telecom/ich.vcf";
+
+    /**
+     * Path to local outgoing calls history object
+     */
+    public static final String OCH_PATH = "telecom/och.vcf";
+
+    /**
+     * Path to local missed calls history object
+     */
+    public static final String MCH_PATH = "telecom/mch.vcf";
+
+    /**
+     * Path to local combined calls history object
+     */
+    public static final String CCH_PATH = "telecom/cch.vcf";
+
+    /**
+     * Path to local main phone book object
+     */
+    public static final String PB_PATH = "telecom/pb.vcf";
+
+    /**
+     * Path to incoming calls history object stored on the phone's SIM card
+     */
+    public static final String SIM_ICH_PATH = "SIM1/telecom/ich.vcf";
+
+    /**
+     * Path to outgoing calls history object stored on the phone's SIM card
+     */
+    public static final String SIM_OCH_PATH = "SIM1/telecom/och.vcf";
+
+    /**
+     * Path to missed calls history object stored on the phone's SIM card
+     */
+    public static final String SIM_MCH_PATH = "SIM1/telecom/mch.vcf";
+
+    /**
+     * Path to combined calls history object stored on the phone's SIM card
+     */
+    public static final String SIM_CCH_PATH = "SIM1/telecom/cch.vcf";
+
+    /**
+     * Path to main phone book object stored on the phone's SIM card
+     */
+    public static final String SIM_PB_PATH = "SIM1/telecom/pb.vcf";
+
+    /**
+     * Indicates to server that default sorting order shall be used for vCard
+     * listing.
+     */
+    public static final byte ORDER_BY_DEFAULT = -1;
+
+    /**
+     * Indicates to server that indexed sorting order shall be used for vCard
+     * listing.
+     */
+    public static final byte ORDER_BY_INDEXED = 0;
+
+    /**
+     * Indicates to server that alphabetical sorting order shall be used for the
+     * vCard listing.
+     */
+    public static final byte ORDER_BY_ALPHABETICAL = 1;
+
+    /**
+     * Indicates to server that phonetical (based on sound attribute) sorting
+     * order shall be used for the vCard listing.
+     */
+    public static final byte ORDER_BY_PHONETIC = 2;
+
+    /**
+     * Indicates to server that Name attribute of vCard shall be used to carry
+     * out the search operation on
+     */
+    public static final byte SEARCH_ATTR_NAME = 0;
+
+    /**
+     * Indicates to server that Number attribute of vCard shall be used to carry
+     * out the search operation on
+     */
+    public static final byte SEARCH_ATTR_NUMBER = 1;
+
+    /**
+     * Indicates to server that Sound attribute of vCard shall be used to carry
+     * out the search operation
+     */
+    public static final byte SEARCH_ATTR_SOUND = 2;
+
+    /**
+     * VCard format version 2.1
+     */
+    public static final byte VCARD_TYPE_21 = 0;
+
+    /**
+     * VCard format version 3.0
+     */
+    public static final byte VCARD_TYPE_30 = 1;
+
+    /* 64-bit mask used to filter out VCard fields */
+    // TODO: Think of extracting to separate class
+    public static final long VCARD_ATTR_VERSION = 0x000000000000000001;
+    public static final long VCARD_ATTR_FN = 0x000000000000000002;
+    public static final long VCARD_ATTR_N = 0x000000000000000004;
+    public static final long VCARD_ATTR_PHOTO = 0x000000000000000008;
+    public static final long VCARD_ATTR_BDAY = 0x000000000000000010;
+    public static final long VCARD_ATTR_ADDR = 0x000000000000000020;
+    public static final long VCARD_ATTR_LABEL = 0x000000000000000040;
+    public static final long VCARD_ATTR_TEL = 0x000000000000000080;
+    public static final long VCARD_ATTR_EMAIL = 0x000000000000000100;
+    public static final long VCARD_ATTR_MAILER = 0x000000000000000200;
+    public static final long VCARD_ATTR_TZ = 0x000000000000000400;
+    public static final long VCARD_ATTR_GEO = 0x000000000000000800;
+    public static final long VCARD_ATTR_TITLE = 0x000000000000001000;
+    public static final long VCARD_ATTR_ROLE = 0x000000000000002000;
+    public static final long VCARD_ATTR_LOGO = 0x000000000000004000;
+    public static final long VCARD_ATTR_AGENT = 0x000000000000008000;
+    public static final long VCARD_ATTR_ORG = 0x000000000000010000;
+    public static final long VCARD_ATTR_NOTE = 0x000000000000020000;
+    public static final long VCARD_ATTR_REV = 0x000000000000040000;
+    public static final long VCARD_ATTR_SOUND = 0x000000000000080000;
+    public static final long VCARD_ATTR_URL = 0x000000000000100000;
+    public static final long VCARD_ATTR_UID = 0x000000000000200000;
+    public static final long VCARD_ATTR_KEY = 0x000000000000400000;
+    public static final long VCARD_ATTR_NICKNAME = 0x000000000000800000;
+    public static final long VCARD_ATTR_CATEGORIES = 0x000000000001000000;
+    public static final long VCARD_ATTR_PROID = 0x000000000002000000;
+    public static final long VCARD_ATTR_CLASS = 0x000000000004000000;
+    public static final long VCARD_ATTR_SORT_STRING = 0x000000000008000000;
+    public static final long VCARD_ATTR_X_IRMC_CALL_DATETIME =
+            0x000000000010000000;
+
+    /**
+     * Maximal number of entries of the phone book that PCE can handle
+     */
+    public static final short MAX_LIST_COUNT = (short) 0xFFFF;
+
+    /**
+     * Event propagated upon completion of <code>setPhoneBookFolderRoot</code>,
+     * <code>setPhoneBookFolderUp</code> or <code>setPhoneBookFolderDown</code>
+     * request.
+     * <p>
+     * This event indicates that request completed successfully.
+     * @see #setPhoneBookFolderRoot
+     * @see #setPhoneBookFolderUp
+     * @see #setPhoneBookFolderDown
+     */
+    public static final int EVENT_SET_PHONE_BOOK_DONE = 1;
+
+    /**
+     * Event propagated upon completion of <code>pullPhoneBook</code> request.
+     * <p>
+     * This event carry on results of the request.
+     * <p>
+     * The resulting message contains:
+     * <table>
+     * <tr>
+     * <td><code>msg.arg1</code></td>
+     * <td>newMissedCalls parameter (only in case of missed calls history object
+     * request)</td>
+     * </tr>
+     * <tr>
+     * <td><code>msg.obj</code></td>
+     * <td>which is a list of <code>VCardEntry</code> objects</td>
+     * </tr>
+     * </table>
+     * @see #pullPhoneBook
+     */
+    public static final int EVENT_PULL_PHONE_BOOK_DONE = 2;
+
+    /**
+     * Event propagated upon completion of <code>pullVcardListing</code>
+     * request.
+     * <p>
+     * This event carry on results of the request.
+     * <p>
+     * The resulting message contains:
+     * <table>
+     * <tr>
+     * <td><code>msg.arg1</code></td>
+     * <td>newMissedCalls parameter (only in case of missed calls history object
+     * request)</td>
+     * </tr>
+     * <tr>
+     * <td><code>msg.obj</code></td>
+     * <td>which is a list of <code>BluetoothPbapCard</code> objects</td>
+     * </tr>
+     * </table>
+     * @see #pullVcardListing
+     */
+    public static final int EVENT_PULL_VCARD_LISTING_DONE = 3;
+
+    /**
+     * Event propagated upon completion of <code>pullVcardEntry</code> request.
+     * <p>
+     * This event carry on results of the request.
+     * <p>
+     * The resulting message contains:
+     * <table>
+     * <tr>
+     * <td><code>msg.obj</code></td>
+     * <td>vCard as and object of type <code>VCardEntry</code></td>
+     * </tr>
+     * </table>
+     * @see #pullVcardEntry
+     */
+    public static final int EVENT_PULL_VCARD_ENTRY_DONE = 4;
+
+    /**
+     * Event propagated upon completion of <code>pullPhoneBookSize</code>
+     * request.
+     * <p>
+     * This event carry on results of the request.
+     * <p>
+     * The resulting message contains:
+     * <table>
+     * <tr>
+     * <td><code>msg.arg1</code></td>
+     * <td>size of the phone book</td>
+     * </tr>
+     * </table>
+     * @see #pullPhoneBookSize
+     */
+    public static final int EVENT_PULL_PHONE_BOOK_SIZE_DONE = 5;
+
+    /**
+     * Event propagated upon completion of <code>pullVcardListingSize</code>
+     * request.
+     * <p>
+     * This event carry on results of the request.
+     * <p>
+     * The resulting message contains:
+     * <table>
+     * <tr>
+     * <td><code>msg.arg1</code></td>
+     * <td>size of the phone book listing</td>
+     * </tr>
+     * </table>
+     * @see #pullVcardListingSize
+     */
+    public static final int EVENT_PULL_VCARD_LISTING_SIZE_DONE = 6;
+
+    /**
+     * Event propagated upon completion of <code>setPhoneBookFolderRoot</code>,
+     * <code>setPhoneBookFolderUp</code> or <code>setPhoneBookFolderDown</code>
+     * request. This event indicates an error during operation.
+     */
+    public static final int EVENT_SET_PHONE_BOOK_ERROR = 101;
+
+    /**
+     * Event propagated upon completion of <code>pullPhoneBook</code> request.
+     * This event indicates an error during operation.
+     */
+    public static final int EVENT_PULL_PHONE_BOOK_ERROR = 102;
+
+    /**
+     * Event propagated upon completion of <code>pullVcardListing</code>
+     * request. This event indicates an error during operation.
+     */
+    public static final int EVENT_PULL_VCARD_LISTING_ERROR = 103;
+
+    /**
+     * Event propagated upon completion of <code>pullVcardEntry</code> request.
+     * This event indicates an error during operation.
+     */
+    public static final int EVENT_PULL_VCARD_ENTRY_ERROR = 104;
+
+    /**
+     * Event propagated upon completion of <code>pullPhoneBookSize</code>
+     * request. This event indicates an error during operation.
+     */
+    public static final int EVENT_PULL_PHONE_BOOK_SIZE_ERROR = 105;
+
+    /**
+     * Event propagated upon completion of <code>pullVcardListingSize</code>
+     * request. This event indicates an error during operation.
+     */
+    public static final int EVENT_PULL_VCARD_LISTING_SIZE_ERROR = 106;
+
+    /**
+     * Event propagated when PCE has been connected to PSE
+     */
+    public static final int EVENT_SESSION_CONNECTED = 201;
+
+    /**
+     * Event propagated when PCE has been disconnected from PSE
+     */
+    public static final int EVENT_SESSION_DISCONNECTED = 202;
+    public static final int EVENT_SESSION_AUTH_REQUESTED = 203;
+    public static final int EVENT_SESSION_AUTH_TIMEOUT = 204;
+
+    public enum ConnectionState {
+        DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING;
+    }
+
+    private final Handler mClientHandler;
+    private final BluetoothPbapSession mSession;
+    private ConnectionState mConnectionState = ConnectionState.DISCONNECTED;
+
+    private SessionHandler mSessionHandler;
+
+    private static class SessionHandler extends Handler {
+
+        private final WeakReference<BluetoothPbapClient> mClient;
+
+        SessionHandler(BluetoothPbapClient client) {
+            mClient = new WeakReference<BluetoothPbapClient>(client);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            Log.d(TAG, "handleMessage: what=" + msg.what);
+
+            BluetoothPbapClient client = mClient.get();
+            if (client == null) {
+                return;
+            }
+
+            switch (msg.what) {
+                case BluetoothPbapSession.REQUEST_FAILED:
+                {
+                    BluetoothPbapRequest req = (BluetoothPbapRequest) msg.obj;
+
+                    if (req instanceof BluetoothPbapRequestPullPhoneBookSize) {
+                        client.sendToClient(EVENT_PULL_PHONE_BOOK_SIZE_ERROR);
+                    } else if (req instanceof BluetoothPbapRequestPullVcardListingSize) {
+                        client.sendToClient(EVENT_PULL_VCARD_LISTING_SIZE_ERROR);
+                    } else if (req instanceof BluetoothPbapRequestPullPhoneBook) {
+                        client.sendToClient(EVENT_PULL_PHONE_BOOK_ERROR);
+                    } else if (req instanceof BluetoothPbapRequestPullVcardListing) {
+                        client.sendToClient(EVENT_PULL_VCARD_LISTING_ERROR);
+                    } else if (req instanceof BluetoothPbapRequestPullVcardEntry) {
+                        client.sendToClient(EVENT_PULL_VCARD_ENTRY_ERROR);
+                    } else if (req instanceof BluetoothPbapRequestSetPath) {
+                        client.sendToClient(EVENT_SET_PHONE_BOOK_ERROR);
+                    }
+
+                    break;
+                }
+
+                case BluetoothPbapSession.REQUEST_COMPLETED:
+                {
+                    BluetoothPbapRequest req = (BluetoothPbapRequest) msg.obj;
+
+                    if (req instanceof BluetoothPbapRequestPullPhoneBookSize) {
+                        int size = ((BluetoothPbapRequestPullPhoneBookSize) req).getSize();
+                        client.sendToClient(EVENT_PULL_PHONE_BOOK_SIZE_DONE, size);
+
+                    } else if (req instanceof BluetoothPbapRequestPullVcardListingSize) {
+                        int size = ((BluetoothPbapRequestPullVcardListingSize) req).getSize();
+                        client.sendToClient(EVENT_PULL_VCARD_LISTING_SIZE_DONE, size);
+
+                    } else if (req instanceof BluetoothPbapRequestPullPhoneBook) {
+                        BluetoothPbapRequestPullPhoneBook r = (BluetoothPbapRequestPullPhoneBook) req;
+                        client.sendToClient(EVENT_PULL_PHONE_BOOK_DONE, r.getNewMissedCalls(),
+                                r.getList());
+
+                    } else if (req instanceof BluetoothPbapRequestPullVcardListing) {
+                        BluetoothPbapRequestPullVcardListing r = (BluetoothPbapRequestPullVcardListing) req;
+                        client.sendToClient(EVENT_PULL_VCARD_LISTING_DONE, r.getNewMissedCalls(),
+                                r.getList());
+
+                    } else if (req instanceof BluetoothPbapRequestPullVcardEntry) {
+                        BluetoothPbapRequestPullVcardEntry r = (BluetoothPbapRequestPullVcardEntry) req;
+                        client.sendToClient(EVENT_PULL_VCARD_ENTRY_DONE, r.getVcard());
+
+                    } else if (req instanceof BluetoothPbapRequestSetPath) {
+                        client.sendToClient(EVENT_SET_PHONE_BOOK_DONE);
+                    }
+
+                    break;
+                }
+
+                case BluetoothPbapSession.AUTH_REQUESTED:
+                    client.sendToClient(EVENT_SESSION_AUTH_REQUESTED);
+                    break;
+
+                case BluetoothPbapSession.AUTH_TIMEOUT:
+                    client.sendToClient(EVENT_SESSION_AUTH_TIMEOUT);
+                    break;
+
+                /*
+                 * app does not need to know when session is connected since
+                 * OBEX session is managed inside BluetoothPbapSession
+                 * automatically - we add this only so app can visualize PBAP
+                 * connection status in case it wants to
+                 */
+
+                case BluetoothPbapSession.SESSION_CONNECTING:
+                    client.mConnectionState = ConnectionState.CONNECTING;
+                    break;
+
+                case BluetoothPbapSession.SESSION_CONNECTED:
+                    client.mConnectionState = ConnectionState.CONNECTED;
+                    client.sendToClient(EVENT_SESSION_CONNECTED);
+                    break;
+
+                case BluetoothPbapSession.SESSION_DISCONNECTED:
+                    client.mConnectionState = ConnectionState.DISCONNECTED;
+                    client.sendToClient(EVENT_SESSION_DISCONNECTED);
+                    break;
+            }
+        }
+    };
+
+    private void sendToClient(int eventId) {
+        sendToClient(eventId, 0, null);
+    }
+
+    private void sendToClient(int eventId, int param) {
+        sendToClient(eventId, param, null);
+    }
+
+    private void sendToClient(int eventId, Object param) {
+        sendToClient(eventId, 0, param);
+    }
+
+    private void sendToClient(int eventId, int param1, Object param2) {
+        mClientHandler.obtainMessage(eventId, param1, 0, param2).sendToTarget();
+    }
+
+    /**
+     * Constructs PCE object
+     *
+     * @param device BluetoothDevice that corresponds to remote acting in PSE
+     *            role
+     * @param handler the handle that will be used by PCE to notify events and
+     *            results to application
+     * @throws NullPointerException
+     */
+    public BluetoothPbapClient(BluetoothDevice device, Handler handler) {
+        if (device == null) {
+            throw new NullPointerException("BluetothDevice is null");
+        }
+
+        mClientHandler = handler;
+
+        mSessionHandler = new SessionHandler(this);
+
+        mSession = new BluetoothPbapSession(device, mSessionHandler);
+    }
+
+    /**
+     * Starts a pbap session. <pb> This method set up rfcomm session, obex
+     * session and waits for requests to be transfered to PSE.
+     */
+    public void connect() {
+        mSession.start();
+    }
+
+    @Override
+    public void finalize() {
+        if (mSession != null) {
+            mSession.stop();
+        }
+    }
+
+    /**
+     * Stops all the active transactions and disconnects from the server.
+     */
+    public void disconnect() {
+        mSession.stop();
+    }
+
+    /**
+     * Aborts current request, if any
+     */
+    public void abort() {
+        mSession.abort();
+    }
+
+    public ConnectionState getState() {
+        return mConnectionState;
+    }
+
+    /**
+     * Sets current folder to root
+     *
+     * @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_SET_PHONE_BOOK_DONE} or
+     *         {@link #EVENT_SET_PHONE_BOOK_ERROR} in case of failure
+     */
+    public boolean setPhoneBookFolderRoot() {
+        BluetoothPbapRequest req = new BluetoothPbapRequestSetPath(false);
+        return mSession.makeRequest(req);
+    }
+
+    /**
+     * Sets current folder to parent
+     *
+     * @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_SET_PHONE_BOOK_DONE} or
+     *         {@link #EVENT_SET_PHONE_BOOK_ERROR} in case of failure
+     */
+    public boolean setPhoneBookFolderUp() {
+        BluetoothPbapRequest req = new BluetoothPbapRequestSetPath(true);
+        return mSession.makeRequest(req);
+    }
+
+    /**
+     * Sets current folder to selected sub-folder
+     *
+     * @param folder the name of the sub-folder
+     * @return @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_SET_PHONE_BOOK_DONE} or
+     *         {@link #EVENT_SET_PHONE_BOOK_ERROR} in case of failure
+     */
+    public boolean setPhoneBookFolderDown(String folder) {
+        BluetoothPbapRequest req = new BluetoothPbapRequestSetPath(folder);
+        return mSession.makeRequest(req);
+    }
+
+    /**
+     * Requests for the number of entries in the phone book.
+     *
+     * @param pbName absolute path to the phone book
+     * @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_PULL_PHONE_BOOK_SIZE_DONE} or
+     *         {@link #EVENT_PULL_PHONE_BOOK_SIZE_ERROR} in case of failure
+     */
+    public boolean pullPhoneBookSize(String pbName) {
+        BluetoothPbapRequestPullPhoneBookSize req = new BluetoothPbapRequestPullPhoneBookSize(
+                pbName);
+
+        return mSession.makeRequest(req);
+    }
+
+    /**
+     * Requests for the number of entries in the phone book listing.
+     *
+     * @param folder the name of the folder to be retrieved
+     * @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_PULL_VCARD_LISTING_SIZE_DONE} or
+     *         {@link #EVENT_PULL_VCARD_LISTING_SIZE_ERROR} in case of failure
+     */
+    public boolean pullVcardListingSize(String folder) {
+        BluetoothPbapRequestPullVcardListingSize req = new BluetoothPbapRequestPullVcardListingSize(
+                folder);
+
+        return mSession.makeRequest(req);
+    }
+
+    /**
+     * Pulls complete phone book. This method pulls phone book which entries are
+     * of <code>VCARD_TYPE_21</code> type and each single vCard contains minimal
+     * required set of fields and the number of entries in response is not
+     * limited.
+     *
+     * @param pbName absolute path to the phone book
+     * @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_PULL_PHONE_BOOK_DONE} or
+     *         {@link #EVENT_PULL_PHONE_BOOK_ERROR} in case of failure
+     */
+    public boolean pullPhoneBook(String pbName) {
+        return pullPhoneBook(pbName, 0, VCARD_TYPE_21, 0, 0);
+    }
+
+    /**
+     * Pulls complete phone book. This method pulls all entries from the phone
+     * book.
+     *
+     * @param pbName absolute path to the phone book
+     * @param filter bit mask which indicates which fields of the vCard shall be
+     *            included in each entry of the resulting list
+     * @param format vCard format of entries in the resulting list
+     * @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_PULL_PHONE_BOOK_DONE} or
+     *         {@link #EVENT_PULL_PHONE_BOOK_ERROR} in case of failure
+     */
+    public boolean pullPhoneBook(String pbName, long filter, byte format) {
+        return pullPhoneBook(pbName, filter, format, 0, 0);
+    }
+
+    /**
+     * Pulls complete phone book. This method pulls entries from the phone book
+     * limited to the number of <code>maxListCount</code> starting from the
+     * position of <code>listStartOffset</code>.
+     * <p>
+     * The resulting list contains vCard objects in version
+     * <code>VCARD_TYPE_21</code> which in turns contain minimal required set of
+     * vCard fields.
+     *
+     * @param pbName absolute path to the phone book
+     * @param maxListCount limits number of entries in the response
+     * @param listStartOffset offset to the first entry of the list that would
+     *            be returned
+     * @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_PULL_PHONE_BOOK_DONE} or
+     *         {@link #EVENT_PULL_PHONE_BOOK_ERROR} in case of failure
+     */
+    public boolean pullPhoneBook(String pbName, int maxListCount, int listStartOffset) {
+        return pullPhoneBook(pbName, 0, VCARD_TYPE_21, maxListCount, listStartOffset);
+    }
+
+    /**
+     * Pulls complete phone book.
+     *
+     * @param pbName absolute path to the phone book
+     * @param filter bit mask which indicates which fields of the vCard hall be
+     *            included in each entry of the resulting list
+     * @param format vCard format of entries in the resulting list
+     * @param maxListCount limits number of entries in the response
+     * @param listStartOffset offset to the first entry of the list that would
+     *            be returned
+     * @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_PULL_PHONE_BOOK_DONE} or
+     *         {@link #EVENT_PULL_PHONE_BOOK_ERROR} in case of failure
+     */
+    public boolean pullPhoneBook(String pbName, long filter, byte format, int maxListCount,
+            int listStartOffset) {
+        BluetoothPbapRequest req = new BluetoothPbapRequestPullPhoneBook(pbName, filter, format,
+                maxListCount, listStartOffset);
+        return mSession.makeRequest(req);
+    }
+
+    /**
+     * Pulls list of entries in the phone book.
+     * <p>
+     * This method pulls the list of entries in the <code>folder</code>.
+     *
+     * @param folder the name of the folder to be retrieved
+     * @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_PULL_VCARD_LISTING_DONE} or
+     *         {@link #EVENT_PULL_VCARD_LISTING_ERROR} in case of failure
+     */
+    public boolean pullVcardListing(String folder) {
+        return pullVcardListing(folder, ORDER_BY_DEFAULT, SEARCH_ATTR_NAME, null, 0, 0);
+    }
+
+    /**
+     * Pulls list of entries in the <code>folder</code>.
+     *
+     * @param folder the name of the folder to be retrieved
+     * @param order the sorting order of the resulting list of entries
+     * @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_PULL_VCARD_LISTING_DONE} or
+     *         {@link #EVENT_PULL_VCARD_LISTING_ERROR} in case of failure
+     */
+    public boolean pullVcardListing(String folder, byte order) {
+        return pullVcardListing(folder, order, SEARCH_ATTR_NAME, null, 0, 0);
+    }
+
+    /**
+     * Pulls list of entries in the <code>folder</code>. Only entries where
+     * <code>searchAttr</code> attribute of vCard matches <code>searchVal</code>
+     * will be listed.
+     *
+     * @param folder the name of the folder to be retrieved
+     * @param searchAttr vCard attribute which shall be used to carry out search
+     *            operation on
+     * @param searchVal text string used by matching routine to match the value
+     *            of the attribute indicated by SearchAttr
+     * @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_PULL_VCARD_LISTING_DONE} or
+     *         {@link #EVENT_PULL_VCARD_LISTING_ERROR} in case of failure
+     */
+    public boolean pullVcardListing(String folder, byte searchAttr, String searchVal) {
+        return pullVcardListing(folder, ORDER_BY_DEFAULT, searchAttr, searchVal, 0, 0);
+    }
+
+    /**
+     * Pulls list of entries in the <code>folder</code>.
+     *
+     * @param folder the name of the folder to be retrieved
+     * @param order the sorting order of the resulting list of entries
+     * @param maxListCount limits number of entries in the response
+     * @param listStartOffset offset to the first entry of the list that would
+     *            be returned
+     * @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_PULL_VCARD_LISTING_DONE} or
+     *         {@link #EVENT_PULL_VCARD_LISTING_ERROR} in case of failure
+     */
+    public boolean pullVcardListing(String folder, byte order, int maxListCount,
+            int listStartOffset) {
+        return pullVcardListing(folder, order, SEARCH_ATTR_NAME, null, maxListCount,
+                listStartOffset);
+    }
+
+    /**
+     * Pulls list of entries in the <code>folder</code>.
+     *
+     * @param folder the name of the folder to be retrieved
+     * @param maxListCount limits number of entries in the response
+     * @param listStartOffset offset to the first entry of the list that would
+     *            be returned
+     * @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_PULL_VCARD_LISTING_DONE} or
+     *         {@link #EVENT_PULL_VCARD_LISTING_ERROR} in case of failure
+     */
+    public boolean pullVcardListing(String folder, int maxListCount, int listStartOffset) {
+        return pullVcardListing(folder, ORDER_BY_DEFAULT, SEARCH_ATTR_NAME, null, maxListCount,
+                listStartOffset);
+    }
+
+    /**
+     * Pulls list of entries in the <code>folder</code>.
+     *
+     * @param folder the name of the folder to be retrieved
+     * @param order the sorting order of the resulting list of entries
+     * @param searchAttr vCard attribute which shall be used to carry out search
+     *            operation on
+     * @param searchVal text string used by matching routine to match the value
+     *            of the attribute indicated by SearchAttr
+     * @param maxListCount limits number of entries in the response
+     * @param listStartOffset offset to the first entry of the list that would
+     *            be returned
+     * @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_PULL_VCARD_LISTING_DONE} or
+     *         {@link #EVENT_PULL_VCARD_LISTING_ERROR} in case of failure
+     */
+    public boolean pullVcardListing(String folder, byte order, byte searchAttr,
+            String searchVal, int maxListCount, int listStartOffset) {
+        BluetoothPbapRequest req = new BluetoothPbapRequestPullVcardListing(folder, order,
+                searchAttr, searchVal, maxListCount, listStartOffset);
+        return mSession.makeRequest(req);
+    }
+
+    /**
+     * Pulls single vCard object
+     *
+     * @param handle handle to the vCard which shall be pulled
+     * @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_PULL_VCARD_DONE} or
+     * @link #EVENT_PULL_VCARD_ERROR} in case of failure
+     */
+    public boolean pullVcardEntry(String handle) {
+        return pullVcardEntry(handle, (byte) 0, VCARD_TYPE_21);
+    }
+
+    /**
+     * Pulls single vCard object
+     *
+     * @param handle handle to the vCard which shall be pulled
+     * @param filter bit mask of the vCard fields that shall be included in the
+     *            resulting vCard
+     * @param format resulting vCard version
+     * @return <code>true</code> if request has been sent successfully;
+     *         <code>false</code> otherwise; upon completion PCE sends
+     *         {@link #EVENT_PULL_VCARD_DONE}
+     * @link #EVENT_PULL_VCARD_ERROR} in case of failure
+     */
+    public boolean pullVcardEntry(String handle, long filter, byte format) {
+        BluetoothPbapRequest req = new BluetoothPbapRequestPullVcardEntry(handle, filter, format);
+        return mSession.makeRequest(req);
+    }
+
+    public boolean setAuthResponse(String key) {
+        Log.d(TAG, " setAuthResponse key=" + key);
+        return mSession.setAuthResponse(key);
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/BluetoothPbapObexAuthenticator.java b/src/android/bluetooth/client/pbap/BluetoothPbapObexAuthenticator.java
new file mode 100644
index 0000000..9402e81
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/BluetoothPbapObexAuthenticator.java
@@ -0,0 +1,86 @@
+/*
+ * 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.pbap;
+
+import android.os.Handler;
+import android.util.Log;
+
+import javax.obex.Authenticator;
+import javax.obex.PasswordAuthentication;
+
+class BluetoothPbapObexAuthenticator implements Authenticator {
+
+    private final static String TAG = "BluetoothPbapObexAuthenticator";
+
+    private String mSessionKey;
+
+    private boolean mReplied;
+
+    private final Handler mCallback;
+
+    public BluetoothPbapObexAuthenticator(Handler callback) {
+        mCallback = callback;
+    }
+
+    public synchronized void setReply(String key) {
+        Log.d(TAG, "setReply key=" + key);
+
+        mSessionKey = key;
+        mReplied = true;
+
+        notify();
+    }
+
+    @Override
+    public PasswordAuthentication onAuthenticationChallenge(String description,
+            boolean isUserIdRequired, boolean isFullAccess) {
+        PasswordAuthentication pa = null;
+
+        mReplied = false;
+
+        Log.d(TAG, "onAuthenticationChallenge: sending request");
+        mCallback.obtainMessage(BluetoothPbapObexSession.OBEX_SESSION_AUTHENTICATION_REQUEST)
+                .sendToTarget();
+
+        synchronized (this) {
+            while (!mReplied) {
+                try {
+                    Log.v(TAG, "onAuthenticationChallenge: waiting for response");
+                    this.wait();
+                } catch (InterruptedException e) {
+                    Log.e(TAG, "Interrupted while waiting for challenge response");
+                }
+            }
+        }
+
+        if (mSessionKey != null && mSessionKey.length() != 0) {
+            Log.v(TAG, "onAuthenticationChallenge: mSessionKey=" + mSessionKey);
+            pa = new PasswordAuthentication(null, mSessionKey.getBytes());
+        } else {
+            Log.v(TAG, "onAuthenticationChallenge: mSessionKey is empty, timeout/cancel occured");
+        }
+
+        return pa;
+    }
+
+    @Override
+    public byte[] onAuthenticationResponse(byte[] userName) {
+        /* required only in case PCE challenges PSE which we don't do now */
+        return null;
+    }
+
+}
diff --git a/src/android/bluetooth/client/pbap/BluetoothPbapObexSession.java b/src/android/bluetooth/client/pbap/BluetoothPbapObexSession.java
new file mode 100644
index 0000000..f558cc4
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/BluetoothPbapObexSession.java
@@ -0,0 +1,232 @@
+/*
+ * 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.pbap;
+
+import android.os.Handler;
+import android.util.Log;
+
+import java.io.IOException;
+
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+import javax.obex.ObexTransport;
+import javax.obex.ResponseCodes;
+
+final class BluetoothPbapObexSession {
+    private static final String TAG = "BluetoothPbapObexSession";
+
+    private static final byte[] PBAP_TARGET = new byte[] {
+            0x79, 0x61, 0x35, (byte) 0xf0, (byte) 0xf0, (byte) 0xc5, 0x11, (byte) 0xd8, 0x09, 0x66,
+            0x08, 0x00, 0x20, 0x0c, (byte) 0x9a, 0x66
+    };
+
+    final static int OBEX_SESSION_CONNECTED = 100;
+    final static int OBEX_SESSION_FAILED = 101;
+    final static int OBEX_SESSION_DISCONNECTED = 102;
+    final static int OBEX_SESSION_REQUEST_COMPLETED = 103;
+    final static int OBEX_SESSION_REQUEST_FAILED = 104;
+    final static int OBEX_SESSION_AUTHENTICATION_REQUEST = 105;
+    final static int OBEX_SESSION_AUTHENTICATION_TIMEOUT = 106;
+
+    private Handler mSessionHandler;
+    private final ObexTransport mTransport;
+    private ObexClientThread mObexClientThread;
+    private BluetoothPbapObexAuthenticator mAuth = null;
+
+    public BluetoothPbapObexSession(ObexTransport transport) {
+        mTransport = transport;
+    }
+
+    public void start(Handler handler) {
+        Log.d(TAG, "start");
+        mSessionHandler = handler;
+
+        mAuth = new BluetoothPbapObexAuthenticator(mSessionHandler);
+
+        mObexClientThread = new ObexClientThread();
+        mObexClientThread.start();
+    }
+
+    public void stop() {
+        Log.d(TAG, "stop");
+
+        if (mObexClientThread != null) {
+            try {
+                mObexClientThread.interrupt();
+                mObexClientThread.join();
+                mObexClientThread = null;
+            } catch (InterruptedException e) {
+            }
+        }
+    }
+
+    public void abort() {
+        Log.d(TAG, "abort");
+
+        if (mObexClientThread != null && mObexClientThread.mRequest != null) {
+            /*
+             * since abort may block until complete GET is processed inside OBEX
+             * session, let's run it in separate thread so it won't block UI
+             */
+            (new Thread() {
+                @Override
+                public void run() {
+                    mObexClientThread.mRequest.abort();
+                }
+            }).run();
+        }
+    }
+
+    public boolean schedule(BluetoothPbapRequest request) {
+        Log.d(TAG, "schedule: " + request.getClass().getSimpleName());
+
+        if (mObexClientThread == null) {
+            Log.e(TAG, "OBEX session not started");
+            return false;
+        }
+
+        return mObexClientThread.schedule(request);
+    }
+
+    public boolean setAuthReply(String key) {
+        Log.d(TAG, "setAuthReply key=" + key);
+
+        if (mAuth == null) {
+            return false;
+        }
+
+        mAuth.setReply(key);
+
+        return true;
+    }
+
+    private class ObexClientThread extends Thread {
+
+        private static final String TAG = "ObexClientThread";
+
+        private ClientSession mClientSession;
+        private BluetoothPbapRequest mRequest;
+
+        private volatile boolean mRunning = true;
+
+        public ObexClientThread() {
+
+            mClientSession = null;
+            mRequest = null;
+        }
+
+        @Override
+        public void run() {
+            super.run();
+
+            if (!connect()) {
+                mSessionHandler.obtainMessage(OBEX_SESSION_FAILED).sendToTarget();
+                return;
+            }
+
+            mSessionHandler.obtainMessage(OBEX_SESSION_CONNECTED).sendToTarget();
+
+            while (mRunning) {
+                synchronized (this) {
+                    try {
+                        if (mRequest == null) {
+                            this.wait();
+                        }
+                    } catch (InterruptedException e) {
+                        mRunning = false;
+                        break;
+                    }
+                }
+
+                if (mRunning && mRequest != null) {
+                    try {
+                        mRequest.execute(mClientSession);
+                    } catch (IOException e) {
+                        // this will "disconnect" for cleanup
+                        mRunning = false;
+                    }
+
+                    if (mRequest.isSuccess()) {
+                        mSessionHandler.obtainMessage(OBEX_SESSION_REQUEST_COMPLETED, mRequest)
+                                .sendToTarget();
+                    } else {
+                        mSessionHandler.obtainMessage(OBEX_SESSION_REQUEST_FAILED, mRequest)
+                                .sendToTarget();
+                    }
+                }
+
+                mRequest = null;
+            }
+
+            disconnect();
+
+            mSessionHandler.obtainMessage(OBEX_SESSION_DISCONNECTED).sendToTarget();
+        }
+
+        public synchronized boolean schedule(BluetoothPbapRequest request) {
+            Log.d(TAG, "schedule: " + request.getClass().getSimpleName());
+
+            if (mRequest != null) {
+                return false;
+            }
+
+            mRequest = request;
+            notify();
+
+            return true;
+        }
+
+        private boolean connect() {
+            Log.d(TAG, "connect");
+
+            try {
+                mClientSession = new ClientSession(mTransport);
+                mClientSession.setAuthenticator(mAuth);
+            } catch (IOException e) {
+                return false;
+            }
+
+            HeaderSet hs = new HeaderSet();
+            hs.setHeader(HeaderSet.TARGET, PBAP_TARGET);
+
+            try {
+                hs = mClientSession.connect(hs);
+
+                if (hs.getResponseCode() != ResponseCodes.OBEX_HTTP_OK) {
+                    disconnect();
+                    return false;
+                }
+            } catch (IOException e) {
+                return false;
+            }
+
+            return true;
+        }
+
+        private void disconnect() {
+            Log.d(TAG, "disconnect");
+
+            if (mClientSession != null) {
+                try {
+                    mClientSession.disconnect(null);
+                    mClientSession.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/BluetoothPbapObexTransport.java b/src/android/bluetooth/client/pbap/BluetoothPbapObexTransport.java
new file mode 100644
index 0000000..98fd9db
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/BluetoothPbapObexTransport.java
@@ -0,0 +1,83 @@
+/*
+ * 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.pbap;
+
+import android.bluetooth.BluetoothSocket;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.obex.ObexTransport;
+
+class BluetoothPbapObexTransport implements ObexTransport {
+
+    private BluetoothSocket mSocket = null;
+
+    public BluetoothPbapObexTransport(BluetoothSocket rfs) {
+        super();
+        mSocket = rfs;
+    }
+
+    @Override
+    public void close() throws IOException {
+        mSocket.close();
+    }
+
+    @Override
+    public DataInputStream openDataInputStream() throws IOException {
+        return new DataInputStream(openInputStream());
+    }
+
+    @Override
+    public DataOutputStream openDataOutputStream() throws IOException {
+        return new DataOutputStream(openOutputStream());
+    }
+
+    @Override
+    public InputStream openInputStream() throws IOException {
+        return mSocket.getInputStream();
+    }
+
+    @Override
+    public OutputStream openOutputStream() throws IOException {
+        return mSocket.getOutputStream();
+    }
+
+    @Override
+    public void connect() throws IOException {
+    }
+
+    @Override
+    public void create() throws IOException {
+    }
+
+    @Override
+    public void disconnect() throws IOException {
+    }
+
+    @Override
+    public void listen() throws IOException {
+    }
+
+    public boolean isConnected() throws IOException {
+        // return true;
+        return mSocket.isConnected();
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/BluetoothPbapRequest.java b/src/android/bluetooth/client/pbap/BluetoothPbapRequest.java
new file mode 100644
index 0000000..0974c75
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/BluetoothPbapRequest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.pbap;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.obex.ClientOperation;
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+import javax.obex.ResponseCodes;
+
+abstract class BluetoothPbapRequest {
+
+    private static final String TAG = "BluetoothPbapRequest";
+
+    protected static final byte OAP_TAGID_ORDER = 0x01;
+    protected static final byte OAP_TAGID_SEARCH_VALUE = 0x02;
+    protected static final byte OAP_TAGID_SEARCH_ATTRIBUTE = 0x03;
+    protected static final byte OAP_TAGID_MAX_LIST_COUNT = 0x04;
+    protected static final byte OAP_TAGID_LIST_START_OFFSET = 0x05;
+    protected static final byte OAP_TAGID_FILTER = 0x06;
+    protected static final byte OAP_TAGID_FORMAT = 0x07;
+    protected static final byte OAP_TAGID_PHONEBOOK_SIZE = 0x08;
+    protected static final byte OAP_TAGID_NEW_MISSED_CALLS = 0x09;
+
+    protected HeaderSet mHeaderSet;
+
+    protected int mResponseCode;
+
+    private boolean mAborted = false;
+
+    private ClientOperation mOp = null;
+
+    public BluetoothPbapRequest() {
+        mHeaderSet = new HeaderSet();
+    }
+
+    final public boolean isSuccess() {
+        return (mResponseCode == ResponseCodes.OBEX_HTTP_OK);
+    }
+
+    public void execute(ClientSession session) throws IOException {
+        Log.v(TAG, "execute");
+
+        /* in case request is aborted before can be executed */
+        if (mAborted) {
+            mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+            return;
+        }
+
+        try {
+            mOp = (ClientOperation) session.get(mHeaderSet);
+
+            /* make sure final flag for GET is used (PBAP spec 6.2.2) */
+            mOp.setGetFinalFlag(true);
+
+            /*
+             * this will trigger ClientOperation to use non-buffered stream so
+             * we can abort operation
+             */
+            mOp.continueOperation(true, false);
+
+            readResponseHeaders(mOp.getReceivedHeader());
+
+            InputStream is = mOp.openInputStream();
+            readResponse(is);
+            is.close();
+
+            mOp.close();
+
+            mResponseCode = mOp.getResponseCode();
+
+            Log.d(TAG, "mResponseCode=" + mResponseCode);
+
+            checkResponseCode(mResponseCode);
+        } catch (IOException e) {
+            Log.e(TAG, "IOException occured when processing request", e);
+            mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+
+            throw e;
+        }
+    }
+
+    public void abort() {
+        mAborted = true;
+
+        if (mOp != null) {
+            try {
+                mOp.abort();
+            } catch (IOException e) {
+                Log.e(TAG, "Exception occured when trying to abort", e);
+            }
+        }
+    }
+
+    protected void readResponse(InputStream stream) throws IOException {
+        Log.v(TAG, "readResponse");
+
+        /* nothing here by default */
+    }
+
+    protected void readResponseHeaders(HeaderSet headerset) {
+        Log.v(TAG, "readResponseHeaders");
+
+        /* nothing here by dafault */
+    }
+
+    protected void checkResponseCode(int responseCode) throws IOException {
+        Log.v(TAG, "checkResponseCode");
+
+        /* nothing here by dafault */
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/BluetoothPbapRequestPullPhoneBook.java b/src/android/bluetooth/client/pbap/BluetoothPbapRequestPullPhoneBook.java
new file mode 100644
index 0000000..15954b1
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/BluetoothPbapRequestPullPhoneBook.java
@@ -0,0 +1,115 @@
+/*
+ * 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.pbap;
+
+import android.util.Log;
+
+import com.android.vcard.VCardEntry;
+import android.bluetooth.client.pbap.utils.ObexAppParameters;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+import javax.obex.HeaderSet;
+
+final class BluetoothPbapRequestPullPhoneBook extends BluetoothPbapRequest {
+
+    private static final String TAG = "BluetoothPbapRequestPullPhoneBook";
+
+    private static final String TYPE = "x-bt/phonebook";
+
+    private BluetoothPbapVcardList mResponse;
+
+    private int mNewMissedCalls = -1;
+
+    private final byte mFormat;
+
+    public BluetoothPbapRequestPullPhoneBook(String pbName, long filter, byte format,
+            int maxListCount, int listStartOffset) {
+
+        if (maxListCount < 0 || maxListCount > 65535) {
+            throw new IllegalArgumentException("maxListCount should be [0..65535]");
+        }
+
+        if (listStartOffset < 0 || listStartOffset > 65535) {
+            throw new IllegalArgumentException("listStartOffset should be [0..65535]");
+        }
+
+        mHeaderSet.setHeader(HeaderSet.NAME, pbName);
+
+        mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
+
+        ObexAppParameters oap = new ObexAppParameters();
+
+        /* make sure format is one of allowed values */
+        if (format != BluetoothPbapClient.VCARD_TYPE_21
+                && format != BluetoothPbapClient.VCARD_TYPE_30) {
+            format = BluetoothPbapClient.VCARD_TYPE_21;
+        }
+
+        if (filter != 0) {
+            oap.add(OAP_TAGID_FILTER, filter);
+        }
+
+        oap.add(OAP_TAGID_FORMAT, format);
+
+        /*
+         * maxListCount is a special case which is handled in
+         * BluetoothPbapRequestPullPhoneBookSize
+         */
+        if (maxListCount > 0) {
+            oap.add(OAP_TAGID_MAX_LIST_COUNT, (short) maxListCount);
+        } else {
+            oap.add(OAP_TAGID_MAX_LIST_COUNT, (short) 65535);
+        }
+
+        if (listStartOffset > 0) {
+            oap.add(OAP_TAGID_LIST_START_OFFSET, (short) listStartOffset);
+        }
+
+        oap.addToHeaderSet(mHeaderSet);
+
+        mFormat = format;
+    }
+
+    @Override
+    protected void readResponse(InputStream stream) throws IOException {
+        Log.v(TAG, "readResponse");
+
+        mResponse = new BluetoothPbapVcardList(stream, mFormat);
+    }
+
+    @Override
+    protected void readResponseHeaders(HeaderSet headerset) {
+        Log.v(TAG, "readResponse");
+
+        ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
+
+        if (oap.exists(OAP_TAGID_NEW_MISSED_CALLS)) {
+            mNewMissedCalls = oap.getByte(OAP_TAGID_NEW_MISSED_CALLS);
+        }
+    }
+
+    public ArrayList<VCardEntry> getList() {
+        return mResponse.getList();
+    }
+
+    public int getNewMissedCalls() {
+        return mNewMissedCalls;
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/BluetoothPbapRequestPullPhoneBookSize.java b/src/android/bluetooth/client/pbap/BluetoothPbapRequestPullPhoneBookSize.java
new file mode 100644
index 0000000..664f081
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/BluetoothPbapRequestPullPhoneBookSize.java
@@ -0,0 +1,55 @@
+/*
+ * 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.pbap;
+
+import android.util.Log;
+
+import android.bluetooth.client.pbap.utils.ObexAppParameters;
+
+import javax.obex.HeaderSet;
+
+class BluetoothPbapRequestPullPhoneBookSize extends BluetoothPbapRequest {
+
+    private static final String TAG = "BluetoothPbapRequestPullPhoneBookSize";
+
+    private static final String TYPE = "x-bt/phonebook";
+
+    private int mSize;
+
+    public BluetoothPbapRequestPullPhoneBookSize(String pbName) {
+        mHeaderSet.setHeader(HeaderSet.NAME, pbName);
+
+        mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
+
+        ObexAppParameters oap = new ObexAppParameters();
+        oap.add(OAP_TAGID_MAX_LIST_COUNT, (short) 0);
+        oap.addToHeaderSet(mHeaderSet);
+    }
+
+    @Override
+    protected void readResponseHeaders(HeaderSet headerset) {
+        Log.v(TAG, "readResponseHeaders");
+
+        ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
+
+        mSize = oap.getShort(OAP_TAGID_PHONEBOOK_SIZE);
+    }
+
+    public int getSize() {
+        return mSize;
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/BluetoothPbapRequestPullVcardEntry.java b/src/android/bluetooth/client/pbap/BluetoothPbapRequestPullVcardEntry.java
new file mode 100644
index 0000000..009ec15
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/BluetoothPbapRequestPullVcardEntry.java
@@ -0,0 +1,85 @@
+/*
+ * 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.pbap;
+
+import android.util.Log;
+
+import com.android.vcard.VCardEntry;
+import android.bluetooth.client.pbap.utils.ObexAppParameters;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.obex.HeaderSet;
+import javax.obex.ResponseCodes;
+
+final class BluetoothPbapRequestPullVcardEntry extends BluetoothPbapRequest {
+
+    private static final String TAG = "BluetoothPbapRequestPullVcardEntry";
+
+    private static final String TYPE = "x-bt/vcard";
+
+    private BluetoothPbapVcardList mResponse;
+
+    private final byte mFormat;
+
+    public BluetoothPbapRequestPullVcardEntry(String handle, long filter, byte format) {
+        mHeaderSet.setHeader(HeaderSet.NAME, handle);
+
+        mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
+
+        /* make sure format is one of allowed values */
+        if (format != BluetoothPbapClient.VCARD_TYPE_21
+                && format != BluetoothPbapClient.VCARD_TYPE_30) {
+            format = BluetoothPbapClient.VCARD_TYPE_21;
+        }
+
+        ObexAppParameters oap = new ObexAppParameters();
+
+        if (filter != 0) {
+            oap.add(OAP_TAGID_FILTER, filter);
+        }
+
+        oap.add(OAP_TAGID_FORMAT, format);
+        oap.addToHeaderSet(mHeaderSet);
+
+        mFormat = format;
+    }
+
+    @Override
+    protected void readResponse(InputStream stream) throws IOException {
+        Log.v(TAG, "readResponse");
+
+        mResponse = new BluetoothPbapVcardList(stream, mFormat);
+    }
+    @Override
+    protected void checkResponseCode(int responseCode) throws IOException {
+        Log.v(TAG, "checkResponseCode");
+
+        if (mResponse.getCount() == 0) {
+            if (responseCode != ResponseCodes.OBEX_HTTP_NOT_FOUND) {
+                throw new IOException("Invalid response received");
+            } else {
+                Log.v(TAG, "Vcard Entry not found");
+            }
+        }
+    }
+
+    public VCardEntry getVcard() {
+        return mResponse.getFirst();
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/BluetoothPbapRequestPullVcardListing.java b/src/android/bluetooth/client/pbap/BluetoothPbapRequestPullVcardListing.java
new file mode 100644
index 0000000..5f042ba
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/BluetoothPbapRequestPullVcardListing.java
@@ -0,0 +1,110 @@
+/*
+ * 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.pbap;
+
+import android.util.Log;
+
+import android.bluetooth.client.pbap.utils.ObexAppParameters;
+import android.bluetooth.client.pbap.BluetoothPbapVcardListing;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+import javax.obex.HeaderSet;
+
+final class BluetoothPbapRequestPullVcardListing extends BluetoothPbapRequest {
+
+    private static final String TAG = "BluetoothPbapRequestPullVcardListing";
+
+    private static final String TYPE = "x-bt/vcard-listing";
+
+    private BluetoothPbapVcardListing mResponse = null;
+
+    private int mNewMissedCalls = -1;
+
+    public BluetoothPbapRequestPullVcardListing(String folder, byte order, byte searchAttr,
+            String searchVal, int maxListCount, int listStartOffset) {
+
+        if (maxListCount < 0 || maxListCount > 65535) {
+            throw new IllegalArgumentException("maxListCount should be [0..65535]");
+        }
+
+        if (listStartOffset < 0 || listStartOffset > 65535) {
+            throw new IllegalArgumentException("listStartOffset should be [0..65535]");
+        }
+
+        if (folder == null) {
+            folder = "";
+        }
+
+        mHeaderSet.setHeader(HeaderSet.NAME, folder);
+
+        mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
+
+        ObexAppParameters oap = new ObexAppParameters();
+
+        if (order >= 0) {
+            oap.add(OAP_TAGID_ORDER, order);
+        }
+
+        if (searchVal != null) {
+            oap.add(OAP_TAGID_SEARCH_ATTRIBUTE, searchAttr);
+            oap.add(OAP_TAGID_SEARCH_VALUE, searchVal);
+        }
+
+        /*
+         * maxListCount is a special case which is handled in
+         * BluetoothPbapRequestPullVcardListingSize
+         */
+        if (maxListCount > 0) {
+            oap.add(OAP_TAGID_MAX_LIST_COUNT, (short) maxListCount);
+        }
+
+        if (listStartOffset > 0) {
+            oap.add(OAP_TAGID_LIST_START_OFFSET, (short) listStartOffset);
+        }
+
+        oap.addToHeaderSet(mHeaderSet);
+    }
+
+    @Override
+    protected void readResponse(InputStream stream) throws IOException {
+        Log.v(TAG, "readResponse");
+
+        mResponse = new BluetoothPbapVcardListing(stream);
+    }
+
+    @Override
+    protected void readResponseHeaders(HeaderSet headerset) {
+        Log.v(TAG, "readResponseHeaders");
+
+        ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
+
+        if (oap.exists(OAP_TAGID_NEW_MISSED_CALLS)) {
+            mNewMissedCalls = oap.getByte(OAP_TAGID_NEW_MISSED_CALLS);
+        }
+    }
+
+    public ArrayList<BluetoothPbapCard> getList() {
+        return mResponse.getList();
+    }
+
+    public int getNewMissedCalls() {
+        return mNewMissedCalls;
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/BluetoothPbapRequestPullVcardListingSize.java b/src/android/bluetooth/client/pbap/BluetoothPbapRequestPullVcardListingSize.java
new file mode 100644
index 0000000..ab276c3
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/BluetoothPbapRequestPullVcardListingSize.java
@@ -0,0 +1,55 @@
+/*
+ * 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.pbap;
+
+import android.util.Log;
+
+import android.bluetooth.client.pbap.utils.ObexAppParameters;
+
+import javax.obex.HeaderSet;
+
+class BluetoothPbapRequestPullVcardListingSize extends BluetoothPbapRequest {
+
+    private static final String TAG = "BluetoothPbapRequestPullVcardListingSize";
+
+    private static final String TYPE = "x-bt/vcard-listing";
+
+    private int mSize;
+
+    public BluetoothPbapRequestPullVcardListingSize(String folder) {
+        mHeaderSet.setHeader(HeaderSet.NAME, folder);
+
+        mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
+
+        ObexAppParameters oap = new ObexAppParameters();
+        oap.add(OAP_TAGID_MAX_LIST_COUNT, (short) 0);
+        oap.addToHeaderSet(mHeaderSet);
+    }
+
+    @Override
+    protected void readResponseHeaders(HeaderSet headerset) {
+        Log.v(TAG, "readResponseHeaders");
+
+        ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
+
+        mSize = oap.getShort(OAP_TAGID_PHONEBOOK_SIZE);
+    }
+
+    public int getSize() {
+        return mSize;
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/BluetoothPbapRequestSetPath.java b/src/android/bluetooth/client/pbap/BluetoothPbapRequestSetPath.java
new file mode 100644
index 0000000..60f5244
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/BluetoothPbapRequestSetPath.java
@@ -0,0 +1,73 @@
+/*
+ * 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.pbap;
+
+import android.util.Log;
+
+import java.io.IOException;
+
+import javax.obex.ClientSession;
+import javax.obex.HeaderSet;
+import javax.obex.ResponseCodes;
+
+final class BluetoothPbapRequestSetPath extends BluetoothPbapRequest {
+
+    private final static String TAG = "BluetoothPbapRequestSetPath";
+
+    private enum SetPathDir {
+        ROOT, UP, DOWN
+    };
+
+    private SetPathDir mDir;
+
+    public BluetoothPbapRequestSetPath(String name) {
+        mDir = SetPathDir.DOWN;
+        mHeaderSet.setHeader(HeaderSet.NAME, name);
+    }
+
+    public BluetoothPbapRequestSetPath(boolean goUp) {
+        mHeaderSet.setEmptyNameHeader();
+        if (goUp) {
+            mDir = SetPathDir.UP;
+        } else {
+            mDir = SetPathDir.ROOT;
+        }
+    }
+
+    @Override
+    public void execute(ClientSession session) {
+        Log.v(TAG, "execute");
+
+        HeaderSet hs = null;
+
+        try {
+            switch (mDir) {
+                case ROOT:
+                case DOWN:
+                    hs = session.setPath(mHeaderSet, false, false);
+                    break;
+                case UP:
+                    hs = session.setPath(mHeaderSet, true, false);
+                    break;
+            }
+
+            mResponseCode = hs.getResponseCode();
+        } catch (IOException e) {
+            mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
+        }
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/BluetoothPbapSession.java b/src/android/bluetooth/client/pbap/BluetoothPbapSession.java
new file mode 100644
index 0000000..70e0ac8
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/BluetoothPbapSession.java
@@ -0,0 +1,330 @@
+/*
+ * 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.pbap;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothSocket;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.Process;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.UUID;
+
+class BluetoothPbapSession implements Callback {
+    private static final String TAG = "android.bluetooth.client.pbap.BluetoothPbapSession";
+
+    /* local use only */
+    private static final int RFCOMM_CONNECTED = 1;
+    private static final int RFCOMM_FAILED = 2;
+
+    /* to BluetoothPbapClient */
+    public static final int REQUEST_COMPLETED = 3;
+    public static final int REQUEST_FAILED = 4;
+    public static final int SESSION_CONNECTING = 5;
+    public static final int SESSION_CONNECTED = 6;
+    public static final int SESSION_DISCONNECTED = 7;
+    public static final int AUTH_REQUESTED = 8;
+    public static final int AUTH_TIMEOUT = 9;
+
+    public static final int ACTION_LISTING = 14;
+    public static final int ACTION_VCARD = 15;
+    public static final int ACTION_PHONEBOOK_SIZE = 16;
+
+    private static final String PBAP_UUID =
+            "0000112f-0000-1000-8000-00805f9b34fb";
+
+    private final BluetoothAdapter mAdapter;
+    private final BluetoothDevice mDevice;
+
+    private final Handler mParentHandler;
+
+    private final HandlerThread mHandlerThread;
+    private final Handler mSessionHandler;
+
+    private RfcommConnectThread mConnectThread;
+    private BluetoothPbapObexTransport mTransport;
+
+    private BluetoothPbapObexSession mObexSession;
+
+    private BluetoothPbapRequest mPendingRequest = null;
+
+    public BluetoothPbapSession(BluetoothDevice device, Handler handler) {
+
+        mAdapter = BluetoothAdapter.getDefaultAdapter();
+        if (mAdapter == null) {
+            throw new NullPointerException("No Bluetooth adapter in the system");
+        }
+
+        mDevice = device;
+        mParentHandler = handler;
+        mConnectThread = null;
+        mTransport = null;
+        mObexSession = null;
+
+        mHandlerThread = new HandlerThread("PBAP session handler",
+                Process.THREAD_PRIORITY_BACKGROUND);
+        mHandlerThread.start();
+        mSessionHandler = new Handler(mHandlerThread.getLooper(), this);
+    }
+
+    @Override
+    public boolean handleMessage(Message msg) {
+        Log.d(TAG, "Handler: msg: " + msg.what);
+
+        switch (msg.what) {
+            case RFCOMM_FAILED:
+                mConnectThread = null;
+
+                mParentHandler.obtainMessage(SESSION_DISCONNECTED).sendToTarget();
+
+                if (mPendingRequest != null) {
+                    mParentHandler.obtainMessage(REQUEST_FAILED, mPendingRequest).sendToTarget();
+                    mPendingRequest = null;
+                }
+                break;
+
+            case RFCOMM_CONNECTED:
+                mConnectThread = null;
+                mTransport = (BluetoothPbapObexTransport) msg.obj;
+                startObexSession();
+                break;
+
+            case BluetoothPbapObexSession.OBEX_SESSION_FAILED:
+                stopObexSession();
+
+                mParentHandler.obtainMessage(SESSION_DISCONNECTED).sendToTarget();
+
+                if (mPendingRequest != null) {
+                    mParentHandler.obtainMessage(REQUEST_FAILED, mPendingRequest).sendToTarget();
+                    mPendingRequest = null;
+                }
+                break;
+
+            case BluetoothPbapObexSession.OBEX_SESSION_CONNECTED:
+                mParentHandler.obtainMessage(SESSION_CONNECTED).sendToTarget();
+
+                if (mPendingRequest != null) {
+                    mObexSession.schedule(mPendingRequest);
+                    mPendingRequest = null;
+                }
+                break;
+
+            case BluetoothPbapObexSession.OBEX_SESSION_DISCONNECTED:
+                mParentHandler.obtainMessage(SESSION_DISCONNECTED).sendToTarget();
+                stopRfcomm();
+                break;
+
+            case BluetoothPbapObexSession.OBEX_SESSION_REQUEST_COMPLETED:
+                /* send to parent, process there */
+                mParentHandler.obtainMessage(REQUEST_COMPLETED, msg.obj).sendToTarget();
+                break;
+
+            case BluetoothPbapObexSession.OBEX_SESSION_REQUEST_FAILED:
+                /* send to parent, process there */
+                mParentHandler.obtainMessage(REQUEST_FAILED, msg.obj).sendToTarget();
+                break;
+
+            case BluetoothPbapObexSession.OBEX_SESSION_AUTHENTICATION_REQUEST:
+                /* send to parent, process there */
+                mParentHandler.obtainMessage(AUTH_REQUESTED).sendToTarget();
+
+                mSessionHandler
+                        .sendMessageDelayed(
+                                mSessionHandler
+                                        .obtainMessage(BluetoothPbapObexSession.OBEX_SESSION_AUTHENTICATION_TIMEOUT),
+                                30000);
+                break;
+
+            case BluetoothPbapObexSession.OBEX_SESSION_AUTHENTICATION_TIMEOUT:
+                /* stop authentication */
+                setAuthResponse(null);
+
+                mParentHandler.obtainMessage(AUTH_TIMEOUT).sendToTarget();
+                break;
+
+            default:
+                return false;
+        }
+
+        return true;
+    }
+
+    public void start() {
+        Log.d(TAG, "start");
+
+        startRfcomm();
+    }
+
+    public void stop() {
+        Log.d(TAG, "Stop");
+
+        stopObexSession();
+        stopRfcomm();
+    }
+
+    public void abort() {
+        Log.d(TAG, "abort");
+
+        /* fail pending request immediately */
+        if (mPendingRequest != null) {
+            mParentHandler.obtainMessage(REQUEST_FAILED, mPendingRequest).sendToTarget();
+            mPendingRequest = null;
+        }
+
+        if (mObexSession != null) {
+            mObexSession.abort();
+        }
+    }
+
+    public boolean makeRequest(BluetoothPbapRequest request) {
+        Log.v(TAG, "makeRequest: " + request.getClass().getSimpleName());
+
+        if (mPendingRequest != null) {
+            Log.w(TAG, "makeRequest: request already queued, exiting");
+            return false;
+        }
+
+        if (mObexSession == null) {
+            mPendingRequest = request;
+
+            /*
+             * since there is no pending request and no session it's safe to
+             * assume that RFCOMM does not exist either and we should start
+             * connecting it
+             */
+            startRfcomm();
+
+            return true;
+        }
+
+        return mObexSession.schedule(request);
+    }
+
+    public boolean setAuthResponse(String key) {
+        Log.d(TAG, "setAuthResponse key=" + key);
+
+        mSessionHandler
+                .removeMessages(BluetoothPbapObexSession.OBEX_SESSION_AUTHENTICATION_TIMEOUT);
+
+        /* does not make sense to set auth response when OBEX session is down */
+        if (mObexSession == null) {
+            return false;
+        }
+
+        return mObexSession.setAuthReply(key);
+    }
+
+    private void startRfcomm() {
+        Log.d(TAG, "startRfcomm");
+
+        if (mConnectThread == null && mObexSession == null) {
+            mParentHandler.obtainMessage(SESSION_CONNECTING).sendToTarget();
+
+            mConnectThread = new RfcommConnectThread();
+            mConnectThread.start();
+        }
+
+        /*
+         * don't care if mConnectThread is not null - it means RFCOMM is being
+         * connected anyway
+         */
+    }
+
+    private void stopRfcomm() {
+        Log.d(TAG, "stopRfcomm");
+
+        if (mConnectThread != null) {
+            try {
+                mConnectThread.join();
+            } catch (InterruptedException e) {
+            }
+
+            mConnectThread = null;
+        }
+
+        if (mTransport != null) {
+            try {
+                mTransport.close();
+            } catch (IOException e) {
+            }
+
+            mTransport = null;
+        }
+    }
+
+    private void startObexSession() {
+        Log.d(TAG, "startObexSession");
+
+        mObexSession = new BluetoothPbapObexSession(mTransport);
+        mObexSession.start(mSessionHandler);
+    }
+
+    private void stopObexSession() {
+        Log.d(TAG, "stopObexSession");
+
+        if (mObexSession != null) {
+            mObexSession.stop();
+            mObexSession = null;
+        }
+    }
+
+    private class RfcommConnectThread extends Thread {
+        private static final String TAG = "RfcommConnectThread";
+
+        private BluetoothSocket mSocket;
+
+        public RfcommConnectThread() {
+            super("RfcommConnectThread");
+        }
+
+        @Override
+        public void run() {
+            if (mAdapter.isDiscovering()) {
+                mAdapter.cancelDiscovery();
+            }
+
+            try {
+                mSocket = mDevice.createRfcommSocketToServiceRecord(UUID.fromString(PBAP_UUID));
+                mSocket.connect();
+
+                BluetoothPbapObexTransport transport;
+                transport = new BluetoothPbapObexTransport(mSocket);
+
+                mSessionHandler.obtainMessage(RFCOMM_CONNECTED, transport).sendToTarget();
+            } catch (IOException e) {
+                closeSocket();
+                mSessionHandler.obtainMessage(RFCOMM_FAILED).sendToTarget();
+            }
+
+        }
+
+        private void closeSocket() {
+            try {
+                if (mSocket != null) {
+                    mSocket.close();
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Error when closing socket", e);
+            }
+        }
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/BluetoothPbapVcardList.java b/src/android/bluetooth/client/pbap/BluetoothPbapVcardList.java
new file mode 100644
index 0000000..8e23e1a
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/BluetoothPbapVcardList.java
@@ -0,0 +1,91 @@
+/*
+ * 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.pbap;
+
+import com.android.vcard.VCardEntry;
+import com.android.vcard.VCardEntryConstructor;
+import com.android.vcard.VCardEntryCounter;
+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 java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+class BluetoothPbapVcardList {
+
+    private final ArrayList<VCardEntry> mCards = new ArrayList<VCardEntry>();
+
+    class CardEntryHandler implements VCardEntryHandler {
+        @Override
+        public void onStart() {
+        }
+
+        @Override
+        public void onEntryCreated(VCardEntry entry) {
+            mCards.add(entry);
+        }
+
+        @Override
+        public void onEnd() {
+        }
+    }
+
+    public BluetoothPbapVcardList(InputStream in, byte format) throws IOException {
+        parse(in, format);
+    }
+
+    private void parse(InputStream in, byte format) throws IOException {
+        VCardParser parser;
+
+        if (format == BluetoothPbapClient.VCARD_TYPE_30) {
+            parser = new VCardParser_V30();
+        } else {
+            parser = new VCardParser_V21();
+        }
+
+        VCardEntryConstructor constructor = new VCardEntryConstructor();
+        VCardEntryCounter counter = new VCardEntryCounter();
+        CardEntryHandler handler = new CardEntryHandler();
+
+        constructor.addEntryHandler(handler);
+
+        parser.addInterpreter(constructor);
+        parser.addInterpreter(counter);
+
+        try {
+            parser.parse(in);
+        } catch (VCardException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public int getCount() {
+        return mCards.size();
+    }
+
+    public ArrayList<VCardEntry> getList() {
+        return mCards;
+    }
+
+    public VCardEntry getFirst() {
+        return mCards.get(0);
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/BluetoothPbapVcardListing.java b/src/android/bluetooth/client/pbap/BluetoothPbapVcardListing.java
new file mode 100644
index 0000000..d963c94
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/BluetoothPbapVcardListing.java
@@ -0,0 +1,66 @@
+/*
+ * 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.pbap;
+
+import android.util.Log;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+class BluetoothPbapVcardListing {
+
+    private static final String TAG = "BluetoothPbapVcardListing";
+
+    ArrayList<BluetoothPbapCard> mCards = new ArrayList<BluetoothPbapCard>();
+
+    public BluetoothPbapVcardListing(InputStream in) throws IOException {
+        parse(in);
+    }
+
+    private void parse(InputStream in) throws IOException {
+        XmlPullParser parser = Xml.newPullParser();
+
+        try {
+            parser.setInput(in, "UTF-8");
+
+            int eventType = parser.getEventType();
+
+            while (eventType != XmlPullParser.END_DOCUMENT) {
+
+                if (eventType == XmlPullParser.START_TAG && parser.getName().equals("card")) {
+                    BluetoothPbapCard card = new BluetoothPbapCard(
+                            parser.getAttributeValue(null, "handle"),
+                            parser.getAttributeValue(null, "name"));
+                    mCards.add(card);
+                }
+
+                eventType = parser.next();
+            }
+        } catch (XmlPullParserException e) {
+            Log.e(TAG, "XML parser error when parsing XML", e);
+        }
+    }
+
+    public ArrayList<BluetoothPbapCard> getList() {
+        return mCards;
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/utils/BmsgTokenizer.java b/src/android/bluetooth/client/pbap/utils/BmsgTokenizer.java
new file mode 100644
index 0000000..cf138c9
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/utils/BmsgTokenizer.java
@@ -0,0 +1,108 @@
+/*
+ * 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.pbap.utils;
+
+import android.util.Log;
+
+import java.text.ParseException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class BmsgTokenizer {
+
+    private final String mStr;
+
+    private final Matcher mMatcher;
+
+    private int mPos = 0;
+
+    private final int mOffset;
+
+    static public class Property {
+        public final String name;
+        public final String value;
+
+        public Property(String name, String value) {
+            if (name == null || value == null) {
+                throw new IllegalArgumentException();
+            }
+
+            this.name = name;
+            this.value = value;
+
+            Log.v("BMSG >> ", toString());
+        }
+
+        @Override
+        public String toString() {
+            return name + ":" + value;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            return ((o instanceof Property) && ((Property) o).name.equals(name) && ((Property) o).value
+                    .equals(value));
+        }
+    };
+
+    public BmsgTokenizer(String str) {
+        this(str, 0);
+    }
+
+    public BmsgTokenizer(String str, int offset) {
+        mStr = str;
+        mOffset = offset;
+        mMatcher = Pattern.compile("(([^:]*):(.*))?\r\n").matcher(str);
+        mPos = mMatcher.regionStart();
+    }
+
+    public Property next(boolean alwaysReturn) throws ParseException {
+        boolean found = false;
+
+        do {
+            mMatcher.region(mPos, mMatcher.regionEnd());
+
+            if (!mMatcher.lookingAt()) {
+                if (alwaysReturn) {
+                    return null;
+                }
+
+                throw new ParseException("Property or empty line expected", pos());
+            }
+
+            mPos = mMatcher.end();
+
+            if (mMatcher.group(1) != null) {
+                found = true;
+            }
+        } while (!found);
+
+        return new Property(mMatcher.group(2), mMatcher.group(3));
+    }
+
+    public Property next() throws ParseException {
+        return next(false);
+    }
+
+    public String remaining() {
+        return mStr.substring(mPos);
+    }
+
+    public int pos() {
+        return mPos + mOffset;
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/utils/ObexAppParameters.java b/src/android/bluetooth/client/pbap/utils/ObexAppParameters.java
new file mode 100644
index 0000000..d70d1e4
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/utils/ObexAppParameters.java
@@ -0,0 +1,182 @@
+/*
+ * 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.pbap.utils;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.obex.HeaderSet;
+
+public final class ObexAppParameters {
+
+    private final HashMap<Byte, byte[]> mParams;
+
+    public ObexAppParameters() {
+        mParams = new HashMap<Byte, byte[]>();
+    }
+
+    public ObexAppParameters(byte[] raw) {
+        mParams = new HashMap<Byte, byte[]>();
+
+        if (raw != null) {
+            for (int i = 0; i < raw.length;) {
+                if (raw.length - i < 2) {
+                    break;
+                }
+
+                byte tag = raw[i++];
+                byte len = raw[i++];
+
+                if (raw.length - i - len < 0) {
+                    break;
+                }
+
+                byte[] val = new byte[len];
+
+                System.arraycopy(raw, i, val, 0, len);
+                this.add(tag, val);
+
+                i += len;
+            }
+        }
+    }
+
+    public static ObexAppParameters fromHeaderSet(HeaderSet headerset) {
+        try {
+            byte[] raw = (byte[]) headerset.getHeader(HeaderSet.APPLICATION_PARAMETER);
+            return new ObexAppParameters(raw);
+        } catch (IOException e) {
+            // won't happen
+        }
+
+        return null;
+    }
+
+    public byte[] getHeader() {
+        int length = 0;
+
+        for (Map.Entry<Byte, byte[]> entry : mParams.entrySet()) {
+            length += (entry.getValue().length + 2);
+        }
+
+        byte[] ret = new byte[length];
+
+        int idx = 0;
+        for (Map.Entry<Byte, byte[]> entry : mParams.entrySet()) {
+            length = entry.getValue().length;
+
+            ret[idx++] = entry.getKey();
+            ret[idx++] = (byte) length;
+            System.arraycopy(entry.getValue(), 0, ret, idx, length);
+            idx += length;
+        }
+
+        return ret;
+    }
+
+    public void addToHeaderSet(HeaderSet headerset) {
+        if (mParams.size() > 0) {
+            headerset.setHeader(HeaderSet.APPLICATION_PARAMETER, getHeader());
+        }
+    }
+
+    public boolean exists(byte tag) {
+        return mParams.containsKey(tag);
+    }
+
+    public void add(byte tag, byte val) {
+        byte[] bval = ByteBuffer.allocate(1).put(val).array();
+        mParams.put(tag, bval);
+    }
+
+    public void add(byte tag, short val) {
+        byte[] bval = ByteBuffer.allocate(2).putShort(val).array();
+        mParams.put(tag, bval);
+    }
+
+    public void add(byte tag, int val) {
+        byte[] bval = ByteBuffer.allocate(4).putInt(val).array();
+        mParams.put(tag, bval);
+    }
+
+    public void add(byte tag, long val) {
+        byte[] bval = ByteBuffer.allocate(8).putLong(val).array();
+        mParams.put(tag, bval);
+    }
+
+    public void add(byte tag, String val) {
+        byte[] bval = val.getBytes();
+        mParams.put(tag, bval);
+    }
+
+    public void add(byte tag, byte[] bval) {
+        mParams.put(tag, bval);
+    }
+
+    public byte getByte(byte tag) {
+        byte[] bval = mParams.get(tag);
+
+        if (bval == null || bval.length < 1) {
+            return 0;
+        }
+
+        return ByteBuffer.wrap(bval).get();
+    }
+
+    public short getShort(byte tag) {
+        byte[] bval = mParams.get(tag);
+
+        if (bval == null || bval.length < 2) {
+            return 0;
+        }
+
+        return ByteBuffer.wrap(bval).getShort();
+    }
+
+    public int getInt(byte tag) {
+        byte[] bval = mParams.get(tag);
+
+        if (bval == null || bval.length < 4) {
+            return 0;
+        }
+
+        return ByteBuffer.wrap(bval).getInt();
+    }
+
+    public String getString(byte tag) {
+        byte[] bval = mParams.get(tag);
+
+        if (bval == null) {
+            return null;
+        }
+
+        return new String(bval);
+    }
+
+    public byte[] getByteArray(byte tag) {
+        byte[] bval = mParams.get(tag);
+
+        return bval;
+    }
+
+    @Override
+    public String toString() {
+        return mParams.toString();
+    }
+}
diff --git a/src/android/bluetooth/client/pbap/utils/ObexTime.java b/src/android/bluetooth/client/pbap/utils/ObexTime.java
new file mode 100644
index 0000000..74bc2ab
--- /dev/null
+++ b/src/android/bluetooth/client/pbap/utils/ObexTime.java
@@ -0,0 +1,101 @@
+/*
+ * 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.pbap.utils;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class ObexTime {
+
+    private Date mDate;
+
+    public ObexTime(String time) {
+        /*
+         * match OBEX time string: YYYYMMDDTHHMMSS with optional UTF offset
+         * +/-hhmm
+         */
+        Pattern p = Pattern
+                .compile("(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})(\\d{2})(([+-])(\\d{2})(\\d{2}))?");
+        Matcher m = p.matcher(time);
+
+        if (m.matches()) {
+
+            /*
+             * matched groups are numberes as follows: YYYY MM DD T HH MM SS +
+             * hh mm ^^^^ ^^ ^^ ^^ ^^ ^^ ^ ^^ ^^ 1 2 3 4 5 6 8 9 10 all groups
+             * are guaranteed to be numeric so conversion will always succeed
+             * (except group 8 which is either + or -)
+             */
+
+            Calendar cal = Calendar.getInstance();
+            cal.set(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)) - 1,
+                    Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)),
+                    Integer.parseInt(m.group(5)), Integer.parseInt(m.group(6)));
+
+            /*
+             * if 7th group is matched then we have UTC offset information
+             * included
+             */
+            if (m.group(7) != null) {
+                int ohh = Integer.parseInt(m.group(9));
+                int omm = Integer.parseInt(m.group(10));
+
+                /* time zone offset is specified in miliseconds */
+                int offset = (ohh * 60 + omm) * 60 * 1000;
+
+                if (m.group(8).equals("-")) {
+                    offset = -offset;
+                }
+
+                TimeZone tz = TimeZone.getTimeZone("UTC");
+                tz.setRawOffset(offset);
+
+                cal.setTimeZone(tz);
+            }
+
+            mDate = cal.getTime();
+        }
+    }
+
+    public ObexTime(Date date) {
+        mDate = date;
+    }
+
+    public Date getTime() {
+        return mDate;
+    }
+
+    @Override
+    public String toString() {
+        if (mDate == null) {
+            return null;
+        }
+
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(mDate);
+
+        /* note that months are numbered stating from 0 */
+        return String.format(Locale.US, "%04d%02d%02dT%02d%02d%02d",
+                cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1,
+                cal.get(Calendar.DATE), cal.get(Calendar.HOUR_OF_DAY),
+                cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND));
+    }
+}