From 238e0f934f1f47263b384bc745ae0678c777130d Mon Sep 17 00:00:00 2001 From: Casper Bonde Date: Thu, 9 Apr 2015 09:24:48 +0200 Subject: OBEX Over L2CAP + SDP search API for BT profiles - Updated OBEX to support SRM - Added support for OBEX over l2cap and SRM. - Minor bugfixes, and reduce CPU load ALOT - Added support to send responses without body data. - Extend BluetoothSocket to support L2CAP - Added functionality to get the channel number needed to be able to create an SDP record with the channel number. - Added interface to get socket type and max packet sizes. - Added interface to perform SDP search and get the resulting SDP record data. Change-Id: I9d37a00ce73dfffc0e3ce03eab5511ba3a86e5b8 --- api/current.txt | 6 + api/system-current.txt | 6 + core/java/android/bluetooth/BluetoothAdapter.java | 48 ++- core/java/android/bluetooth/BluetoothDevice.java | 69 +++- .../android/bluetooth/BluetoothServerSocket.java | 52 ++- core/java/android/bluetooth/BluetoothSocket.java | 197 ++++++++-- core/java/android/bluetooth/IBluetooth.aidl | 2 +- core/java/android/bluetooth/SdpMasRecord.java | 147 ++++++++ core/java/android/bluetooth/SdpMnsRecord.java | 112 ++++++ core/java/android/bluetooth/SdpOppOpsRecord.java | 118 ++++++ core/java/android/bluetooth/SdpPseRecord.java | 125 +++++++ core/java/android/bluetooth/SdpRecord.java | 76 ++++ obex/Android.mk | 11 + obex/javax/obex/ClientOperation.java | 174 ++++++--- obex/javax/obex/ClientSession.java | 247 ++++++++---- obex/javax/obex/HeaderSet.java | 65 +++- obex/javax/obex/ObexHelper.java | 87 ++++- obex/javax/obex/ObexPacket.java | 71 ++++ obex/javax/obex/ObexSession.java | 6 + obex/javax/obex/ObexTransport.java | 35 ++ obex/javax/obex/ServerOperation.java | 416 +++++++++++++-------- obex/javax/obex/ServerRequestHandler.java | 9 + obex/javax/obex/ServerSession.java | 59 ++- 23 files changed, 1809 insertions(+), 329 deletions(-) create mode 100644 core/java/android/bluetooth/SdpMasRecord.java create mode 100644 core/java/android/bluetooth/SdpMnsRecord.java create mode 100644 core/java/android/bluetooth/SdpOppOpsRecord.java create mode 100644 core/java/android/bluetooth/SdpPseRecord.java create mode 100644 core/java/android/bluetooth/SdpRecord.java create mode 100644 obex/javax/obex/ObexPacket.java diff --git a/api/current.txt b/api/current.txt index a5a907c..b59d217 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6505,10 +6505,16 @@ package android.bluetooth { public final class BluetoothSocket implements java.io.Closeable { method public void close() throws java.io.IOException; method public void connect() throws java.io.IOException; + method public int getConnectionType(); method public java.io.InputStream getInputStream() throws java.io.IOException; + method public int getMaxReceivePacketSize(); + method public int getMaxTransmitPacketSize(); method public java.io.OutputStream getOutputStream() throws java.io.IOException; method public android.bluetooth.BluetoothDevice getRemoteDevice(); method public boolean isConnected(); + field public static final int TYPE_L2CAP = 3; // 0x3 + field public static final int TYPE_RFCOMM = 1; // 0x1 + field public static final int TYPE_SCO = 2; // 0x2 } } diff --git a/api/system-current.txt b/api/system-current.txt index 0e5af53..aefa999 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -6721,10 +6721,16 @@ package android.bluetooth { public final class BluetoothSocket implements java.io.Closeable { method public void close() throws java.io.IOException; method public void connect() throws java.io.IOException; + method public int getConnectionType(); method public java.io.InputStream getInputStream() throws java.io.IOException; + method public int getMaxReceivePacketSize(); + method public int getMaxTransmitPacketSize(); method public java.io.OutputStream getOutputStream() throws java.io.IOException; method public android.bluetooth.BluetoothDevice getRemoteDevice(); method public boolean isConnected(); + field public static final int TYPE_L2CAP = 3; // 0x3 + field public static final int TYPE_RFCOMM = 1; // 0x1 + field public static final int TYPE_SCO = 2; // 0x2 } } diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index 0442d09..ac74a62 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -1,5 +1,6 @@ /* - * Copyright (C) 2009-2014 The Android Open Source Project + * Copyright (C) 2009-2015 The Android Open Source Project + * Copyright (C) 2015 Samsung LSI * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -377,6 +378,18 @@ public final class BluetoothAdapter { /** @hide */ public static final String BLUETOOTH_MANAGER_SERVICE = "bluetooth_manager"; + + /** When creating a ServerSocket using listenUsingRfcommOn() or + * listenUsingL2capOn() use SOCKET_CHANNEL_AUTO_STATIC to create + * a ServerSocket that auto assigns a channel number to the first + * bluetooth socket. + * The channel number assigned to this first Bluetooth Socket will + * be stored in the ServerSocket, and reused for subsequent Bluetooth + * sockets. + * @hide */ + public static final int SOCKET_CHANNEL_AUTO_STATIC_NO_SDP = -2; + + private static final int ADDRESS_LENGTH = 17; private static final int CONTROLLER_ENERGY_UPDATE_TIMEOUT_MILLIS = 30; @@ -1144,6 +1157,9 @@ public final class BluetoothAdapter { BluetoothServerSocket socket = new BluetoothServerSocket( BluetoothSocket.TYPE_RFCOMM, true, true, channel); int errno = socket.mSocket.bindListen(); + if(channel == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { + socket.setChannel(socket.mSocket.getPort()); + } if (errno != 0) { //TODO(BT): Throw the same exception error code // that the previous code was using. @@ -1278,6 +1294,9 @@ public final class BluetoothAdapter { BluetoothServerSocket socket = new BluetoothServerSocket( BluetoothSocket.TYPE_RFCOMM, false, false, port); int errno = socket.mSocket.bindListen(); + if(port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { + socket.setChannel(socket.mSocket.getPort()); + } if (errno != 0) { //TODO(BT): Throw the same exception error code // that the previous code was using. @@ -1300,6 +1319,9 @@ public final class BluetoothAdapter { BluetoothServerSocket socket = new BluetoothServerSocket( BluetoothSocket.TYPE_RFCOMM, false, true, port); int errno = socket.mSocket.bindListen(); + if(port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { + socket.setChannel(socket.mSocket.getPort()); + } if (errno < 0) { //TODO(BT): Throw the same exception error code // that the previous code was using. @@ -1330,6 +1352,30 @@ public final class BluetoothAdapter { } /** + * Construct an encrypted, authenticated, L2CAP server socket. + * Call #accept to retrieve connections to this socket. + * @return An L2CAP BluetoothServerSocket + * @throws IOException On error, for example Bluetooth not available, or + * insufficient permissions. + * @hide + */ + public BluetoothServerSocket listenUsingL2capOn(int port) throws IOException { + BluetoothServerSocket socket = new BluetoothServerSocket( + BluetoothSocket.TYPE_L2CAP, true, true, port); + int errno = socket.mSocket.bindListen(); + if(port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { + socket.setChannel(socket.mSocket.getPort()); + } + if (errno != 0) { + //TODO(BT): Throw the same exception error code + // that the previous code was using. + //socket.mSocket.throwErrnoNative(errno); + throw new IOException("Error: " + errno); + } + return socket; + } + + /** * Read the local Out of Band Pairing Data *

Requires {@link android.Manifest.permission#BLUETOOTH} * diff --git a/core/java/android/bluetooth/BluetoothDevice.java b/core/java/android/bluetooth/BluetoothDevice.java index bb0d0a3..1fdf9f4 100644 --- a/core/java/android/bluetooth/BluetoothDevice.java +++ b/core/java/android/bluetooth/BluetoothDevice.java @@ -302,6 +302,12 @@ public final class BluetoothDevice implements Parcelable { */ public static final int DEVICE_TYPE_DUAL = 3; + + /** @hide */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_SDP_RECORD = + "android.bluetooth.device.action.SDP_RECORD"; + /** * Broadcast Action: This intent is used to broadcast the {@link UUID} * wrapped as a {@link android.os.ParcelUuid} of the remote device after it @@ -526,6 +532,13 @@ public final class BluetoothDevice implements Parcelable { */ public static final String EXTRA_UUID = "android.bluetooth.device.extra.UUID"; + /** @hide */ + public static final String EXTRA_SDP_RECORD = + "android.bluetooth.device.extra.SDP_RECORD"; + + /** @hide */ + public static final String EXTRA_SDP_SEARCH_STATUS = + "android.bluetooth.device.extra.SDP_SEARCH_STATUS"; /** * For {@link #getPhonebookAccessPermission}, {@link #setPhonebookAccessPermission}, * {@link #getMessageAccessPermission} and {@link #setMessageAccessPermission}. @@ -1054,14 +1067,34 @@ public final class BluetoothDevice implements Parcelable { return false; } + /** + * Perform a service discovery on the remote device to get the SDP records associated + * with the specified UUID. + * + *

This API is asynchronous and {@link #ACTION_SDP_RECORD} intent is sent, + * with the SDP records found on the remote end. If there is an error + * in getting the SDP records or if the process takes a long time, + * {@link #ACTION_SDP_RECORD} intent is sent with an status value in + * {@link #EXTRA_SDP_SEARCH_STATUS} different from 0. + * Detailed status error codes can be found by members of the Bluetooth package in + * the AbstractionLayer class. + *

Requires {@link android.Manifest.permission#BLUETOOTH}. + * The SDP record data will be stored in the intent as {@link #EXTRA_SDP_RECORD}. + * The object type will match one of the SdpXxxRecord types, depending on the UUID searched + * for. + * + * @return False if the sanity check fails, True if the process + * of initiating an ACL connection to the remote device + * was started. + */ /** @hide */ - public boolean fetchMasInstances() { + public boolean sdpSearch(ParcelUuid uuid) { if (sService == null) { - Log.e(TAG, "BT not enabled. Cannot query remote device for MAS instances"); + Log.e(TAG, "BT not enabled. Cannot query remote device sdp records"); return false; } try { - return sService.fetchRemoteMasInstances(this); + return sService.sdpSearch(this,uuid); } catch (RemoteException e) {Log.e(TAG, "", e);} return false; } @@ -1261,6 +1294,36 @@ public final class BluetoothDevice implements Parcelable { } /** + * Create an L2cap {@link BluetoothSocket} ready to start a secure + * outgoing connection to this remote device on given channel. + *

The remote device will be authenticated and communication on this + * socket will be encrypted. + *

Use this socket only if an authenticated socket link is possible. + * Authentication refers to the authentication of the link key to + * prevent man-in-the-middle type of attacks. + * For example, for Bluetooth 2.1 devices, if any of the devices does not + * have an input and output capability or just has the ability to + * display a numeric key, a secure socket connection is not possible. + * In such a case, use {#link createInsecureRfcommSocket}. + * For more details, refer to the Security Model section 5.2 (vol 3) of + * Bluetooth Core Specification version 2.1 + EDR. + *

Use {@link BluetoothSocket#connect} to initiate the outgoing + * connection. + *

Valid L2CAP PSM channels are in range 1 to 2^16. + *

Requires {@link android.Manifest.permission#BLUETOOTH} + * + * @param channel L2cap PSM/channel to connect to + * @return a RFCOMM BluetoothServerSocket ready for an outgoing connection + * @throws IOException on error, for example Bluetooth not available, or + * insufficient permissions + * @hide + */ + public BluetoothSocket createL2capSocket(int channel) throws IOException { + return new BluetoothSocket(BluetoothSocket.TYPE_L2CAP, -1, true, true, this, channel, + null); + } + + /** * Create an RFCOMM {@link BluetoothSocket} ready to start a secure * outgoing connection to this remote device using SDP lookup of uuid. *

This is designed to be used with {@link diff --git a/core/java/android/bluetooth/BluetoothServerSocket.java b/core/java/android/bluetooth/BluetoothServerSocket.java index bc56e55..21024a6 100644 --- a/core/java/android/bluetooth/BluetoothServerSocket.java +++ b/core/java/android/bluetooth/BluetoothServerSocket.java @@ -18,6 +18,7 @@ package android.bluetooth; import android.os.Handler; import android.os.ParcelUuid; +import android.util.Log; import java.io.Closeable; import java.io.IOException; @@ -66,10 +67,11 @@ import java.io.IOException; */ public final class BluetoothServerSocket implements Closeable { + private static final String TAG = "BluetoothServerSocket"; /*package*/ final BluetoothSocket mSocket; private Handler mHandler; private int mMessage; - private final int mChannel; + private int mChannel; /** * Construct a socket for incoming connections. @@ -84,6 +86,9 @@ public final class BluetoothServerSocket implements Closeable { throws IOException { mChannel = port; mSocket = new BluetoothSocket(type, -1, auth, encrypt, null, port, null); + if(port == BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { + mSocket.setExcludeSdp(true); + } } /** @@ -98,6 +103,7 @@ public final class BluetoothServerSocket implements Closeable { /*package*/ BluetoothServerSocket(int type, boolean auth, boolean encrypt, ParcelUuid uuid) throws IOException { mSocket = new BluetoothSocket(type, -1, auth, encrypt, null, -1, uuid); + // TODO: This is the same as mChannel = -1 - is this intentional? mChannel = mSocket.getPort(); } @@ -153,6 +159,7 @@ public final class BluetoothServerSocket implements Closeable { /*package*/ void setServiceName(String ServiceName) { mSocket.setServiceName(ServiceName); } + /** * Returns the channel on which this socket is bound. * @hide @@ -160,4 +167,47 @@ public final class BluetoothServerSocket implements Closeable { public int getChannel() { return mChannel; } + + /** + * Sets the channel on which future sockets are bound. + * Currently used only when a channel is auto generated. + */ + /*package*/ void setChannel(int newChannel) { + /* TODO: From a design/architecture perspective this is wrong. + * The bind operation should be conducted through this class + * and the resulting port should be kept in mChannel, and + * not set from BluetoothAdapter. */ + if(mSocket != null) { + if(mSocket.getPort() != newChannel) { + Log.w(TAG,"The port set is different that the underlying port. mSocket.getPort(): " + + mSocket.getPort() + " requested newChannel: " + newChannel); + } + } + mChannel = newChannel; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("ServerSocket: Type: "); + switch(mSocket.getConnectionType()) { + case BluetoothSocket.TYPE_RFCOMM: + { + sb.append("TYPE_RFCOMM"); + break; + } + case BluetoothSocket.TYPE_L2CAP: + { + sb.append("TYPE_L2CAP"); + break; + } + case BluetoothSocket.TYPE_SCO: + { + sb.append("TYPE_SCO"); + break; + } + } + sb.append(" Channel: ").append(mChannel); + return sb.toString(); + } } diff --git a/core/java/android/bluetooth/BluetoothSocket.java b/core/java/android/bluetooth/BluetoothSocket.java index 36997e5..5702d11 100644 --- a/core/java/android/bluetooth/BluetoothSocket.java +++ b/core/java/android/bluetooth/BluetoothSocket.java @@ -21,6 +21,7 @@ import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Log; +import java.io.BufferedInputStream; import java.io.Closeable; import java.io.FileDescriptor; import java.io.IOException; @@ -29,6 +30,8 @@ import java.io.OutputStream; import java.util.Locale; import java.util.UUID; import android.net.LocalSocket; + +import java.nio.Buffer; import java.nio.ByteOrder; import java.nio.ByteBuffer; /** @@ -86,17 +89,19 @@ public final class BluetoothSocket implements Closeable { /** @hide */ public static final int MAX_RFCOMM_CHANNEL = 30; + /*package*/ static final int MAX_L2CAP_PACKAGE_SIZE = 0xFFFF; /** Keep TYPE_ fields in sync with BluetoothSocket.cpp */ - /*package*/ static final int TYPE_RFCOMM = 1; - /*package*/ static final int TYPE_SCO = 2; - /*package*/ static final int TYPE_L2CAP = 3; + public static final int TYPE_RFCOMM = 1; + public static final int TYPE_SCO = 2; + public static final int TYPE_L2CAP = 3; /*package*/ static final int EBADFD = 77; /*package*/ static final int EADDRINUSE = 98; /*package*/ static final int SEC_FLAG_ENCRYPT = 1; /*package*/ static final int SEC_FLAG_AUTH = 1 << 1; + /*package*/ static final int BTSOCK_FLAG_NO_SDP = 1 << 2; private final int mType; /* one of TYPE_RFCOMM etc */ private BluetoothDevice mDevice; /* remote device */ @@ -106,6 +111,7 @@ public final class BluetoothSocket implements Closeable { private final BluetoothInputStream mInputStream; private final BluetoothOutputStream mOutputStream; private final ParcelUuid mUuid; + private boolean mExcludeSdp = false; private ParcelFileDescriptor mPfd; private LocalSocket mSocket; private InputStream mSocketIS; @@ -115,7 +121,11 @@ public final class BluetoothSocket implements Closeable { private String mServiceName; private static int PROXY_CONNECTION_TIMEOUT = 5000; - private static int SOCK_SIGNAL_SIZE = 16; + private static int SOCK_SIGNAL_SIZE = 20; + + private ByteBuffer mL2capBuffer = null; + private int mMaxTxPacketSize = 0; // The l2cap maximum packet size supported by the peer. + private int mMaxRxPacketSize = 0; // The l2cap maximum packet size that can be received. private enum SocketState { INIT, @@ -144,12 +154,14 @@ public final class BluetoothSocket implements Closeable { */ /*package*/ BluetoothSocket(int type, int fd, boolean auth, boolean encrypt, BluetoothDevice device, int port, ParcelUuid uuid) throws IOException { - if (type == BluetoothSocket.TYPE_RFCOMM && uuid == null && fd == -1) { + if (VDBG) Log.d(TAG, "Creating new BluetoothSocket of type: " + type); + if (type == BluetoothSocket.TYPE_RFCOMM && uuid == null && fd == -1 + && port != BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) { if (port < 1 || port > MAX_RFCOMM_CHANNEL) { throw new IOException("Invalid RFCOMM channel: " + port); } } - if(uuid != null) + if (uuid != null) mUuid = uuid; else mUuid = new ParcelUuid(new UUID(0, 0)); mType = type; @@ -172,6 +184,7 @@ public final class BluetoothSocket implements Closeable { mOutputStream = new BluetoothOutputStream(this); } private BluetoothSocket(BluetoothSocket s) { + if (VDBG) Log.d(TAG, "Creating new Private BluetoothSocket of type: " + s.mType); mUuid = s.mUuid; mType = s.mType; mAuth = s.mAuth; @@ -179,7 +192,11 @@ public final class BluetoothSocket implements Closeable { mPort = s.mPort; mInputStream = new BluetoothInputStream(this); mOutputStream = new BluetoothOutputStream(this); + mMaxRxPacketSize = s.mMaxRxPacketSize; + mMaxTxPacketSize = s.mMaxTxPacketSize; + mServiceName = s.mServiceName; + mExcludeSdp = s.mExcludeSdp; } private BluetoothSocket acceptSocket(String RemoteAddr) throws IOException { BluetoothSocket as = new BluetoothSocket(this); @@ -229,6 +246,8 @@ public final class BluetoothSocket implements Closeable { flags |= SEC_FLAG_AUTH; if(mEncrypt) flags |= SEC_FLAG_ENCRYPT; + if(mExcludeSdp) + flags |= BTSOCK_FLAG_NO_SDP; return flags; } @@ -298,7 +317,8 @@ public final class BluetoothSocket implements Closeable { try { if (mSocketState == SocketState.CLOSED) throw new IOException("socket closed"); - IBluetooth bluetoothProxy = BluetoothAdapter.getDefaultAdapter().getBluetoothService(null); + IBluetooth bluetoothProxy = + BluetoothAdapter.getDefaultAdapter().getBluetoothService(null); if (bluetoothProxy == null) throw new IOException("Bluetooth is off"); mPfd = bluetoothProxy.connectSocket(mDevice, mType, mUuid, mPort, getSecurityFlags()); @@ -370,7 +390,7 @@ public final class BluetoothSocket implements Closeable { mSocketState = SocketState.LISTENING; } if (DBG) Log.d(TAG, "channel: " + channel); - if (mPort == -1) { + if (mPort <= -1) { mPort = channel; } // else ASSERT(mPort == channel) ret = 0; @@ -391,7 +411,8 @@ public final class BluetoothSocket implements Closeable { /*package*/ BluetoothSocket accept(int timeout) throws IOException { BluetoothSocket acceptedSocket; - if (mSocketState != SocketState.LISTENING) throw new IOException("bt socket is not in listen state"); + if (mSocketState != SocketState.LISTENING) + throw new IOException("bt socket is not in listen state"); if(timeout > 0) { Log.d(TAG, "accept() set timeout (ms):" + timeout); mSocket.setSoTimeout(timeout); @@ -427,27 +448,80 @@ public final class BluetoothSocket implements Closeable { } /*package*/ int read(byte[] b, int offset, int length) throws IOException { - if (mSocketIS == null) throw new IOException("read is called on null InputStream"); + int ret = 0; if (VDBG) Log.d(TAG, "read in: " + mSocketIS + " len: " + length); - int ret = mSocketIS.read(b, offset, length); - if(ret < 0) + if(mType == TYPE_L2CAP) + { + int bytesToRead = length; + if (VDBG) Log.v(TAG, "l2cap: read(): offset: " + offset + " length:" + length + + "mL2capBuffer= " + mL2capBuffer); + if (mL2capBuffer == null) { + createL2capRxBuffer(); + } + if (mL2capBuffer.remaining() == 0) { + if (VDBG) Log.v(TAG, "l2cap buffer empty, refilling..."); + if (fillL2capRxBuffer() == -1) { + return -1; + } + } + if (bytesToRead > mL2capBuffer.remaining()) { + bytesToRead = mL2capBuffer.remaining(); + } + if(VDBG) Log.v(TAG, "get(): offset: " + offset + + " bytesToRead: " + bytesToRead); + mL2capBuffer.get(b, offset, bytesToRead); + ret = bytesToRead; + }else { + if (VDBG) Log.v(TAG, "default: read(): offset: " + offset + " length:" + length); + ret = mSocketIS.read(b, offset, length); + } + if (ret < 0) throw new IOException("bt socket closed, read return: " + ret); if (VDBG) Log.d(TAG, "read out: " + mSocketIS + " ret: " + ret); return ret; } /*package*/ int write(byte[] b, int offset, int length) throws IOException { - if (mSocketOS == null) throw new IOException("write is called on null OutputStream"); - if (VDBG) Log.d(TAG, "write: " + mSocketOS + " length: " + length); - mSocketOS.write(b, offset, length); - // There is no good way to confirm since the entire process is asynchronous anyway - if (VDBG) Log.d(TAG, "write out: " + mSocketOS + " length: " + length); - return length; + + //TODO: Since bindings can exist between the SDU size and the + // protocol, we might need to throw an exception instead of just + // splitting the write into multiple smaller writes. + // Rfcomm uses dynamic allocation, and should not have any bindings + // to the actual message length. + if (VDBG) Log.d(TAG, "write: " + mSocketOS + " length: " + length); + if (mType == TYPE_L2CAP) { + if(length <= mMaxTxPacketSize) { + mSocketOS.write(b, offset, length); + } else { + int tmpOffset = offset; + int tmpLength = mMaxTxPacketSize; + int endIndex = offset + length; + boolean done = false; + if(DBG) Log.w(TAG, "WARNING: Write buffer larger than L2CAP packet size!\n" + + "Packet will be divided into SDU packets of size " + + mMaxTxPacketSize); + do{ + mSocketOS.write(b, tmpOffset, tmpLength); + tmpOffset += mMaxTxPacketSize; + if((tmpOffset + mMaxTxPacketSize) > endIndex) { + tmpLength = endIndex - tmpOffset; + done = true; + } + } while(!done); + + } + } else { + mSocketOS.write(b, offset, length); + } + // There is no good way to confirm since the entire process is asynchronous anyway + if (VDBG) Log.d(TAG, "write out: " + mSocketOS + " length: " + length); + return length; } @Override public void close() throws IOException { - if (DBG) Log.d(TAG, "close() in, this: " + this + ", channel: " + mPort + ", state: " + mSocketState); + if (DBG) Log.d(TAG, "close() in, this: " + this + ", channel: " + mPort + ", state: " + + mSocketState); if(mSocketState == SocketState.CLOSED) return; else @@ -457,8 +531,9 @@ public final class BluetoothSocket implements Closeable { if(mSocketState == SocketState.CLOSED) return; mSocketState = SocketState.CLOSED; - if (DBG) Log.d(TAG, "close() this: " + this + ", channel: " + mPort + ", mSocketIS: " + mSocketIS + - ", mSocketOS: " + mSocketOS + "mSocket: " + mSocket); + if (DBG) Log.d(TAG, "close() this: " + this + ", channel: " + mPort + + ", mSocketIS: " + mSocketIS + ", mSocketOS: " + mSocketOS + + "mSocket: " + mSocket); if(mSocket != null) { if (DBG) Log.d(TAG, "Closing mSocket: " + mSocket); mSocket.shutdownInput(); @@ -480,6 +555,47 @@ public final class BluetoothSocket implements Closeable { /*package */ int getPort() { return mPort; } + + /** + * Get the maximum supported Transmit packet size for the underlying transport. + * Use this to optimize the writes done to the output socket, to avoid sending + * half full packets. + * @return the maximum supported Transmit packet size for the underlying transport. + */ + public int getMaxTransmitPacketSize(){ + return mMaxTxPacketSize; + } + + /** + * Get the maximum supported Receive packet size for the underlying transport. + * Use this to optimize the reads done on the input stream, as any call to read + * will return a maximum of this amount of bytes - or for some transports a + * multiple of this value. + * @return the maximum supported Receive packet size for the underlying transport. + */ + public int getMaxReceivePacketSize(){ + return mMaxRxPacketSize; + } + + /** + * Get the type of the underlying connection + * @return one of TYPE_ + */ + public int getConnectionType() { + return mType; + } + + /** + * Change if a SDP entry should be automatically created. + * Must be called before calling .bind, for the call to have any effect. + * @param mExcludeSdp

  • TRUE - do not auto generate SDP record. + *
  • FALSE - default - auto generate SPP SDP record. + * @hide + */ + public void setExcludeSdp(boolean excludeSdp) { + this.mExcludeSdp = excludeSdp; + } + private String convertAddr(final byte[] addr) { return String.format(Locale.US, "%02X:%02X:%02X:%02X:%02X:%02X", addr[0] , addr[1], addr[2], addr[3] , addr[4], addr[5]); @@ -487,8 +603,10 @@ public final class BluetoothSocket implements Closeable { private String waitSocketSignal(InputStream is) throws IOException { byte [] sig = new byte[SOCK_SIGNAL_SIZE]; int ret = readAll(is, sig); - if (VDBG) Log.d(TAG, "waitSocketSignal read 16 bytes signal ret: " + ret); + if (VDBG) Log.d(TAG, "waitSocketSignal read " + SOCK_SIGNAL_SIZE + + " bytes signal ret: " + ret); ByteBuffer bb = ByteBuffer.wrap(sig); + /* the struct in native is decorated with __attribute__((packed)), hence this is possible */ bb.order(ByteOrder.nativeOrder()); int size = bb.getShort(); if(size != SOCK_SIGNAL_SIZE) @@ -497,19 +615,36 @@ public final class BluetoothSocket implements Closeable { bb.get(addr); int channel = bb.getInt(); int status = bb.getInt(); + mMaxTxPacketSize = (bb.getShort() & 0xffff); // Convert to unsigned value + mMaxRxPacketSize = (bb.getShort() & 0xffff); // Convert to unsigned value String RemoteAddr = convertAddr(addr); if (VDBG) Log.d(TAG, "waitSocketSignal: sig size: " + size + ", remote addr: " - + RemoteAddr + ", channel: " + channel + ", status: " + status); + + RemoteAddr + ", channel: " + channel + ", status: " + status + + " MaxRxPktSize: " + mMaxRxPacketSize + " MaxTxPktSize: " + mMaxTxPacketSize); if(status != 0) throw new IOException("Connection failure, status: " + status); return RemoteAddr; } + + private void createL2capRxBuffer(){ + if(mType == TYPE_L2CAP) { + // Allocate the buffer to use for reads. + if(VDBG) Log.v(TAG, " Creating mL2capBuffer: mMaxPacketSize: " + mMaxRxPacketSize); + mL2capBuffer = ByteBuffer.wrap(new byte[mMaxRxPacketSize]); + if(VDBG) Log.v(TAG, "mL2capBuffer.remaining()" + mL2capBuffer.remaining()); + mL2capBuffer.limit(0); // Ensure we do a real read at the first read-request + if(VDBG) Log.v(TAG, "mL2capBuffer.remaining() after limit(0):" + + mL2capBuffer.remaining()); + } + } + private int readAll(InputStream is, byte[] b) throws IOException { int left = b.length; while(left > 0) { int ret = is.read(b, b.length - left, left); if(ret <= 0) - throw new IOException("read failed, socket might closed or timeout, read ret: " + ret); + throw new IOException("read failed, socket might closed or timeout, read ret: " + + ret); left -= ret; if(left != 0) Log.w(TAG, "readAll() looping, read partial size: " + (b.length - left) + @@ -526,4 +661,18 @@ public final class BluetoothSocket implements Closeable { bb.order(ByteOrder.nativeOrder()); return bb.getInt(); } + + private int fillL2capRxBuffer() throws IOException { + mL2capBuffer.rewind(); + int ret = mSocketIS.read(mL2capBuffer.array()); + if(ret == -1) { + // reached end of stream - return -1 + mL2capBuffer.limit(0); + return -1; + } + mL2capBuffer.limit(ret); + return ret; + } + + } diff --git a/core/java/android/bluetooth/IBluetooth.aidl b/core/java/android/bluetooth/IBluetooth.aidl index 299f4c8..af560df 100644 --- a/core/java/android/bluetooth/IBluetooth.aidl +++ b/core/java/android/bluetooth/IBluetooth.aidl @@ -68,7 +68,7 @@ interface IBluetooth int getRemoteClass(in BluetoothDevice device); ParcelUuid[] getRemoteUuids(in BluetoothDevice device); boolean fetchRemoteUuids(in BluetoothDevice device); - boolean fetchRemoteMasInstances(in BluetoothDevice device); + boolean sdpSearch(in BluetoothDevice device, in ParcelUuid uuid); boolean setPin(in BluetoothDevice device, boolean accept, int len, in byte[] pinCode); boolean setPasskey(in BluetoothDevice device, boolean accept, int len, in byte[] diff --git a/core/java/android/bluetooth/SdpMasRecord.java b/core/java/android/bluetooth/SdpMasRecord.java new file mode 100644 index 0000000..fa164c0 --- /dev/null +++ b/core/java/android/bluetooth/SdpMasRecord.java @@ -0,0 +1,147 @@ +/* +* Copyright (C) 2015 Samsung System LSI +* 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; + +import android.os.Parcel; +import android.os.Parcelable; + +/** @hide */ +public class SdpMasRecord implements Parcelable { + private final int mMasInstanceId; + private final int mL2capPsm; + private final int mRfcommChannelNumber; + private final int mProfileVersion; + private final int mSupportedFeatures; + private final int mSupportedMessageTypes; + private final String mServiceName; + public static final class MessageType { + public static final int EMAIL = 0x01; + public static final int SMS_GSM = 0x02; + public static final int SMS_CDMA = 0x04; + public static final int MMS = 0x08; + } + + public SdpMasRecord(int mas_instance_id, + int l2cap_psm, + int rfcomm_channel_number, + int profile_version, + int supported_features, + int supported_message_types, + String service_name){ + this.mMasInstanceId = mas_instance_id; + this.mL2capPsm = l2cap_psm; + this.mRfcommChannelNumber = rfcomm_channel_number; + this.mProfileVersion = profile_version; + this.mSupportedFeatures = supported_features; + this.mSupportedMessageTypes = supported_message_types; + this.mServiceName = service_name; + } + + public SdpMasRecord(Parcel in){ + this.mMasInstanceId = in.readInt(); + this.mL2capPsm = in.readInt(); + this.mRfcommChannelNumber = in.readInt(); + this.mProfileVersion = in.readInt(); + this.mSupportedFeatures = in.readInt(); + this.mSupportedMessageTypes = in.readInt(); + this.mServiceName = in.readString(); + } + @Override + public int describeContents() { + // TODO Auto-generated method stub + return 0; + } + + public int getMasInstanceId() { + return mMasInstanceId; + } + + public int getL2capPsm() { + return mL2capPsm; + } + + public int getRfcommCannelNumber() { + return mRfcommChannelNumber; + } + + public int getProfileVersion() { + return mProfileVersion; + } + + public int getSupportedFeatures() { + return mSupportedFeatures; + } + + public int getSupportedMessageTypes() { + return mSupportedMessageTypes; + } + + public boolean msgSupported(int msg) { + return (mSupportedMessageTypes & msg) != 0; + } + + public String getServiceName() { + return mServiceName; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + + dest.writeInt(this.mMasInstanceId); + dest.writeInt(this.mL2capPsm); + dest.writeInt(this.mRfcommChannelNumber); + dest.writeInt(this.mProfileVersion); + dest.writeInt(this.mSupportedFeatures); + dest.writeInt(this.mSupportedMessageTypes); + dest.writeString(this.mServiceName); + + } + @Override + public String toString(){ + String ret = "Bluetooth MAS SDP Record:\n"; + + if(mMasInstanceId != -1){ + ret += "Mas Instance Id: " + mMasInstanceId + "\n"; + } + if(mRfcommChannelNumber != -1){ + ret += "RFCOMM Chan Number: " + mRfcommChannelNumber + "\n"; + } + if(mL2capPsm != -1){ + ret += "L2CAP PSM: " + mL2capPsm + "\n"; + } + if(mServiceName != null){ + ret += "Service Name: " + mServiceName + "\n"; + } + if(mProfileVersion != -1){ + ret += "Profile version: " + mProfileVersion + "\n"; + } + if(mSupportedMessageTypes != -1){ + ret += "Supported msg types: " + mSupportedMessageTypes + "\n"; + } + if(mSupportedFeatures != -1){ + ret += "Supported features: " + mSupportedFeatures + "\n"; + } + return ret; + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public SdpMasRecord createFromParcel(Parcel in) { + return new SdpMasRecord(in); + } + public SdpRecord[] newArray(int size) { + return new SdpRecord[size]; + } + }; +} diff --git a/core/java/android/bluetooth/SdpMnsRecord.java b/core/java/android/bluetooth/SdpMnsRecord.java new file mode 100644 index 0000000..c02bb5a --- /dev/null +++ b/core/java/android/bluetooth/SdpMnsRecord.java @@ -0,0 +1,112 @@ +/* +* Copyright (C) 2015 Samsung System LSI +* 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; + +import android.os.Parcel; +import android.os.Parcelable; + +/** @hide */ +public class SdpMnsRecord implements Parcelable { + private final int mL2capPsm; + private final int mRfcommChannelNumber; + private final int mSupportedFeatures; + private final int mProfileVersion; + private final String mServiceName; + + public SdpMnsRecord(int l2cap_psm, + int rfcomm_channel_number, + int profile_version, + int supported_features, + String service_name){ + this.mL2capPsm = l2cap_psm; + this.mRfcommChannelNumber = rfcomm_channel_number; + this.mSupportedFeatures = supported_features; + this.mServiceName = service_name; + this.mProfileVersion = profile_version; + } + + public SdpMnsRecord(Parcel in){ + this.mRfcommChannelNumber = in.readInt(); + this.mL2capPsm = in.readInt(); + this.mServiceName = in.readString(); + this.mSupportedFeatures = in.readInt(); + this.mProfileVersion = in.readInt(); + } + @Override + public int describeContents() { + // TODO Auto-generated method stub + return 0; + } + + + public int getL2capPsm() { + return mL2capPsm; + } + + public int getRfcommChannelNumber() { + return mRfcommChannelNumber; + } + + public int getSupportedFeatures() { + return mSupportedFeatures; + } + + public String getServiceName() { + return mServiceName; + } + + public int getProfileVersion() { + return mProfileVersion; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRfcommChannelNumber); + dest.writeInt(mL2capPsm); + dest.writeString(mServiceName); + dest.writeInt(mSupportedFeatures); + dest.writeInt(mProfileVersion); + } + + public String toString(){ + String ret = "Bluetooth MNS SDP Record:\n"; + + if(mRfcommChannelNumber != -1){ + ret += "RFCOMM Chan Number: " + mRfcommChannelNumber + "\n"; + } + if(mL2capPsm != -1){ + ret += "L2CAP PSM: " + mL2capPsm + "\n"; + } + if(mServiceName != null){ + ret += "Service Name: " + mServiceName + "\n"; + } + if(mSupportedFeatures != -1){ + ret += "Supported features: " + mSupportedFeatures + "\n"; + } + if(mProfileVersion != -1){ + ret += "Profile_version: " + mProfileVersion+"\n"; + } + return ret; + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public SdpMnsRecord createFromParcel(Parcel in) { + return new SdpMnsRecord(in); + } + public SdpMnsRecord[] newArray(int size) { + return new SdpMnsRecord[size]; + } + }; +} diff --git a/core/java/android/bluetooth/SdpOppOpsRecord.java b/core/java/android/bluetooth/SdpOppOpsRecord.java new file mode 100644 index 0000000..e0e4007 --- /dev/null +++ b/core/java/android/bluetooth/SdpOppOpsRecord.java @@ -0,0 +1,118 @@ +/* +* Copyright (C) 2015 Samsung System LSI +* 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; + +import java.util.Arrays; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Data representation of a Object Push Profile Server side SDP record. + */ +/** @hide */ +public class SdpOppOpsRecord implements Parcelable { + + private final String mServiceName; + private final int mRfcommChannel; + private final int mL2capPsm; + private final int mProfileVersion; + private final byte[] mFormatsList; + + public SdpOppOpsRecord(String serviceName, int rfcommChannel, + int l2capPsm, int version, byte[] formatsList) { + super(); + this.mServiceName = serviceName; + this.mRfcommChannel = rfcommChannel; + this.mL2capPsm = l2capPsm; + this.mProfileVersion = version; + this.mFormatsList = formatsList; + } + + public String getServiceName() { + return mServiceName; + } + + public int getRfcommChannel() { + return mRfcommChannel; + } + + public int getL2capPsm() { + return mL2capPsm; + } + + public int getProfileVersion() { + return mProfileVersion; + } + + public byte[] getFormatsList() { + return mFormatsList; + } + + @Override + public int describeContents() { + /* No special objects */ + return 0; + } + + public SdpOppOpsRecord(Parcel in){ + this.mRfcommChannel = in.readInt(); + this.mL2capPsm = in.readInt(); + this.mProfileVersion = in.readInt(); + this.mServiceName = in.readString(); + int arrayLength = in.readInt(); + if(arrayLength > 0) { + byte[] bytes = new byte[arrayLength]; + in.readByteArray(bytes); + this.mFormatsList = bytes; + } else { + this.mFormatsList = null; + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRfcommChannel); + dest.writeInt(mL2capPsm); + dest.writeInt(mProfileVersion); + dest.writeString(mServiceName); + if(mFormatsList!= null && mFormatsList.length > 0) { + dest.writeInt(mFormatsList.length); + dest.writeByteArray(mFormatsList); + } else { + dest.writeInt(0); + } + } + + public String toString(){ + StringBuilder sb = new StringBuilder("Bluetooth OPP Server SDP Record:\n"); + sb.append(" RFCOMM Chan Number: ").append(mRfcommChannel); + sb.append("\n L2CAP PSM: ").append(mL2capPsm); + sb.append("\n Profile version: ").append(mProfileVersion); + sb.append("\n Service Name: ").append(mServiceName); + sb.append("\n Formats List: ").append(Arrays.toString(mFormatsList)); + return sb.toString(); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public SdpOppOpsRecord createFromParcel(Parcel in) { + return new SdpOppOpsRecord(in); + } + public SdpOppOpsRecord[] newArray(int size) { + return new SdpOppOpsRecord[size]; + } + }; + +} diff --git a/core/java/android/bluetooth/SdpPseRecord.java b/core/java/android/bluetooth/SdpPseRecord.java new file mode 100644 index 0000000..2c159cc --- /dev/null +++ b/core/java/android/bluetooth/SdpPseRecord.java @@ -0,0 +1,125 @@ +/* +* Copyright (C) 2015 Samsung System LSI +* 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; + +import android.os.Parcel; +import android.os.Parcelable; + +/** @hide */ +public class SdpPseRecord implements Parcelable { + private final int mL2capPsm; + private final int mRfcommChannelNumber; + private final int mProfileVersion; + private final int mSupportedFeatures; + private final int mSupportedRepositories; + private final String mServiceName; + + public SdpPseRecord(int l2cap_psm, + int rfcomm_channel_number, + int profile_version, + int supported_features, + int supported_repositories, + String service_name){ + this.mL2capPsm = l2cap_psm; + this.mRfcommChannelNumber = rfcomm_channel_number; + this.mProfileVersion = profile_version; + this.mSupportedFeatures = supported_features; + this.mSupportedRepositories = supported_repositories; + this.mServiceName = service_name; + } + + public SdpPseRecord(Parcel in){ + this.mRfcommChannelNumber = in.readInt(); + this.mL2capPsm = in.readInt(); + this.mProfileVersion = in.readInt(); + this.mSupportedFeatures = in.readInt(); + this.mSupportedRepositories = in.readInt(); + this.mServiceName = in.readString(); + } + @Override + public int describeContents() { + // TODO Auto-generated method stub + return 0; + } + + public int getL2capPsm() { + return mL2capPsm; + } + + public int getRfcommChannelNumber() { + return mRfcommChannelNumber; + } + + public int getSupportedFeatures() { + return mSupportedFeatures; + } + + public String getServiceName() { + return mServiceName; + } + + public int getProfileVersion() { + return mProfileVersion; + } + + public int getSupportedRepositories() { + return mSupportedRepositories; + } + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRfcommChannelNumber); + dest.writeInt(mL2capPsm); + dest.writeInt(mProfileVersion); + dest.writeInt(mSupportedFeatures); + dest.writeInt(mSupportedRepositories); + dest.writeString(mServiceName); + + } + + public String toString(){ + String ret = "Bluetooth MNS SDP Record:\n"; + + if(mRfcommChannelNumber != -1){ + ret += "RFCOMM Chan Number: " + mRfcommChannelNumber + "\n"; + } + if(mL2capPsm != -1){ + ret += "L2CAP PSM: " + mL2capPsm + "\n"; + } + if(mProfileVersion != -1){ + ret += "profile version: " + mProfileVersion + "\n"; + } + if(mServiceName != null){ + ret += "Service Name: " + mServiceName + "\n"; + } + if(mSupportedFeatures != -1){ + ret += "Supported features: " + mSupportedFeatures + "\n"; + } + if(mSupportedRepositories != -1){ + ret += "Supported repositories: " + mSupportedRepositories + "\n"; + } + + return ret; + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public SdpPseRecord createFromParcel(Parcel in) { + return new SdpPseRecord(in); + } + public SdpPseRecord[] newArray(int size) { + return new SdpPseRecord[size]; + } + }; +} diff --git a/core/java/android/bluetooth/SdpRecord.java b/core/java/android/bluetooth/SdpRecord.java new file mode 100644 index 0000000..6f1065e --- /dev/null +++ b/core/java/android/bluetooth/SdpRecord.java @@ -0,0 +1,76 @@ +/* +* Copyright (C) 2015 Samsung System LSI +* 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; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Arrays; + +/** @hide */ +public class SdpRecord implements Parcelable{ + + private final byte[] mRawData; + private final int mRawSize; + + @Override + public String toString() { + return "BluetoothSdpRecord [rawData=" + Arrays.toString(mRawData) + + ", rawSize=" + mRawSize + "]"; + } + + public SdpRecord(int size_record, byte[] record){ + this.mRawData = record; + this.mRawSize = size_record; + } + + public SdpRecord(Parcel in){ + this.mRawSize = in.readInt(); + this.mRawData = new byte[mRawSize]; + in.readByteArray(this.mRawData); + + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(this.mRawSize); + dest.writeByteArray(this.mRawData); + + + } + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public SdpRecord createFromParcel(Parcel in) { + return new SdpRecord(in); + } + + public SdpRecord[] newArray(int size) { + return new SdpRecord[size]; + } + }; + + public byte[] getRawData() { + return mRawData; + } + + public int getRawSize() { + return mRawSize; + } +} diff --git a/obex/Android.mk b/obex/Android.mk index fbfe9be..e7c1fd3 100644 --- a/obex/Android.mk +++ b/obex/Android.mk @@ -7,3 +7,14 @@ LOCAL_SRC_FILES := $(call all-subdir-java-files) LOCAL_MODULE:= javax.obex include $(BUILD_JAVA_LIBRARY) + + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_MODULE:= javax.obexstatic + +include $(BUILD_STATIC_JAVA_LIBRARY) \ No newline at end of file diff --git a/obex/javax/obex/ClientOperation.java b/obex/javax/obex/ClientOperation.java index 75278b5..cc20d39 100644 --- a/obex/javax/obex/ClientOperation.java +++ b/obex/javax/obex/ClientOperation.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 2014 The Android Open Source Project + * Copyright (c) 2015 The Android Open Source Project + * Copyright (C) 2015 Samsung LSI * Copyright (c) 2008-2009, Motorola, Inc. * * All rights reserved. @@ -40,6 +41,8 @@ import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.ByteArrayOutputStream; +import android.util.Log; + /** * This class implements the Operation interface. It will read and * write data via puts and gets. @@ -47,6 +50,10 @@ import java.io.ByteArrayOutputStream; */ public final class ClientOperation implements Operation, BaseStream { + private static final String TAG = "ClientOperation"; + + private static final boolean V = ObexHelper.VDBG; + private ClientSession mParent; private boolean mInputOpen; @@ -75,6 +82,19 @@ public final class ClientOperation implements Operation, BaseStream { private boolean mEndOfBodySent; + private boolean mSendBodyHeader = true; + // A latch - when triggered, there is not way back ;-) + private boolean mSrmActive = false; + + // Assume SRM disabled - until support is confirmed + // by the server + private boolean mSrmEnabled = false; + // keep waiting until final-bit is received in request + // to handle the case where the SRM enable header is in + // a different OBEX packet than the SRMP header. + private boolean mSrmWaitingForRemote = true; + + /** * Creates new OperationImpl to read and write data to a server * @param maxSize the maximum packet size @@ -164,7 +184,7 @@ public final class ClientOperation implements Operation, BaseStream { * Since we are not sending any headers or returning any headers then * we just need to write and read the same bytes */ - mParent.sendRequest(ObexHelper.OBEX_OPCODE_ABORT, null, mReplyHeader, null); + mParent.sendRequest(ObexHelper.OBEX_OPCODE_ABORT, null, mReplyHeader, null, false); if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_OK) { throw new IOException("Invalid response code from server"); @@ -215,6 +235,7 @@ public final class ClientOperation implements Operation, BaseStream { try { return (String)mReplyHeader.getHeader(HeaderSet.TYPE); } catch (IOException e) { + if(V) Log.d(TAG, "Exception occured - returning null",e); return null; } } @@ -236,6 +257,7 @@ public final class ClientOperation implements Operation, BaseStream { return temp.longValue(); } } catch (IOException e) { + if(V) Log.d(TAG,"Exception occured - returning -1",e); return -1; } } @@ -408,7 +430,9 @@ public final class ClientOperation implements Operation, BaseStream { } /** - * Sends a request to the client of the specified type + * Sends a request to the client of the specified type. + * This function will enable SRM and set SRM active if the server + * response allows this. * @param opCode the request code to send to the client * @return true if there is more data to send; * false if there is no more data to send @@ -431,13 +455,16 @@ public final class ClientOperation implements Operation, BaseStream { * length, but it is a waste of resources if we can't send much of * the body. */ - if ((ObexHelper.BASE_PACKET_LENGTH + headerArray.length) > mMaxPacketSize) { + final int MINIMUM_BODY_LENGTH = 3; + if ((ObexHelper.BASE_PACKET_LENGTH + headerArray.length + MINIMUM_BODY_LENGTH) + > mMaxPacketSize) { int end = 0; int start = 0; // split & send the headerArray in multiple packets. while (end != headerArray.length) { //split the headerArray + end = ObexHelper.findHeaderEnd(headerArray, start, mMaxPacketSize - ObexHelper.BASE_PACKET_LENGTH); // can not split @@ -459,7 +486,7 @@ public final class ClientOperation implements Operation, BaseStream { byte[] sendHeader = new byte[end - start]; System.arraycopy(headerArray, start, sendHeader, 0, sendHeader.length); - if (!mParent.sendRequest(opCode, sendHeader, mReplyHeader, mPrivateInput)) { + if (!mParent.sendRequest(opCode, sendHeader, mReplyHeader, mPrivateInput, false)) { return false; } @@ -470,12 +497,20 @@ public final class ClientOperation implements Operation, BaseStream { start = end; } + // Enable SRM if it should be enabled + checkForSrm(); + if (bodyLength > 0) { return true; } else { return false; } } else { + /* All headers will fit into a single package */ + if(mSendBodyHeader == false) { + /* As we are not to send any body data, set the FINAL_BIT */ + opCode |= ObexHelper.OBEX_OPCODE_FINAL_BIT_MASK; + } out.write(headerArray); } @@ -499,11 +534,11 @@ public final class ClientOperation implements Operation, BaseStream { * (End of Body) otherwise, we need to send 0x48 (Body) */ if ((mPrivateOutput.isClosed()) && (!returnValue) && (!mEndOfBodySent) - && ((opCode & 0x80) != 0)) { - out.write(0x49); + && ((opCode & ObexHelper.OBEX_OPCODE_FINAL_BIT_MASK) != 0)) { + out.write(HeaderSet.END_OF_BODY); mEndOfBodySent = true; } else { - out.write(0x48); + out.write(HeaderSet.BODY); } bodyLength += 3; @@ -517,12 +552,11 @@ public final class ClientOperation implements Operation, BaseStream { if (mPrivateOutputOpen && bodyLength <= 0 && !mEndOfBodySent) { // only 0x82 or 0x83 can send 0x49 - if ((opCode & 0x80) == 0) { - out.write(0x48); + if ((opCode & ObexHelper.OBEX_OPCODE_FINAL_BIT_MASK) == 0) { + out.write(HeaderSet.BODY); } else { - out.write(0x49); + out.write(HeaderSet.END_OF_BODY); mEndOfBodySent = true; - } bodyLength = 3; @@ -531,15 +565,20 @@ public final class ClientOperation implements Operation, BaseStream { } if (out.size() == 0) { - if (!mParent.sendRequest(opCode, null, mReplyHeader, mPrivateInput)) { + if (!mParent.sendRequest(opCode, null, mReplyHeader, mPrivateInput, mSrmActive)) { return false; } + // Enable SRM if it should be enabled + checkForSrm(); return returnValue; } if ((out.size() > 0) - && (!mParent.sendRequest(opCode, out.toByteArray(), mReplyHeader, mPrivateInput))) { + && (!mParent.sendRequest(opCode, out.toByteArray(), + mReplyHeader, mPrivateInput, mSrmActive))) { return false; } + // Enable SRM if it should be enabled + checkForSrm(); // send all of the output data in 0x48, // send 0x49 with empty body @@ -549,6 +588,35 @@ public final class ClientOperation implements Operation, BaseStream { return returnValue; } + private void checkForSrm() throws IOException { + Byte srmMode = (Byte)mReplyHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if(mParent.isSrmSupported() == true && srmMode != null + && srmMode == ObexHelper.OBEX_SRM_ENABLE) { + mSrmEnabled = true; + } + /** + * Call this only when a complete obex packet have been received. + * (This is not optimal, but the current design is not really suited to + * the way SRM is specified.) + * The BT usage of SRM is not really safe - it assumes that the SRMP will fit + * into every OBEX packet, hence if another header occupies the entire packet, + * the scheme will not work - unlikely though. + */ + if(mSrmEnabled) { + mSrmWaitingForRemote = false; + Byte srmp = (Byte)mReplyHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER); + if(srmp != null && srmp == ObexHelper.OBEX_SRMP_WAIT) { + mSrmWaitingForRemote = true; + // Clear the wait header, as the absence of the header in the next packet + // indicates don't wait anymore. + mReplyHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, null); + } + } + if((mSrmWaitingForRemote == false) && (mSrmEnabled == true)) { + mSrmActive = true; + } + } + /** * This method starts the processing thread results. It will send the * initial request. If the response takes more then one packet, a thread @@ -564,40 +632,35 @@ public final class ClientOperation implements Operation, BaseStream { if (mGetOperation) { if (!mOperationDone) { - if (!mGetFinalFlag) { - mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; - while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { - more = sendRequest(0x03); - } - - if (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { - mParent.sendRequest(0x83, null, mReplyHeader, mPrivateInput); - } - if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { - mOperationDone = true; - } - } else { - more = sendRequest(0x83); - - if (more) { - throw new IOException("FINAL_GET forced but data did not fit into single packet!"); - } - + mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; + while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { + more = sendRequest(ObexHelper.OBEX_OPCODE_GET); + } + // For GET we need to loop until all headers have been sent, + // And then we wait for the first continue package with the + // reply. + if (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { + mParent.sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL, + null, mReplyHeader, mPrivateInput, mSrmActive); + } + if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { mOperationDone = true; + } else { + checkForSrm(); } } } else { - + // PUT operation if (!mOperationDone) { mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { - more = sendRequest(0x02); - + more = sendRequest(ObexHelper.OBEX_OPCODE_PUT); } } if (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { - mParent.sendRequest(0x82, null, mReplyHeader, mPrivateInput); + mParent.sendRequest(ObexHelper.OBEX_OPCODE_PUT_FINAL, + null, mReplyHeader, mPrivateInput, mSrmActive); } if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { @@ -617,15 +680,21 @@ public final class ClientOperation implements Operation, BaseStream { public synchronized boolean continueOperation(boolean sendEmpty, boolean inStream) throws IOException { + // One path to the first put operation - the other one does not need to + // handle SRM, as all will fit into one packet. + if (mGetOperation) { if ((inStream) && (!mOperationDone)) { // to deal with inputstream in get operation - mParent.sendRequest(0x83, null, mReplyHeader, mPrivateInput); + mParent.sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL, + null, mReplyHeader, mPrivateInput, mSrmActive); /* * Determine if that was not the last packet in the operation */ if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { mOperationDone = true; + } else { + checkForSrm(); } return true; @@ -636,16 +705,7 @@ public final class ClientOperation implements Operation, BaseStream { if (mPrivateInput == null) { mPrivateInput = new PrivateInputStream(this); } - - if (!mGetFinalFlag) { - sendRequest(0x03); - } else { - sendRequest(0x83); - - if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { - mOperationDone = true; - } - } + sendRequest(ObexHelper.OBEX_OPCODE_GET); return true; } else if (mOperationDone) { @@ -653,12 +713,13 @@ public final class ClientOperation implements Operation, BaseStream { } } else { + // PUT operation if ((!inStream) && (!mOperationDone)) { // to deal with outputstream in put operation if (mReplyHeader.responseCode == -1) { mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; } - sendRequest(0x02); + sendRequest(ObexHelper.OBEX_OPCODE_PUT); return true; } else if ((inStream) && (!mOperationDone)) { // How to deal with inputstream in put operation ? @@ -696,7 +757,7 @@ public final class ClientOperation implements Operation, BaseStream { } while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { - more = sendRequest(0x02); + more = sendRequest(ObexHelper.OBEX_OPCODE_PUT); } /* @@ -706,7 +767,7 @@ public final class ClientOperation implements Operation, BaseStream { */ while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { - sendRequest(0x82); + sendRequest(ObexHelper.OBEX_OPCODE_PUT_FINAL); } mOperationDone = true; } else if ((inStream) && (mOperationDone)) { @@ -724,12 +785,14 @@ public final class ClientOperation implements Operation, BaseStream { } while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { - if (!sendRequest(0x83)) { + if (!sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL)) { break; } } while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { - mParent.sendRequest(0x83, null, mReplyHeader, mPrivateInput); + mParent.sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL, null, + mReplyHeader, mPrivateInput, false); + // Regardless of the SRM state, wait for the response. } mOperationDone = true; } else if ((!inStream) && (!mOperationDone)) { @@ -752,9 +815,9 @@ public final class ClientOperation implements Operation, BaseStream { mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { - more = sendRequest(0x03); + more = sendRequest(ObexHelper.OBEX_OPCODE_GET); } - sendRequest(0x83); + sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL); // parent.sendRequest(0x83, null, replyHeaders, privateInput); if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { mOperationDone = true; @@ -764,5 +827,6 @@ public final class ClientOperation implements Operation, BaseStream { } public void noBodyHeader(){ + mSendBodyHeader = false; } } diff --git a/obex/javax/obex/ClientSession.java b/obex/javax/obex/ClientSession.java index 27d8976..272a920 100644 --- a/obex/javax/obex/ClientSession.java +++ b/obex/javax/obex/ClientSession.java @@ -1,4 +1,6 @@ /* + * Copyright (c) 2015 The Android Open Source Project + * Copyright (C) 2015 Samsung LSI * Copyright (c) 2008-2009, Motorola, Inc. * * All rights reserved. @@ -37,12 +39,16 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import android.util.Log; + /** * This class in an implementation of the OBEX ClientSession. * @hide */ public final class ClientSession extends ObexSession { + private static final String TAG = "ClientSession"; + private boolean mOpen; // Determines if an OBEX layer connection has been established @@ -51,10 +57,10 @@ public final class ClientSession extends ObexSession { private byte[] mConnectionId = null; /* - * The max Packet size must be at least 256 according to the OBEX + * The max Packet size must be at least 255 according to the OBEX * specification. */ - private int maxPacketSize = 256; + private int mMaxTxPacketSize = ObexHelper.LOWER_LIMIT_MAX_PACKET_SIZE; private boolean mRequestActive; @@ -62,11 +68,33 @@ public final class ClientSession extends ObexSession { private final OutputStream mOutput; + private final boolean mLocalSrmSupported; + + private final ObexTransport mTransport; + public ClientSession(final ObexTransport trans) throws IOException { mInput = trans.openInputStream(); mOutput = trans.openOutputStream(); mOpen = true; mRequestActive = false; + mLocalSrmSupported = trans.isSrmSupported(); + mTransport = trans; + } + + /** + * Create a ClientSession + * @param trans The transport to use for OBEX transactions + * @param supportsSrm True if Single Response Mode should be used e.g. if the + * supplied transport is a TCP or l2cap channel. + * @throws IOException if it occurs while opening the transport streams. + */ + public ClientSession(final ObexTransport trans, final boolean supportsSrm) throws IOException { + mInput = trans.openInputStream(); + mOutput = trans.openOutputStream(); + mOpen = true; + mRequestActive = false; + mLocalSrmSupported = supportsSrm; + mTransport = trans; } public HeaderSet connect(final HeaderSet header) throws IOException { @@ -98,23 +126,25 @@ public final class ClientSession extends ObexSession { * Byte 7 to n: headers */ byte[] requestPacket = new byte[totalLength]; + int maxRxPacketSize = ObexHelper.getMaxRxPacketSize(mTransport); // We just need to start at byte 3 since the sendRequest() method will // handle the length and 0x80. requestPacket[0] = (byte)0x10; requestPacket[1] = (byte)0x00; - requestPacket[2] = (byte)(ObexHelper.MAX_PACKET_SIZE_INT >> 8); - requestPacket[3] = (byte)(ObexHelper.MAX_PACKET_SIZE_INT & 0xFF); + requestPacket[2] = (byte)(maxRxPacketSize >> 8); + requestPacket[3] = (byte)(maxRxPacketSize & 0xFF); if (head != null) { System.arraycopy(head, 0, requestPacket, 4, head.length); } - // check with local max packet size + // Since we are not yet connected, the peer max packet size is unknown, + // hence we are only guaranteed the server will use the first 7 bytes. if ((requestPacket.length + 3) > ObexHelper.MAX_PACKET_SIZE_INT) { - throw new IOException("Packet size exceeds max packet size"); + throw new IOException("Packet size exceeds max packet size for connect"); } HeaderSet returnHeaderSet = new HeaderSet(); - sendRequest(ObexHelper.OBEX_OPCODE_CONNECT, requestPacket, returnHeaderSet, null); + sendRequest(ObexHelper.OBEX_OPCODE_CONNECT, requestPacket, returnHeaderSet, null, false); /* * Read the response from the OBEX server. @@ -158,7 +188,18 @@ public final class ClientSession extends ObexSession { System.arraycopy(mConnectionId, 0, head.mConnectionID, 0, 4); } - return new ClientOperation(maxPacketSize, this, head, true); + if(mLocalSrmSupported) { + head.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, ObexHelper.OBEX_SRM_ENABLE); + /* TODO: Consider creating an interface to get the wait state. + * On an android system, I cannot see when this is to be used. + * except perhaps if we are to wait for user accept on a push message. + if(getLocalWaitState()) { + head.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, ObexHelper.OBEX_SRMP_WAIT); + } + */ + } + + return new ClientOperation(mMaxTxPacketSize, this, head, true); } /** @@ -202,7 +243,7 @@ public final class ClientSession extends ObexSession { } head = ObexHelper.createHeader(header, false); - if ((head.length + 3) > maxPacketSize) { + if ((head.length + 3) > mMaxTxPacketSize) { throw new IOException("Packet size exceeds max packet size"); } } else { @@ -215,7 +256,7 @@ public final class ClientSession extends ObexSession { } HeaderSet returnHeaderSet = new HeaderSet(); - sendRequest(ObexHelper.OBEX_OPCODE_DISCONNECT, head, returnHeaderSet, null); + sendRequest(ObexHelper.OBEX_OPCODE_DISCONNECT, head, returnHeaderSet, null, false); /* * An OBEX DISCONNECT reply from the server: @@ -269,7 +310,16 @@ public final class ClientSession extends ObexSession { System.arraycopy(mConnectionId, 0, head.mConnectionID, 0, 4); } - return new ClientOperation(maxPacketSize, this, head, false); + if(mLocalSrmSupported) { + head.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, ObexHelper.OBEX_SRM_ENABLE); + /* TODO: Consider creating an interface to get the wait state. + * On an android system, I cannot see when this is to be used. + if(getLocalWaitState()) { + head.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, ObexHelper.OBEX_SRMP_WAIT); + } + */ + } + return new ClientOperation(mMaxTxPacketSize, this, head, false); } public void setAuthenticator(Authenticator auth) throws IOException { @@ -314,7 +364,7 @@ public final class ClientSession extends ObexSession { head = ObexHelper.createHeader(headset, false); totalLength += head.length; - if (totalLength > maxPacketSize) { + if (totalLength > mMaxTxPacketSize) { throw new IOException("Packet size exceeds max packet size"); } @@ -348,7 +398,7 @@ public final class ClientSession extends ObexSession { } HeaderSet returnHeaderSet = new HeaderSet(); - sendRequest(ObexHelper.OBEX_OPCODE_SETPATH, packet, returnHeaderSet, null); + sendRequest(ObexHelper.OBEX_OPCODE_SETPATH, packet, returnHeaderSet, null, false); /* * An OBEX SETPATH reply from the server: @@ -400,20 +450,40 @@ public final class ClientSession extends ObexSession { * @param head the headers to send to the client * @param header the header object to update with the response * @param privateInput the input stream used by the Operation object; null - * if this is called on a CONNECT, SETPATH or DISCONNECT return + * if this is called on a CONNECT, SETPATH or DISCONNECT + * @return * true if the operation completed successfully; * false if an authentication response failed to pass * @throws IOException if an IO error occurs */ public boolean sendRequest(int opCode, byte[] head, HeaderSet header, - PrivateInputStream privateInput) throws IOException { + PrivateInputStream privateInput, boolean srmActive) throws IOException { //check header length with local max size if (head != null) { if ((head.length + 3) > ObexHelper.MAX_PACKET_SIZE_INT) { + // TODO: This is an implementation limit - not a specification requirement. throw new IOException("header too large "); } } + boolean skipSend = false; + boolean skipReceive = false; + if (srmActive == true) { + if (opCode == ObexHelper.OBEX_OPCODE_PUT) { + // we are in the middle of a SRM PUT operation, don't expect a continue. + skipReceive = true; + } else if (opCode == ObexHelper.OBEX_OPCODE_GET) { + // We are still sending the get request, send, but don't expect continue + // until the request is transfered (the final bit is set) + skipReceive = true; + } else if (opCode == ObexHelper.OBEX_OPCODE_GET_FINAL) { + // All done sending the request, expect data from the server, without + // sending continue. + skipSend = true; + } + + } + int bytesReceived; ByteArrayOutputStream out = new ByteArrayOutputStream(); out.write((byte)opCode); @@ -428,86 +498,105 @@ public final class ClientSession extends ObexSession { out.write(head); } - // Write the request to the output stream and flush the stream - mOutput.write(out.toByteArray()); - mOutput.flush(); + if (!skipSend) { + // Write the request to the output stream and flush the stream + mOutput.write(out.toByteArray()); + // TODO: is this really needed? if this flush is implemented + // correctly, we will get a gap between each obex packet. + // which is kind of the idea behind SRM to avoid. + // Consider offloading to another thread (async action) + mOutput.flush(); + } - header.responseCode = mInput.read(); + if (!skipReceive) { + header.responseCode = mInput.read(); - int length = ((mInput.read() << 8) | (mInput.read())); + int length = ((mInput.read() << 8) | (mInput.read())); - if (length > ObexHelper.MAX_PACKET_SIZE_INT) { - throw new IOException("Packet received exceeds packet size limit"); - } - if (length > ObexHelper.BASE_PACKET_LENGTH) { - byte[] data = null; - if (opCode == ObexHelper.OBEX_OPCODE_CONNECT) { - @SuppressWarnings("unused") - int version = mInput.read(); - @SuppressWarnings("unused") - int flags = mInput.read(); - maxPacketSize = (mInput.read() << 8) + mInput.read(); + if (length > ObexHelper.getMaxRxPacketSize(mTransport)) { + throw new IOException("Packet received exceeds packet size limit"); + } + if (length > ObexHelper.BASE_PACKET_LENGTH) { + byte[] data = null; + if (opCode == ObexHelper.OBEX_OPCODE_CONNECT) { + @SuppressWarnings("unused") + int version = mInput.read(); + @SuppressWarnings("unused") + int flags = mInput.read(); + mMaxTxPacketSize = (mInput.read() << 8) + mInput.read(); + + //check with local max size + if (mMaxTxPacketSize > ObexHelper.MAX_CLIENT_PACKET_SIZE) { + mMaxTxPacketSize = ObexHelper.MAX_CLIENT_PACKET_SIZE; + } - //check with local max size - if (maxPacketSize > ObexHelper.MAX_CLIENT_PACKET_SIZE) { - maxPacketSize = ObexHelper.MAX_CLIENT_PACKET_SIZE; - } + // check with transport maximum size + if(mMaxTxPacketSize > ObexHelper.getMaxTxPacketSize(mTransport)) { + // To increase this size, increase the buffer size in L2CAP layer + // in Bluedroid. + Log.w(TAG, "An OBEX packet size of " + mMaxTxPacketSize + "was" + + " requested. Transport only allows: " + + ObexHelper.getMaxTxPacketSize(mTransport) + + " Lowering limit to this value."); + mMaxTxPacketSize = ObexHelper.getMaxTxPacketSize(mTransport); + } - if (length > 7) { - data = new byte[length - 7]; + if (length > 7) { + data = new byte[length - 7]; - bytesReceived = mInput.read(data); - while (bytesReceived != (length - 7)) { - bytesReceived += mInput.read(data, bytesReceived, data.length - - bytesReceived); + bytesReceived = mInput.read(data); + while (bytesReceived != (length - 7)) { + bytesReceived += mInput.read(data, bytesReceived, data.length + - bytesReceived); + } + } else { + return true; } } else { - return true; - } - } else { - data = new byte[length - 3]; - bytesReceived = mInput.read(data); + data = new byte[length - 3]; + bytesReceived = mInput.read(data); - while (bytesReceived != (length - 3)) { - bytesReceived += mInput.read(data, bytesReceived, data.length - bytesReceived); - } - if (opCode == ObexHelper.OBEX_OPCODE_ABORT) { - return true; + while (bytesReceived != (length - 3)) { + bytesReceived += mInput.read(data, bytesReceived, data.length - bytesReceived); + } + if (opCode == ObexHelper.OBEX_OPCODE_ABORT) { + return true; + } } - } - byte[] body = ObexHelper.updateHeaderSet(header, data); - if ((privateInput != null) && (body != null)) { - privateInput.writeBytes(body, 1); - } + byte[] body = ObexHelper.updateHeaderSet(header, data); + if ((privateInput != null) && (body != null)) { + privateInput.writeBytes(body, 1); + } - if (header.mConnectionID != null) { - mConnectionId = new byte[4]; - System.arraycopy(header.mConnectionID, 0, mConnectionId, 0, 4); - } + if (header.mConnectionID != null) { + mConnectionId = new byte[4]; + System.arraycopy(header.mConnectionID, 0, mConnectionId, 0, 4); + } - if (header.mAuthResp != null) { - if (!handleAuthResp(header.mAuthResp)) { - setRequestInactive(); - throw new IOException("Authentication Failed"); + if (header.mAuthResp != null) { + if (!handleAuthResp(header.mAuthResp)) { + setRequestInactive(); + throw new IOException("Authentication Failed"); + } } - } - if ((header.responseCode == ResponseCodes.OBEX_HTTP_UNAUTHORIZED) - && (header.mAuthChall != null)) { + if ((header.responseCode == ResponseCodes.OBEX_HTTP_UNAUTHORIZED) + && (header.mAuthChall != null)) { - if (handleAuthChall(header)) { - out.write((byte)HeaderSet.AUTH_RESPONSE); - out.write((byte)((header.mAuthResp.length + 3) >> 8)); - out.write((byte)(header.mAuthResp.length + 3)); - out.write(header.mAuthResp); - header.mAuthChall = null; - header.mAuthResp = null; + if (handleAuthChall(header)) { + out.write((byte)HeaderSet.AUTH_RESPONSE); + out.write((byte)((header.mAuthResp.length + 3) >> 8)); + out.write((byte)(header.mAuthResp.length + 3)); + out.write(header.mAuthResp); + header.mAuthChall = null; + header.mAuthResp = null; - byte[] sendHeaders = new byte[out.size() - 3]; - System.arraycopy(out.toByteArray(), 3, sendHeaders, 0, sendHeaders.length); + byte[] sendHeaders = new byte[out.size() - 3]; + System.arraycopy(out.toByteArray(), 3, sendHeaders, 0, sendHeaders.length); - return sendRequest(opCode, sendHeaders, header, privateInput); + return sendRequest(opCode, sendHeaders, header, privateInput, false); + } } } } @@ -520,4 +609,8 @@ public final class ClientSession extends ObexSession { mInput.close(); mOutput.close(); } + + public boolean isSrmSupported() { + return mLocalSrmSupported; + } } diff --git a/obex/javax/obex/HeaderSet.java b/obex/javax/obex/HeaderSet.java index 51b560a..35fe186 100644 --- a/obex/javax/obex/HeaderSet.java +++ b/obex/javax/obex/HeaderSet.java @@ -40,7 +40,7 @@ import java.security.SecureRandom; /** * This class implements the javax.obex.HeaderSet interface for OBEX over - * RFCOMM. + * RFCOMM or OBEX over l2cap. * @hide */ public final class HeaderSet { @@ -178,6 +178,22 @@ public final class HeaderSet { */ public static final int OBJECT_CLASS = 0x4F; + /** + * Represents the OBEX Single Response Mode (SRM). This header is used + * for Single response mode, introduced in OBEX 1.5. + *

    + * The value of SINGLE_RESPONSE_MODE is 0x97 (151). + */ + public static final int SINGLE_RESPONSE_MODE = 0x97; + + /** + * Represents the OBEX Single Response Mode Parameters. This header is used + * for Single response mode, introduced in OBEX 1.5. + *

    + * The value of SINGLE_RESPONSE_MODE_PARAMETER is 0x98 (152). + */ + public static final int SINGLE_RESPONSE_MODE_PARAMETER = 0x98; + private Long mCount; // 4 byte unsigned integer private String mName; // null terminated Unicode text string @@ -204,7 +220,7 @@ public final class HeaderSet { private byte[] mObjectClass; // byte sequence - private String[] mUnicodeUserDefined; //null terminated unicode string + private String[] mUnicodeUserDefined; // null terminated unicode string private byte[][] mSequenceUserDefined; // byte sequence user defined @@ -212,7 +228,12 @@ public final class HeaderSet { private Long[] mIntegerUserDefined; // 4 byte unsigned integer - private final SecureRandom mRandom; + private SecureRandom mRandom = null; + + private Byte mSingleResponseMode; // byte to indicate enable/disable/support for SRM + + private Byte mSrmParam; // byte representing the SRM parameters - only "wait" + // is supported by Bluetooth /*package*/ byte[] nonce; @@ -234,7 +255,6 @@ public final class HeaderSet { mByteUserDefined = new Byte[16]; mIntegerUserDefined = new Long[16]; responseCode = -1; - mRandom = new SecureRandom(); } /** @@ -393,6 +413,30 @@ public final class HeaderSet { } } break; + case SINGLE_RESPONSE_MODE: + if (headerValue == null) { + mSingleResponseMode = null; + } else { + if (!(headerValue instanceof Byte)) { + throw new IllegalArgumentException( + "Single Response Mode must be a Byte"); + } else { + mSingleResponseMode = (Byte)headerValue; + } + } + break; + case SINGLE_RESPONSE_MODE_PARAMETER: + if (headerValue == null) { + mSrmParam = null; + } else { + if (!(headerValue instanceof Byte)) { + throw new IllegalArgumentException( + "Single Response Mode Parameter must be a Byte"); + } else { + mSrmParam = (Byte)headerValue; + } + } + break; default: // Verify that it was not a Unicode String user Defined if ((headerID >= 0x30) && (headerID <= 0x3F)) { @@ -493,6 +537,10 @@ public final class HeaderSet { return mObjectClass; case APPLICATION_PARAMETER: return mAppParam; + case SINGLE_RESPONSE_MODE: + return mSingleResponseMode; + case SINGLE_RESPONSE_MODE_PARAMETER: + return mSrmParam; default: // Verify that it was not a Unicode String user Defined if ((headerID >= 0x30) && (headerID <= 0x3F)) { @@ -564,6 +612,12 @@ public final class HeaderSet { if (mObjectClass != null) { out.write(OBJECT_CLASS); } + if(mSingleResponseMode != null) { + out.write(SINGLE_RESPONSE_MODE); + } + if(mSrmParam != null) { + out.write(SINGLE_RESPONSE_MODE_PARAMETER); + } for (int i = 0x30; i < 0x40; i++) { if (mUnicodeUserDefined[i - 0x30] != null) { @@ -625,6 +679,9 @@ public final class HeaderSet { throws IOException { nonce = new byte[16]; + if(mRandom == null) { + mRandom = new SecureRandom(); + } for (int i = 0; i < 16; i++) { nonce[i] = (byte)mRandom.nextInt(); } diff --git a/obex/javax/obex/ObexHelper.java b/obex/javax/obex/ObexHelper.java index 0a06709..fa50943 100644 --- a/obex/javax/obex/ObexHelper.java +++ b/obex/javax/obex/ObexHelper.java @@ -1,5 +1,6 @@ /* - * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2015 Samsung LSI * Copyright (c) 2008-2009, Motorola, Inc. * * All rights reserved. @@ -42,12 +43,16 @@ import java.util.Calendar; import java.util.Date; import java.util.TimeZone; +import android.util.Log; + /** * This class defines a set of helper methods for the implementation of Obex. * @hide */ public final class ObexHelper { + private static final String TAG = "ObexHelper"; + public static final boolean VDBG = false; /** * Defines the basic packet length used by OBEX. Every OBEX packet has the * same basic format:
    @@ -65,18 +70,24 @@ public final class ObexHelper { * should be the Max incoming MTU minus TODO: L2CAP package headers and * RFCOMM package headers. TODO: Retrieve the max incoming MTU from TODO: * LocalDevice.getProperty(). + * NOTE: This value must be larger than or equal to the L2CAP SDU */ /* * android note set as 0xFFFE to match remote MPS */ public static final int MAX_PACKET_SIZE_INT = 0xFFFE; + // The minimum allowed max packet size is 255 according to the OBEX specification + public static final int LOWER_LIMIT_MAX_PACKET_SIZE = 255; + /** * Temporary workaround to be able to push files to Windows 7. * TODO: Should be removed as soon as Microsoft updates their driver. */ public static final int MAX_CLIENT_PACKET_SIZE = 0xFC00; + public static final int OBEX_OPCODE_FINAL_BIT_MASK = 0x80; + public static final int OBEX_OPCODE_CONNECT = 0x80; public static final int OBEX_OPCODE_DISCONNECT = 0x81; @@ -119,6 +130,12 @@ public final class ObexHelper { public static final int OBEX_AUTH_REALM_CHARSET_UNICODE = 0xFF; + public static final byte OBEX_SRM_ENABLE = 0x01; // For BT we only need enable/disable + public static final byte OBEX_SRM_DISABLE = 0x00; + public static final byte OBEX_SRM_SUPPORT = 0x02; // Unused for now + + public static final byte OBEX_SRMP_WAIT = 0x01; // Only SRMP value used by BT + /** * Updates the HeaderSet with the headers received in the byte array * provided. Invalid headers are ignored. @@ -314,7 +331,7 @@ public final class ObexHelper { } } catch (Exception e) { // Not a valid header so ignore - throw new IOException("Header was not formatted properly"); + throw new IOException("Header was not formatted properly", e); } index += 4; break; @@ -322,7 +339,7 @@ public final class ObexHelper { } } catch (IOException e) { - throw new IOException("Header was not formatted properly"); + throw new IOException("Header was not formatted properly", e); } return body; @@ -672,6 +689,33 @@ public final class ObexHelper { } } + // TODO: + // If the SRM and SRMP header is in use, they must be send in the same OBEX packet + // But the current structure of the obex code cannot handle this, and therefore + // it makes sense to put them in the tail of the headers, since we then reduce the + // chance of enabling SRM to soon. The down side is that SRM cannot be used while + // transferring non-body headers + + // Add the SRM header + byteHeader = (Byte)headImpl.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if (byteHeader != null) { + out.write((byte)HeaderSet.SINGLE_RESPONSE_MODE); + out.write(byteHeader.byteValue()); + if (nullOut) { + headImpl.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, null); + } + } + + // Add the SRM parameter header + byteHeader = (Byte)headImpl.getHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER); + if (byteHeader != null) { + out.write((byte)HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER); + out.write(byteHeader.byteValue()); + if (nullOut) { + headImpl.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, null); + } + } + } catch (IOException e) { } finally { result = out.toByteArray(); @@ -702,6 +746,8 @@ public final class ObexHelper { int index = start; int length = 0; + // TODO: Ensure SRM and SRMP headers are not split into two OBEX packets + while ((fullLength < maxSize) && (index < headerArray.length)) { int headerID = (headerArray[index] < 0 ? headerArray[index] + 256 : headerArray[index]); lastLength = fullLength; @@ -1008,4 +1054,39 @@ public final class ObexHelper { return authChall; } + + /** + * Return the maximum allowed OBEX packet to transmit. + * OBEX packets transmitted must be smaller than this value. + * @param transport Reference to the ObexTransport in use. + * @return the maximum allowed OBEX packet to transmit + */ + public static int getMaxTxPacketSize(ObexTransport transport) { + int size = transport.getMaxTransmitPacketSize(); + return validateMaxPacketSize(size); + } + + /** + * Return the maximum allowed OBEX packet to receive - used in OBEX connect. + * @param transport + * @return he maximum allowed OBEX packet to receive + */ + public static int getMaxRxPacketSize(ObexTransport transport) { + int size = transport.getMaxReceivePacketSize(); + return validateMaxPacketSize(size); + } + + private static int validateMaxPacketSize(int size) { + if(VDBG && (size > MAX_PACKET_SIZE_INT)) Log.w(TAG, + "The packet size supported for the connection (" + size + ") is larger" + + " than the configured OBEX packet size: " + MAX_PACKET_SIZE_INT); + if(size != -1) { + if(size < LOWER_LIMIT_MAX_PACKET_SIZE) { + throw new IllegalArgumentException(size + " is less that the lower limit: " + + LOWER_LIMIT_MAX_PACKET_SIZE); + } + return size; + } + return MAX_PACKET_SIZE_INT; + } } diff --git a/obex/javax/obex/ObexPacket.java b/obex/javax/obex/ObexPacket.java new file mode 100644 index 0000000..bb6c96e --- /dev/null +++ b/obex/javax/obex/ObexPacket.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2015 The Android Open Source Project + * Copyright (c) 2015 Samsung LSI + * + * 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 javax.obex; + +import java.io.IOException; +import java.io.InputStream; + +public class ObexPacket { + public int mHeaderId; + public int mLength; + public byte[] mPayload = null; + + private ObexPacket(int headerId, int length) { + mHeaderId = headerId; + mLength = length; + } + + /** + * Create a complete OBEX packet by reading data from an InputStream. + * @param is the input stream to read from. + * @return the OBEX packet read. + * @throws IOException if an IO exception occurs during read. + */ + public static ObexPacket read(InputStream is) throws IOException { + int headerId = is.read(); + return read(headerId, is); + } + + /** + * Read the remainder of an OBEX packet, with a specified headerId. + * @param headerId the headerId already read from the stream. + * @param is the stream to read from, assuming 1 byte have already been read. + * @return the OBEX packet read. + * @throws IOException + */ + public static ObexPacket read(int headerId, InputStream is) throws IOException { + // Read the 2 byte length field from the stream + int length = is.read(); + length = (length << 8) + is.read(); + + ObexPacket newPacket = new ObexPacket(headerId, length); + + int bytesReceived; + byte[] temp = null; + if (length > 3) { + // First three bytes already read, compensating for this + temp = new byte[length - 3]; + bytesReceived = is.read(temp); + while (bytesReceived != temp.length) { + bytesReceived += is.read(temp, bytesReceived, temp.length - bytesReceived); + } + } + newPacket.mPayload = temp; + return newPacket; + } +} diff --git a/obex/javax/obex/ObexSession.java b/obex/javax/obex/ObexSession.java index a7daeb5..542b9c8 100644 --- a/obex/javax/obex/ObexSession.java +++ b/obex/javax/obex/ObexSession.java @@ -34,6 +34,8 @@ package javax.obex; import java.io.IOException; +import android.util.Log; + /** * The ObexSession interface characterizes the term * "OBEX Connection" as defined in the IrDA Object Exchange Protocol v1.2, which @@ -47,6 +49,9 @@ import java.io.IOException; */ public class ObexSession { + private static final String TAG = "ObexSession"; + private static final boolean V = ObexHelper.VDBG; + protected Authenticator mAuthenticator; protected byte[] mChallengeDigest; @@ -125,6 +130,7 @@ public class ObexSession { result = mAuthenticator .onAuthenticationChallenge(realm, isUserIDRequired, isFullAccess); } catch (Exception e) { + if (V) Log.d(TAG, "Exception occured - returning false", e); return false; } diff --git a/obex/javax/obex/ObexTransport.java b/obex/javax/obex/ObexTransport.java index 445e267..a5a75f5 100644 --- a/obex/javax/obex/ObexTransport.java +++ b/obex/javax/obex/ObexTransport.java @@ -73,4 +73,39 @@ public interface ObexTransport { DataOutputStream openDataOutputStream() throws IOException; + /** + * Must return the maximum allowed OBEX packet that can be sent over + * the transport. For L2CAP this will be the Max SDU reported by the + * peer device. + * The returned value will be used to set the outgoing OBEX packet + * size. Therefore this value shall not change. + * For RFCOMM or other transport types where the OBEX packets size + * is unrelated to the transport packet size, return -1; + * @return the maximum allowed OBEX packet that can be send over + * the transport. Or -1 in case of don't care. + */ + int getMaxTransmitPacketSize(); + + /** + * Must return the maximum allowed OBEX packet that can be received over + * the transport. For L2CAP this will be the Max SDU configured for the + * L2CAP channel. + * The returned value will be used to validate the incoming packet size + * values. + * For RFCOMM or other transport types where the OBEX packets size + * is unrelated to the transport packet size, return -1; + * @return the maximum allowed OBEX packet that can be send over + * the transport. Or -1 in case of don't care. + */ + int getMaxReceivePacketSize(); + + /** + * Shall return true if the transport in use supports SRM. + * @return + * true if SRM operation is supported, and is to be enabled. + * false if SRM operations are not supported, or should not be used. + */ + boolean isSrmSupported(); + + } diff --git a/obex/javax/obex/ServerOperation.java b/obex/javax/obex/ServerOperation.java index fc441e0..56a675a 100644 --- a/obex/javax/obex/ServerOperation.java +++ b/obex/javax/obex/ServerOperation.java @@ -1,4 +1,5 @@ -/* +/* Copyright (c) 2015 The Android Open Source Project + * Copyright (C) 2015 Samsung LSI * Copyright (c) 2008-2009, Motorola, Inc. * * All rights reserved. @@ -39,6 +40,8 @@ import java.io.OutputStream; import java.io.DataOutputStream; import java.io.ByteArrayOutputStream; +import android.util.Log; + /** * This class implements the Operation interface for server side connections. *

    @@ -54,6 +57,10 @@ import java.io.ByteArrayOutputStream; */ public final class ServerOperation implements Operation, BaseStream { + private static final String TAG = "ServerOperation"; + + private static final boolean V = ObexHelper.VDBG; // Verbose debugging + public boolean isAborted; public HeaderSet requestHeader; @@ -78,6 +85,8 @@ public final class ServerOperation implements Operation, BaseStream { private PrivateOutputStream mPrivateOutput; + private ObexTransport mTransport; + private boolean mPrivateOutputOpen; private String mExceptionString; @@ -89,6 +98,19 @@ public final class ServerOperation implements Operation, BaseStream { private boolean mHasBody; private boolean mSendBodyHeader = true; + // Assume SRM disabled - needs to be explicit + // enabled by client + private boolean mSrmEnabled = false; + // A latch - when triggered, there is not way back ;-) + private boolean mSrmActive = false; + // Set to true when a SRM enable response have been send + private boolean mSrmResponseSent = false; + // keep waiting until final-bit is received in request + // to handle the case where the SRM enable header is in + // a different OBEX packet than the SRMP header. + private boolean mSrmWaitingForRemote = true; + // Why should we wait? - currently not exposed to apps. + private boolean mSrmLocalWait = false; /** * Creates new ServerOperation @@ -116,12 +138,14 @@ public final class ServerOperation implements Operation, BaseStream { mRequestFinished = false; mPrivateOutputOpen = false; mHasBody = false; - int bytesReceived; + ObexPacket packet; + mTransport = p.getTransport(); /* * Determine if this is a PUT request */ - if ((request == 0x02) || (request == 0x82)) { + if ((request == ObexHelper.OBEX_OPCODE_PUT) || + (request == ObexHelper.OBEX_OPCODE_PUT_FINAL)) { /* * It is a PUT request. */ @@ -130,13 +154,14 @@ public final class ServerOperation implements Operation, BaseStream { /* * Determine if the final bit is set */ - if ((request & 0x80) == 0) { + if ((request & ObexHelper.OBEX_OPCODE_FINAL_BIT_MASK) == 0) { finalBitSet = false; } else { finalBitSet = true; mRequestFinished = true; } - } else if ((request == 0x03) || (request == 0x83)) { + } else if ((request == ObexHelper.OBEX_OPCODE_GET) || + (request == ObexHelper.OBEX_OPCODE_GET_FINAL)) { /* * It is a GET request. */ @@ -145,71 +170,32 @@ public final class ServerOperation implements Operation, BaseStream { // For Get request, final bit set is decided by server side logic finalBitSet = false; - if (request == 0x83) { + if (request == ObexHelper.OBEX_OPCODE_GET_FINAL) { mRequestFinished = true; } } else { throw new IOException("ServerOperation can not handle such request"); } - int length = in.read(); - length = (length << 8) + in.read(); + packet = ObexPacket.read(request, mInput); /* * Determine if the packet length is larger than this device can receive */ - if (length > ObexHelper.MAX_PACKET_SIZE_INT) { + if (packet.mLength > ObexHelper.getMaxRxPacketSize(mTransport)) { mParent.sendResponse(ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE, null); - throw new IOException("Packet received was too large"); + throw new IOException("Packet received was too large. Length: " + + packet.mLength + " maxLength: " + ObexHelper.getMaxRxPacketSize(mTransport)); } /* * Determine if any headers were sent in the initial request */ - if (length > 3) { - byte[] data = new byte[length - 3]; - bytesReceived = in.read(data); - - while (bytesReceived != data.length) { - bytesReceived += in.read(data, bytesReceived, data.length - bytesReceived); + if (packet.mLength > 3) { + if(!handleObexPacket(packet)) { + return; } - - byte[] body = ObexHelper.updateHeaderSet(requestHeader, data); - - if (body != null) { - mHasBody = true; - } - - if (mListener.getConnectionId() != -1 && requestHeader.mConnectionID != null) { - mListener.setConnectionId(ObexHelper.convertToLong(requestHeader.mConnectionID)); - } else { - mListener.setConnectionId(1); - } - - if (requestHeader.mAuthResp != null) { - if (!mParent.handleAuthResp(requestHeader.mAuthResp)) { - mExceptionString = "Authentication Failed"; - mParent.sendResponse(ResponseCodes.OBEX_HTTP_UNAUTHORIZED, null); - mClosed = true; - requestHeader.mAuthResp = null; - return; - } - } - - if (requestHeader.mAuthChall != null) { - mParent.handleAuthChall(requestHeader); - // send the authResp to the client - replyHeader.mAuthResp = new byte[requestHeader.mAuthResp.length]; - System.arraycopy(requestHeader.mAuthResp, 0, replyHeader.mAuthResp, 0, - replyHeader.mAuthResp.length); - requestHeader.mAuthResp = null; - requestHeader.mAuthChall = null; - - } - - if (body != null) { - mPrivateInput.writeBytes(body, 1); - } else { + if (!mHasBody) { while ((!mGetOperation) && (!finalBitSet)) { sendReply(ResponseCodes.OBEX_HTTP_CONTINUE); if (mPrivateInput.available() > 0) { @@ -232,6 +218,100 @@ public final class ServerOperation implements Operation, BaseStream { } } + /** + * Parse headers and update member variables + * @param packet the received obex packet + * @return false for failing authentication - and a OBEX_HTTP_UNAUTHORIZED + * response have been send. Else true. + * @throws IOException + */ + private boolean handleObexPacket(ObexPacket packet) throws IOException { + byte[] body = updateRequestHeaders(packet); + + if (body != null) { + mHasBody = true; + } + if (mListener.getConnectionId() != -1 && requestHeader.mConnectionID != null) { + mListener.setConnectionId(ObexHelper + .convertToLong(requestHeader.mConnectionID)); + } else { + mListener.setConnectionId(1); + } + + if (requestHeader.mAuthResp != null) { + if (!mParent.handleAuthResp(requestHeader.mAuthResp)) { + mExceptionString = "Authentication Failed"; + mParent.sendResponse(ResponseCodes.OBEX_HTTP_UNAUTHORIZED, null); + mClosed = true; + requestHeader.mAuthResp = null; + return false; + } + requestHeader.mAuthResp = null; + } + + if (requestHeader.mAuthChall != null) { + mParent.handleAuthChall(requestHeader); + // send the auhtResp to the client + replyHeader.mAuthResp = new byte[requestHeader.mAuthResp.length]; + System.arraycopy(requestHeader.mAuthResp, 0, replyHeader.mAuthResp, 0, + replyHeader.mAuthResp.length); + requestHeader.mAuthResp = null; + requestHeader.mAuthChall = null; + } + + if (body != null) { + mPrivateInput.writeBytes(body, 1); + } + return true; + } + + /** + * Update the request header set, and sniff on SRM headers to update local state. + * @param data the OBEX packet data + * @return any bytes in a body/end-of-body header returned by {@link ObexHelper.updateHeaderSet} + * @throws IOException + */ + private byte[] updateRequestHeaders(ObexPacket packet) throws IOException { + byte[] body = null; + if (packet.mPayload != null) { + body = ObexHelper.updateHeaderSet(requestHeader, packet.mPayload); + } + Byte srmMode = (Byte)requestHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if(mTransport.isSrmSupported() && srmMode != null + && srmMode == ObexHelper.OBEX_SRM_ENABLE) { + mSrmEnabled = true; + if(V) Log.d(TAG,"SRM is now ENABLED (but not active) for this operation"); + } + checkForSrmWait(packet.mHeaderId); + if((!mSrmWaitingForRemote) && (mSrmEnabled)) { + if(V) Log.d(TAG,"SRM is now ACTIVE for this operation"); + mSrmActive = true; + } + return body; + } + + /** + * Call this only when a complete request have been received. + * (This is not optimal, but the current design is not really suited to + * the way SRM is specified.) + */ + private void checkForSrmWait(int headerId){ + if (mSrmEnabled && (headerId == ObexHelper.OBEX_OPCODE_GET + || headerId == ObexHelper.OBEX_OPCODE_GET_FINAL + || headerId == ObexHelper.OBEX_OPCODE_PUT)) { + try { + mSrmWaitingForRemote = false; + Byte srmp = (Byte)requestHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER); + if(srmp != null && srmp == ObexHelper.OBEX_SRMP_WAIT) { + mSrmWaitingForRemote = true; + // Clear the wait header, as the absents of the header when the final bit is set + // indicates don't wait. + requestHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, null); + } + } catch (IOException e) {if(V){Log.w(TAG,"Exception while extracting header",e);}} + } + } + public boolean isValidBody() { return mHasBody; } @@ -274,17 +354,19 @@ public final class ServerOperation implements Operation, BaseStream { /** * Sends a reply to the client. If the reply is a OBEX_HTTP_CONTINUE, it - * will wait for a response from the client before ending. + * will wait for a response from the client before ending unless SRM is active. * @param type the response code to send back to the client * @return true if the final bit was not set on the reply; * false if no reply was received because the operation - * ended, an abort was received, or the final bit was set in the - * reply + * ended, an abort was received, the final bit was set in the + * reply or SRM is active. * @throws IOException if an IO error occurs */ public synchronized boolean sendReply(int type) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); - int bytesReceived; + boolean skipSend = false; + boolean skipReceive = false; + boolean srmRespSendPending = false; long id = mListener.getConnectionId(); if (id == -1) { @@ -293,7 +375,19 @@ public final class ServerOperation implements Operation, BaseStream { replyHeader.mConnectionID = ObexHelper.convertToByteArray(id); } - byte[] headerArray = ObexHelper.createHeader(replyHeader, true); + if(mSrmEnabled && !mSrmResponseSent) { + // As we are not ensured that the SRM enable is in the first OBEX packet + // We must check for each reply. + if(V)Log.v(TAG, "mSrmEnabled==true, sending SRM enable response."); + replyHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, (byte)ObexHelper.OBEX_SRM_ENABLE); + srmRespSendPending = true; + } + + if(mSrmEnabled && !mGetOperation && mSrmLocalWait) { + replyHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, (byte)ObexHelper.OBEX_SRMP_WAIT); + } + + byte[] headerArray = ObexHelper.createHeader(replyHeader, true); // This clears the headers int bodyLength = -1; int orginalBodyLength = -1; @@ -347,6 +441,28 @@ public final class ServerOperation implements Operation, BaseStream { finalBitSet = true; } + if(mSrmActive) { + if(!mGetOperation && type == ResponseCodes.OBEX_HTTP_CONTINUE && + mSrmResponseSent == true) { + // we are in the middle of a SRM PUT operation, don't send a continue. + skipSend = true; + } else if(mGetOperation && mRequestFinished == false && mSrmResponseSent == true) { + // We are still receiving the get request, receive, but don't send continue. + skipSend = true; + } else if(mGetOperation && mRequestFinished == true) { + // All done receiving the GET request, send data to the client, without + // expecting a continue. + skipReceive = true; + } + if(V)Log.v(TAG, "type==" + type + " skipSend==" + skipSend + + " skipReceive==" + skipReceive); + } + if(srmRespSendPending) { + if(V)Log.v(TAG, + "SRM Enabled (srmRespSendPending == true)- sending SRM Enable response"); + mSrmResponseSent = true; + } + if ((finalBitSet) || (headerArray.length < (mMaxPacketLength - 20))) { if (bodyLength > 0) { /* @@ -387,7 +503,7 @@ public final class ServerOperation implements Operation, BaseStream { } if ((finalBitSet) && (type == ResponseCodes.OBEX_HTTP_OK) && (orginalBodyLength <= 0)) { - if(mSendBodyHeader == true) { + if(mSendBodyHeader) { out.write(0x49); orginalBodyLength = 3; out.write((byte)(orginalBodyLength >> 8)); @@ -395,107 +511,66 @@ public final class ServerOperation implements Operation, BaseStream { } } - mResponseSize = 3; - mParent.sendResponse(type, out.toByteArray()); + if(skipSend == false) { + mResponseSize = 3; + mParent.sendResponse(type, out.toByteArray()); + } if (type == ResponseCodes.OBEX_HTTP_CONTINUE) { - int headerID = mInput.read(); - int length = mInput.read(); - length = (length << 8) + mInput.read(); - if ((headerID != ObexHelper.OBEX_OPCODE_PUT) - && (headerID != ObexHelper.OBEX_OPCODE_PUT_FINAL) - && (headerID != ObexHelper.OBEX_OPCODE_GET) - && (headerID != ObexHelper.OBEX_OPCODE_GET_FINAL)) { - - if (length > 3) { - byte[] temp = new byte[length - 3]; - // First three bytes already read, compensating for this - bytesReceived = mInput.read(temp); - - while (bytesReceived != temp.length) { - bytesReceived += mInput.read(temp, bytesReceived, - temp.length - bytesReceived); - } - } - /* - * Determine if an ABORT was sent as the reply - */ - if (headerID == ObexHelper.OBEX_OPCODE_ABORT) { - mParent.sendResponse(ResponseCodes.OBEX_HTTP_OK, null); - mClosed = true; - isAborted = true; - mExceptionString = "Abort Received"; - throw new IOException("Abort Received"); - } else { - mParent.sendResponse(ResponseCodes.OBEX_HTTP_BAD_REQUEST, null); - mClosed = true; - mExceptionString = "Bad Request Received"; - throw new IOException("Bad Request Received"); - } + if(mGetOperation && skipReceive) { + // Here we need to check for and handle abort (throw an exception). + // Any other signal received should be discarded silently (only on server side) + checkSrmRemoteAbort(); } else { - - if ((headerID == ObexHelper.OBEX_OPCODE_PUT_FINAL)) { - finalBitSet = true; - } else if (headerID == ObexHelper.OBEX_OPCODE_GET_FINAL) { - mRequestFinished = true; - } - - /* - * Determine if the packet length is larger then this device can receive - */ - if (length > ObexHelper.MAX_PACKET_SIZE_INT) { - mParent.sendResponse(ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE, null); - throw new IOException("Packet received was too large"); - } - - /* - * Determine if any headers were sent in the initial request - */ - if (length > 3) { - byte[] data = new byte[length - 3]; - bytesReceived = mInput.read(data); - - while (bytesReceived != data.length) { - bytesReceived += mInput.read(data, bytesReceived, data.length - - bytesReceived); - } - byte[] body = ObexHelper.updateHeaderSet(requestHeader, data); - if (body != null) { - mHasBody = true; - } - if (mListener.getConnectionId() != -1 && requestHeader.mConnectionID != null) { - mListener.setConnectionId(ObexHelper - .convertToLong(requestHeader.mConnectionID)); + // Receive and handle data (only send reply if !skipSend) + // Read a complete OBEX Packet + ObexPacket packet = ObexPacket.read(mInput); + + int headerId = packet.mHeaderId; + if ((headerId != ObexHelper.OBEX_OPCODE_PUT) + && (headerId != ObexHelper.OBEX_OPCODE_PUT_FINAL) + && (headerId != ObexHelper.OBEX_OPCODE_GET) + && (headerId != ObexHelper.OBEX_OPCODE_GET_FINAL)) { + + /* + * Determine if an ABORT was sent as the reply + */ + if (headerId == ObexHelper.OBEX_OPCODE_ABORT) { + handleRemoteAbort(); } else { - mListener.setConnectionId(1); + // TODO:shall we send this if it occurs during SRM? Errata on the subject + mParent.sendResponse(ResponseCodes.OBEX_HTTP_BAD_REQUEST, null); + mClosed = true; + mExceptionString = "Bad Request Received"; + throw new IOException("Bad Request Received"); } + } else { - if (requestHeader.mAuthResp != null) { - if (!mParent.handleAuthResp(requestHeader.mAuthResp)) { - mExceptionString = "Authentication Failed"; - mParent.sendResponse(ResponseCodes.OBEX_HTTP_UNAUTHORIZED, null); - mClosed = true; - requestHeader.mAuthResp = null; - return false; - } - requestHeader.mAuthResp = null; + if ((headerId == ObexHelper.OBEX_OPCODE_PUT_FINAL)) { + finalBitSet = true; + } else if (headerId == ObexHelper.OBEX_OPCODE_GET_FINAL) { + mRequestFinished = true; } - if (requestHeader.mAuthChall != null) { - mParent.handleAuthChall(requestHeader); - // send the auhtResp to the client - replyHeader.mAuthResp = new byte[requestHeader.mAuthResp.length]; - System.arraycopy(requestHeader.mAuthResp, 0, replyHeader.mAuthResp, 0, - replyHeader.mAuthResp.length); - requestHeader.mAuthResp = null; - requestHeader.mAuthChall = null; + /* + * Determine if the packet length is larger than the negotiated packet size + */ + if (packet.mLength > ObexHelper.getMaxRxPacketSize(mTransport)) { + mParent.sendResponse(ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE, null); + throw new IOException("Packet received was too large"); } - if (body != null) { - mPrivateInput.writeBytes(body, 1); + /* + * Determine if any headers were sent in the initial request + */ + if (packet.mLength > 3 || (mSrmEnabled && packet.mLength == 3)) { + if(handleObexPacket(packet) == false) { + return false; + } } } + } return true; } else { @@ -504,6 +579,53 @@ public final class ServerOperation implements Operation, BaseStream { } /** + * This method will look for an abort from the peer during a SRM transfer. + * The function will not block if no data has been received from the remote device. + * If data have been received, the function will block while reading the incoming + * OBEX package. + * An Abort request will be handled, and cause an IOException("Abort Received"). + * Other messages will be discarded silently as per GOEP specification. + * @throws IOException if an abort request have been received. + * TODO: I think this is an error in the specification. If we discard other messages, + * the peer device will most likely stall, as it will not receive the expected + * response for the message... + * I'm not sure how to understand "Receipt of invalid or unexpected SRM or SRMP + * header values shall be ignored by the receiving device." + * If any signal is received during an active SRM transfer it is unexpected regardless + * whether or not it contains SRM/SRMP headers... + */ + private void checkSrmRemoteAbort() throws IOException { + if(mInput.available() > 0) { + ObexPacket packet = ObexPacket.read(mInput); + /* + * Determine if an ABORT was sent as the reply + */ + if (packet.mHeaderId == ObexHelper.OBEX_OPCODE_ABORT) { + handleRemoteAbort(); + } else { + // TODO: should we throw an exception here anyway? - don't see how to + // ignore SRM/SRMP headers without ignoring the complete signal + // (in this particular case). + Log.w(TAG, "Received unexpected request from client - discarding...\n" + + " headerId: " + packet.mHeaderId + " length: " + packet.mLength); + } + } + } + + private void handleRemoteAbort() throws IOException { + /* TODO: To increase the speed of the abort operation in SRM, we need + * to be able to flush the L2CAP queue for the PSM in use. + * This could be implemented by introducing a control + * message to be send over the socket, that in the abort case + * could carry a flush command. */ + mParent.sendResponse(ResponseCodes.OBEX_HTTP_OK, null); + mClosed = true; + isAborted = true; + mExceptionString = "Abort Received"; + throw new IOException("Abort Received"); + } + + /** * Sends an ABORT message to the server. By calling this method, the * corresponding input and output streams will be closed along with this * object. diff --git a/obex/javax/obex/ServerRequestHandler.java b/obex/javax/obex/ServerRequestHandler.java index 0882572..09cbc2c 100644 --- a/obex/javax/obex/ServerRequestHandler.java +++ b/obex/javax/obex/ServerRequestHandler.java @@ -275,4 +275,13 @@ public class ServerRequestHandler { */ public void onClose() { } + + /** + * Override to add Single Response Mode support - e.g. if the supplied + * transport is l2cap. + * @return True if SRM is supported, else False + */ + public boolean isSrmSupported() { + return false; + } } diff --git a/obex/javax/obex/ServerSession.java b/obex/javax/obex/ServerSession.java index f1b9a0d..acee5dd 100644 --- a/obex/javax/obex/ServerSession.java +++ b/obex/javax/obex/ServerSession.java @@ -1,4 +1,6 @@ /* + * Copyright (C) 2015 The Android Open Source Project + * Copyright (c) 2015 Samsung LSI * Copyright (c) 2008-2009, Motorola, Inc. * * All rights reserved. @@ -45,6 +47,7 @@ import java.io.OutputStream; public final class ServerSession extends ObexSession implements Runnable { private static final String TAG = "Obex ServerSession"; + private static final boolean V = ObexHelper.VDBG; private ObexTransport mTransport; @@ -91,7 +94,9 @@ public final class ServerSession extends ObexSession implements Runnable { boolean done = false; while (!done && !mClosed) { + if(V) Log.v(TAG, "Waiting for incoming request..."); int requestType = mInput.read(); + if(V) Log.v(TAG, "Read request: " + requestType); switch (requestType) { case ObexHelper.OBEX_OPCODE_CONNECT: handleConnectRequest(); @@ -140,9 +145,9 @@ public final class ServerSession extends ObexSession implements Runnable { } } catch (NullPointerException e) { - Log.d(TAG, e.toString()); + Log.d(TAG, "Exception occured - ignoring", e); } catch (Exception e) { - Log.d(TAG, e.toString()); + Log.d(TAG, "Exception occured - ignoring", e); } close(); } @@ -163,7 +168,7 @@ public final class ServerSession extends ObexSession implements Runnable { int length = mInput.read(); length = (length << 8) + mInput.read(); - if (length > ObexHelper.MAX_PACKET_SIZE_INT) { + if (length > ObexHelper.getMaxRxPacketSize(mTransport)) { code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; } else { for (int i = 3; i < length; i++) { @@ -215,6 +220,7 @@ public final class ServerSession extends ObexSession implements Runnable { *internal error should not be sent because server has already replied with *OK response in "sendReply") */ + if(V) Log.d(TAG,"Exception occured - sending OBEX_HTTP_INTERNAL_ERROR reply",e); if (!op.isAborted) { sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); } @@ -243,6 +249,7 @@ public final class ServerSession extends ObexSession implements Runnable { op.sendReply(response); } } catch (Exception e) { + if(V) Log.d(TAG,"Exception occured - sending OBEX_HTTP_INTERNAL_ERROR reply",e); sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); } } @@ -275,7 +282,7 @@ public final class ServerSession extends ObexSession implements Runnable { data[2] = (byte)totalLength; } op.write(data); - op.flush(); + op.flush(); // TODO: Do we need to flush? } /** @@ -304,7 +311,7 @@ public final class ServerSession extends ObexSession implements Runnable { flags = mInput.read(); constants = mInput.read(); - if (length > ObexHelper.MAX_PACKET_SIZE_INT) { + if (length > ObexHelper.getMaxRxPacketSize(mTransport)) { code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; totalLength = 3; } else { @@ -358,6 +365,7 @@ public final class ServerSession extends ObexSession implements Runnable { try { code = mListener.onSetPath(request, reply, backup, create); } catch (Exception e) { + if(V) Log.d(TAG,"Exception occured - sending OBEX_HTTP_INTERNAL_ERROR reply",e); sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); return; } @@ -425,7 +433,7 @@ public final class ServerSession extends ObexSession implements Runnable { length = mInput.read(); length = (length << 8) + mInput.read(); - if (length > ObexHelper.MAX_PACKET_SIZE_INT) { + if (length > ObexHelper.getMaxRxPacketSize(mTransport)) { code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; totalLength = 3; } else { @@ -466,6 +474,7 @@ public final class ServerSession extends ObexSession implements Runnable { try { mListener.onDisconnect(request, reply); } catch (Exception e) { + if(V) Log.d(TAG,"Exception occured - sending OBEX_HTTP_INTERNAL_ERROR reply",e); sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); return; } @@ -531,23 +540,38 @@ public final class ServerSession extends ObexSession implements Runnable { HeaderSet reply = new HeaderSet(); int bytesReceived; + if(V) Log.v(TAG,"handleConnectRequest()"); + /* * Read in the length of the OBEX packet, OBEX version, flags, and max * packet length */ packetLength = mInput.read(); packetLength = (packetLength << 8) + mInput.read(); + if(V) Log.v(TAG,"handleConnectRequest() - packetLength: " + packetLength); + version = mInput.read(); flags = mInput.read(); mMaxPacketLength = mInput.read(); mMaxPacketLength = (mMaxPacketLength << 8) + mInput.read(); + if(V) Log.v(TAG,"handleConnectRequest() - version: " + version + + " MaxLength: " + mMaxPacketLength + " flags: " + flags); + // should we check it? if (mMaxPacketLength > ObexHelper.MAX_PACKET_SIZE_INT) { mMaxPacketLength = ObexHelper.MAX_PACKET_SIZE_INT; } - if (packetLength > ObexHelper.MAX_PACKET_SIZE_INT) { + if(mMaxPacketLength > ObexHelper.getMaxTxPacketSize(mTransport)) { + Log.w(TAG, "Requested MaxObexPacketSize " + mMaxPacketLength + + " is larger than the max size supported by the transport: " + + ObexHelper.getMaxTxPacketSize(mTransport) + + " Reducing to this size."); + mMaxPacketLength = ObexHelper.getMaxTxPacketSize(mTransport); + } + + if (packetLength > ObexHelper.getMaxRxPacketSize(mTransport)) { code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; totalLength = 7; } else { @@ -614,7 +638,7 @@ public final class ServerSession extends ObexSession implements Runnable { code = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } } catch (Exception e) { - e.printStackTrace(); + if(V) Log.d(TAG,"Exception occured - sending OBEX_HTTP_INTERNAL_ERROR reply",e); totalLength = 7; head = null; code = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; @@ -633,13 +657,14 @@ public final class ServerSession extends ObexSession implements Runnable { * Packet Length (Defined in MAX_PACKET_SIZE) Byte 7 to n: headers */ byte[] sendData = new byte[totalLength]; + int maxRxLength = ObexHelper.getMaxRxPacketSize(mTransport); sendData[0] = (byte)code; sendData[1] = length[2]; sendData[2] = length[3]; sendData[3] = (byte)0x10; sendData[4] = (byte)0x00; - sendData[5] = (byte)(ObexHelper.MAX_PACKET_SIZE_INT >> 8); - sendData[6] = (byte)(ObexHelper.MAX_PACKET_SIZE_INT & 0xFF); + sendData[5] = (byte)(maxRxLength >> 8); + sendData[6] = (byte)(maxRxLength & 0xFF); if (head != null) { System.arraycopy(head, 0, sendData, 7, head.length); @@ -659,11 +684,16 @@ public final class ServerSession extends ObexSession implements Runnable { mListener.onClose(); } try { - mInput.close(); - mOutput.close(); - mTransport.close(); + /* Set state to closed before interrupting the thread by closing the streams */ mClosed = true; + if(mInput != null) + mInput.close(); + if(mOutput != null) + mOutput.close(); + if(mTransport != null) + mTransport.close(); } catch (Exception e) { + if(V) Log.d(TAG,"Exception occured during close() - ignore",e); } mTransport = null; mInput = null; @@ -702,4 +732,7 @@ public final class ServerSession extends ObexSession implements Runnable { return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } + public ObexTransport getTransport() { + return mTransport; + } } -- cgit v1.1