/*
* Copyright (c) 2015 The Android Open Source Project
* Copyright (C) 2015 Samsung LSI
* Copyright (c) 2008-2009, Motorola, Inc.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* - Neither the name of the Motorola, Inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package javax.obex;
import java.io.ByteArrayOutputStream;
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
private boolean mObexConnected;
private byte[] mConnectionId = null;
/*
* The max Packet size must be at least 255 according to the OBEX
* specification.
*/
private int mMaxTxPacketSize = ObexHelper.LOWER_LIMIT_MAX_PACKET_SIZE;
private boolean mRequestActive;
private final InputStream mInput;
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 {
ensureOpen();
if (mObexConnected) {
throw new IOException("Already connected to server");
}
setRequestActive();
int totalLength = 4;
byte[] head = null;
// Determine the header byte array
if (header != null) {
if (header.nonce != null) {
mChallengeDigest = new byte[16];
System.arraycopy(header.nonce, 0, mChallengeDigest, 0, 16);
}
head = ObexHelper.createHeader(header, false);
totalLength += head.length;
}
/*
* Write the OBEX CONNECT packet to the server.
* Byte 0: 0x80
* Byte 1&2: Connect Packet Length
* Byte 3: OBEX Version Number (Presently, 0x10)
* Byte 4: Flags (For TCP 0x00)
* Byte 5&6: Max OBEX Packet Length (Defined in MAX_PACKET_SIZE)
* 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)(maxRxPacketSize >> 8);
requestPacket[3] = (byte)(maxRxPacketSize & 0xFF);
if (head != null) {
System.arraycopy(head, 0, requestPacket, 4, head.length);
}
// 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 for connect");
}
HeaderSet returnHeaderSet = new HeaderSet();
sendRequest(ObexHelper.OBEX_OPCODE_CONNECT, requestPacket, returnHeaderSet, null, false);
/*
* Read the response from the OBEX server.
* Byte 0: Response Code (If successful then OBEX_HTTP_OK)
* Byte 1&2: Packet Length
* Byte 3: OBEX Version Number
* Byte 4: Flags3
* Byte 5&6: Max OBEX packet Length
* Byte 7 to n: Optional HeaderSet
*/
if (returnHeaderSet.responseCode == ResponseCodes.OBEX_HTTP_OK) {
mObexConnected = true;
}
setRequestInactive();
return returnHeaderSet;
}
public Operation get(HeaderSet header) throws IOException {
if (!mObexConnected) {
throw new IOException("Not connected to the server");
}
setRequestActive();
ensureOpen();
HeaderSet head;
if (header == null) {
head = new HeaderSet();
} else {
head = header;
if (head.nonce != null) {
mChallengeDigest = new byte[16];
System.arraycopy(head.nonce, 0, mChallengeDigest, 0, 16);
}
}
// Add the connection ID if one exists
if (mConnectionId != null) {
head.mConnectionID = new byte[4];
System.arraycopy(mConnectionId, 0, head.mConnectionID, 0, 4);
}
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);
}
/**
* 0xCB Connection Id an identifier used for OBEX connection multiplexing
*/
public void setConnectionID(long id) {
if ((id < 0) || (id > 0xFFFFFFFFL)) {
throw new IllegalArgumentException("Connection ID is not in a valid range");
}
mConnectionId = ObexHelper.convertToByteArray(id);
}
public HeaderSet delete(HeaderSet header) throws IOException {
Operation op = put(header);
op.getResponseCode();
HeaderSet returnValue = op.getReceivedHeader();
op.close();
return returnValue;
}
public HeaderSet disconnect(HeaderSet header) throws IOException {
if (!mObexConnected) {
throw new IOException("Not connected to the server");
}
setRequestActive();
ensureOpen();
// Determine the header byte array
byte[] head = null;
if (header != null) {
if (header.nonce != null) {
mChallengeDigest = new byte[16];
System.arraycopy(header.nonce, 0, mChallengeDigest, 0, 16);
}
// Add the connection ID if one exists
if (mConnectionId != null) {
header.mConnectionID = new byte[4];
System.arraycopy(mConnectionId, 0, header.mConnectionID, 0, 4);
}
head = ObexHelper.createHeader(header, false);
if ((head.length + 3) > mMaxTxPacketSize) {
throw new IOException("Packet size exceeds max packet size");
}
} else {
// Add the connection ID if one exists
if (mConnectionId != null) {
head = new byte[5];
head[0] = (byte)HeaderSet.CONNECTION_ID;
System.arraycopy(mConnectionId, 0, head, 1, 4);
}
}
HeaderSet returnHeaderSet = new HeaderSet();
sendRequest(ObexHelper.OBEX_OPCODE_DISCONNECT, head, returnHeaderSet, null, false);
/*
* An OBEX DISCONNECT reply from the server:
* Byte 1: Response code
* Bytes 2 & 3: packet size
* Bytes 4 & up: headers
*/
/* response code , and header are ignored
* */
synchronized (this) {
mObexConnected = false;
setRequestInactive();
}
return returnHeaderSet;
}
public long getConnectionID() {
if (mConnectionId == null) {
return -1;
}
return ObexHelper.convertToLong(mConnectionId);
}
public Operation put(HeaderSet header) throws IOException {
if (!mObexConnected) {
throw new IOException("Not connected to the server");
}
setRequestActive();
ensureOpen();
HeaderSet head;
if (header == null) {
head = new HeaderSet();
} else {
head = header;
// when auth is initiated by client ,save the digest
if (head.nonce != null) {
mChallengeDigest = new byte[16];
System.arraycopy(head.nonce, 0, mChallengeDigest, 0, 16);
}
}
// Add the connection ID if one exists
if (mConnectionId != null) {
head.mConnectionID = new byte[4];
System.arraycopy(mConnectionId, 0, head.mConnectionID, 0, 4);
}
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 {
if (auth == null) {
throw new IOException("Authenticator may not be null");
}
mAuthenticator = auth;
}
public HeaderSet setPath(HeaderSet header, boolean backup, boolean create) throws IOException {
if (!mObexConnected) {
throw new IOException("Not connected to the server");
}
setRequestActive();
ensureOpen();
int totalLength = 2;
byte[] head = null;
HeaderSet headset;
if (header == null) {
headset = new HeaderSet();
} else {
headset = header;
if (headset.nonce != null) {
mChallengeDigest = new byte[16];
System.arraycopy(headset.nonce, 0, mChallengeDigest, 0, 16);
}
}
// when auth is initiated by client ,save the digest
if (headset.nonce != null) {
mChallengeDigest = new byte[16];
System.arraycopy(headset.nonce, 0, mChallengeDigest, 0, 16);
}
// Add the connection ID if one exists
if (mConnectionId != null) {
headset.mConnectionID = new byte[4];
System.arraycopy(mConnectionId, 0, headset.mConnectionID, 0, 4);
}
head = ObexHelper.createHeader(headset, false);
totalLength += head.length;
if (totalLength > mMaxTxPacketSize) {
throw new IOException("Packet size exceeds max packet size");
}
int flags = 0;
/*
* The backup flag bit is bit 0 so if we add 1, this will set that bit
*/
if (backup) {
flags++;
}
/*
* The create bit is bit 1 so if we or with 2 the bit will be set.
*/
if (!create) {
flags |= 2;
}
/*
* An OBEX SETPATH packet to the server:
* Byte 1: 0x85
* Byte 2 & 3: packet size
* Byte 4: flags
* Byte 5: constants
* Byte 6 & up: headers
*/
byte[] packet = new byte[totalLength];
packet[0] = (byte)flags;
packet[1] = (byte)0x00;
if (headset != null) {
System.arraycopy(head, 0, packet, 2, head.length);
}
HeaderSet returnHeaderSet = new HeaderSet();
sendRequest(ObexHelper.OBEX_OPCODE_SETPATH, packet, returnHeaderSet, null, false);
/*
* An OBEX SETPATH reply from the server:
* Byte 1: Response code
* Bytes 2 & 3: packet size
* Bytes 4 & up: headers
*/
setRequestInactive();
return returnHeaderSet;
}
/**
* Verifies that the connection is open.
* @throws IOException if the connection is closed
*/
public synchronized void ensureOpen() throws IOException {
if (!mOpen) {
throw new IOException("Connection closed");
}
}
/**
* Set request inactive. Allows Put and get operation objects to tell this
* object when they are done.
*/
/*package*/synchronized void setRequestInactive() {
mRequestActive = false;
}
/**
* Set request to active.
* @throws IOException if already active
*/
private synchronized void setRequestActive() throws IOException {
if (mRequestActive) {
throw new IOException("OBEX request is already being performed");
}
mRequestActive = true;
}
/**
* Sends a standard request to the client. It will then wait for the reply
* and update the header set object provided. If any authentication headers
* (i.e. authentication challenge or authentication response) are received,
* they will be processed.
* @param opCode the type of request to send to the client
* @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
* <code>true</code> if the operation completed successfully;
* <code>false</code> 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, 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);
// Determine if there are any headers to send
if (head == null) {
out.write(0x00);
out.write(0x03);
} else {
out.write((byte)((head.length + 3) >> 8));
out.write((byte)(head.length + 3));
out.write(head);
}
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();
}
if (!skipReceive) {
header.responseCode = mInput.read();
int length = ((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 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];
bytesReceived = mInput.read(data);
while (bytesReceived != (length - 7)) {
bytesReceived += mInput.read(data, bytesReceived, data.length
- bytesReceived);
}
} else {
return true;
}
} else {
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;
}
}
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.mAuthResp != null) {
if (!handleAuthResp(header.mAuthResp)) {
setRequestInactive();
throw new IOException("Authentication Failed");
}
}
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;
byte[] sendHeaders = new byte[out.size() - 3];
System.arraycopy(out.toByteArray(), 3, sendHeaders, 0, sendHeaders.length);
return sendRequest(opCode, sendHeaders, header, privateInput, false);
}
}
}
}
return true;
}
public void close() throws IOException {
mOpen = false;
mInput.close();
mOutput.close();
}
public boolean isSrmSupported() {
return mLocalSrmSupported;
}
}