summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.mk35
-rw-r--r--src/android/bluetooth/client/map/BluetoothMapBmessage.java169
-rw-r--r--src/android/bluetooth/client/map/BluetoothMapBmessageBuilder.java160
-rw-r--r--src/android/bluetooth/client/map/BluetoothMapBmessageParser.java421
-rw-r--r--src/android/bluetooth/client/map/BluetoothMapEventReport.java223
-rw-r--r--src/android/bluetooth/client/map/BluetoothMapFolderListing.java69
-rw-r--r--src/android/bluetooth/client/map/BluetoothMapMessage.java332
-rw-r--r--src/android/bluetooth/client/map/BluetoothMapMessagesListing.java84
-rw-r--r--src/android/bluetooth/client/map/BluetoothMapRfcommTransport.java77
-rw-r--r--src/android/bluetooth/client/map/BluetoothMasClient.java1102
-rw-r--r--src/android/bluetooth/client/map/BluetoothMasObexClientSession.java192
-rw-r--r--src/android/bluetooth/client/map/BluetoothMasRequest.java161
-rw-r--r--src/android/bluetooth/client/map/BluetoothMasRequestGetFolderListing.java75
-rw-r--r--src/android/bluetooth/client/map/BluetoothMasRequestGetFolderListingSize.java56
-rw-r--r--src/android/bluetooth/client/map/BluetoothMasRequestGetMessage.java89
-rw-r--r--src/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListing.java150
-rw-r--r--src/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListingSize.java57
-rw-r--r--src/android/bluetooth/client/map/BluetoothMasRequestPushMessage.java79
-rw-r--r--src/android/bluetooth/client/map/BluetoothMasRequestSetMessageStatus.java52
-rw-r--r--src/android/bluetooth/client/map/BluetoothMasRequestSetNotificationRegistration.java52
-rw-r--r--src/android/bluetooth/client/map/BluetoothMasRequestSetPath.java71
-rw-r--r--src/android/bluetooth/client/map/BluetoothMasRequestUpdateInbox.java36
-rw-r--r--src/android/bluetooth/client/map/BluetoothMnsObexServer.java136
-rw-r--r--src/android/bluetooth/client/map/BluetoothMnsService.java195
-rw-r--r--src/android/bluetooth/client/map/utils/BmsgTokenizer.java108
-rw-r--r--src/android/bluetooth/client/map/utils/ObexAppParameters.java182
-rw-r--r--src/android/bluetooth/client/map/utils/ObexTime.java101
-rw-r--r--src/android/bluetooth/client/pbap/BluetoothPbapCard.java142
-rw-r--r--src/android/bluetooth/client/pbap/BluetoothPbapClient.java846
-rw-r--r--src/android/bluetooth/client/pbap/BluetoothPbapObexAuthenticator.java86
-rw-r--r--src/android/bluetooth/client/pbap/BluetoothPbapObexSession.java232
-rw-r--r--src/android/bluetooth/client/pbap/BluetoothPbapObexTransport.java83
-rw-r--r--src/android/bluetooth/client/pbap/BluetoothPbapRequest.java130
-rw-r--r--src/android/bluetooth/client/pbap/BluetoothPbapRequestPullPhoneBook.java115
-rw-r--r--src/android/bluetooth/client/pbap/BluetoothPbapRequestPullPhoneBookSize.java55
-rw-r--r--src/android/bluetooth/client/pbap/BluetoothPbapRequestPullVcardEntry.java85
-rw-r--r--src/android/bluetooth/client/pbap/BluetoothPbapRequestPullVcardListing.java110
-rw-r--r--src/android/bluetooth/client/pbap/BluetoothPbapRequestPullVcardListingSize.java55
-rw-r--r--src/android/bluetooth/client/pbap/BluetoothPbapRequestSetPath.java73
-rw-r--r--src/android/bluetooth/client/pbap/BluetoothPbapSession.java330
-rw-r--r--src/android/bluetooth/client/pbap/BluetoothPbapVcardList.java91
-rw-r--r--src/android/bluetooth/client/pbap/BluetoothPbapVcardListing.java66
-rw-r--r--src/android/bluetooth/client/pbap/utils/BmsgTokenizer.java108
-rw-r--r--src/android/bluetooth/client/pbap/utils/ObexAppParameters.java182
-rw-r--r--src/android/bluetooth/client/pbap/utils/ObexTime.java101
45 files changed, 7354 insertions, 0 deletions
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));
+ }
+}