| /* |
| * 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; |
| } |
| } |