diff options
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)); + } +} |