/**
* BlueCove - Java library for Bluetooth
* Copyright (C) 2007-2009 Vlad Skarzhevskyy
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*
* @version $Id$
*/
package com.intel.bluetooth.obex;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import javax.obex.HeaderSet;
import javax.obex.Operation;
import javax.obex.ResponseCodes;
import com.intel.bluetooth.DebugLog;
abstract class OBEXClientOperation implements Operation, OBEXOperation, OBEXOperationReceive, OBEXOperationDelivery {
/**
* This is not 100% by JSR-82 doc. But some know implementations of OBEX are working this way. This solves the
* problems for Samsung phones that are sending nothing in response to GET request without final bit.
*
* Basically instead of sending at least two packets 'initial' and 'final' we are sending just 'final' one when
* applicable.
*/
final static boolean SHORT_REQUEST_PHASE = true;
protected OBEXClientSessionImpl session;
protected char operationId;
protected HeaderSet replyHeaders;
protected boolean isClosed;
protected boolean operationInProgress;
protected boolean operationInContinue;
protected OBEXOperationOutputStream outputStream;
protected boolean outputStreamOpened = false;
protected OBEXOperationInputStream inputStream;
protected boolean inputStreamOpened = false;
protected boolean errorReceived = false;
protected boolean requestEnded = false;
protected boolean finalBodyReceived = false;
protected OBEXHeaderSetImpl startOperationHeaders = null;
private boolean authenticationResponseCreated = false;
protected Object lock;
OBEXClientOperation(OBEXClientSessionImpl session, char operationId, OBEXHeaderSetImpl sendHeaders)
throws IOException {
this.session = session;
this.operationId = operationId;
this.isClosed = false;
this.operationInProgress = false;
this.lock = new Object();
this.inputStream = new OBEXOperationInputStream(this);
startOperation(sendHeaders);
}
static boolean isShortRequestPhase() {
return SHORT_REQUEST_PHASE;
}
protected void startOperation(OBEXHeaderSetImpl sendHeaders) throws IOException {
if (SHORT_REQUEST_PHASE) {
this.startOperationHeaders = sendHeaders;
} else {
this.operationInProgress = true;
exchangePacket(sendHeaders);
}
}
/*
* (non-Javadoc)
*
* @see com.intel.bluetooth.obex.OBEXOperationReceive#receiveData(com.intel.bluetooth.obex.OBEXOperationInputStream)
*/
public void receiveData(OBEXOperationInputStream is) throws IOException {
if (SHORT_REQUEST_PHASE) {
exchangePacket(this.startOperationHeaders);
this.startOperationHeaders = null;
} else {
exchangePacket(null);
}
}
/*
* (non-Javadoc)
*
* @see com.intel.bluetooth.obex.OBEXOperationDelivery#deliverPacket(boolean, byte[])
*/
public void deliverPacket(boolean finalPacket, byte[] buffer) throws IOException {
if (requestEnded) {
return;
}
if (SHORT_REQUEST_PHASE && (this.startOperationHeaders != null)) {
exchangePacket(this.startOperationHeaders);
this.startOperationHeaders = null;
}
int dataHeaderID = OBEXHeaderSetImpl.OBEX_HDR_BODY;
if (finalPacket) {
this.operationId |= OBEXOperationCodes.FINAL_BIT;
dataHeaderID = OBEXHeaderSetImpl.OBEX_HDR_BODY_END;
DebugLog.debug("client Request Phase ended");
requestEnded = true;
}
OBEXHeaderSetImpl dataHeaders = OBEXSessionBase.createOBEXHeaderSetImpl();
dataHeaders.setHeader(dataHeaderID, buffer);
exchangePacket(dataHeaders);
}
protected void endRequestPhase() throws IOException {
if (requestEnded) {
return;
}
DebugLog.debug("client ends Request Phase");
this.operationInProgress = false;
this.requestEnded = true;
this.operationId |= OBEXOperationCodes.FINAL_BIT;
if (SHORT_REQUEST_PHASE) {
exchangePacket(this.startOperationHeaders);
this.startOperationHeaders = null;
} else {
exchangePacket(null);
}
}
private void exchangePacket(OBEXHeaderSetImpl headers) throws IOException {
boolean success = false;
try {
session.writePacket(this.operationId, headers);
byte[] b = session.readPacket();
OBEXHeaderSetImpl dataHeaders = OBEXHeaderSetImpl.readHeaders(b[0], b, 3);
session.handleAuthenticationResponse(dataHeaders, null);
int responseCode = dataHeaders.getResponseCode();
DebugLog.debug0x("client operation got reply", OBEXUtils.toStringObexResponseCodes(responseCode), responseCode);
switch (responseCode) {
case ResponseCodes.OBEX_HTTP_UNAUTHORIZED:
if ((!authenticationResponseCreated) && (dataHeaders.hasAuthenticationChallenge())) {
DebugLog.debug("client resend request with auth response");
// Send the original data again, since it is not accepted
OBEXHeaderSetImpl retryHeaders = OBEXHeaderSetImpl.cloneHeaders(headers);
session.handleAuthenticationChallenge(dataHeaders, retryHeaders);
authenticationResponseCreated = true;
exchangePacket(retryHeaders);
} else {
this.errorReceived = true;
this.operationInContinue = false;
processIncommingHeaders(dataHeaders);
throw new IOException("Authentication Failure");
}
break;
case OBEXOperationCodes.OBEX_RESPONSE_SUCCESS:
processIncommingHeaders(dataHeaders);
processIncommingData(dataHeaders, true);
this.operationInProgress = false;
this.operationInContinue = false;
break;
case OBEXOperationCodes.OBEX_RESPONSE_CONTINUE:
processIncommingHeaders(dataHeaders);
processIncommingData(dataHeaders, false);
this.operationInContinue = true;
// if ((!authenticationResponseCreated) && (dataHeaders.hasAuthenticationChallenge())) {
// // Send the original data again, since it is not accepted = This is bug On Sony Ericsson
// DebugLog.debug("client resend request with auth response");
// OBEXHeaderSetImpl retryHeaders = OBEXHeaderSetImpl.cloneHeaders(headers);
// session.handleAuthenticationChallenge(dataHeaders, retryHeaders);
// authenticationResponseCreated = true;
// exchangePacket(retryHeaders);
// }
break;
default:
this.errorReceived = true;
this.operationInContinue = false;
// responseCode may be reported by getResponseCode()
processIncommingHeaders(dataHeaders);
processIncommingData(dataHeaders, true);
// OFF; Rely on getResponseCode() to report the error to the application.
// if ((this.operationId & OBEXOperationCodes.FINAL_BIT) == 0) {
// throw new IOException("Operation error, 0x" + Integer.toHexString(responseCode) + " "
// + OBEXUtils.toStringObexResponseCodes(responseCode));
// }
}
success = true;
} finally {
if (!success) {
errorReceived = true;
}
}
}
protected void processIncommingHeaders(HeaderSet dataHeaders) throws IOException {
if (replyHeaders != null) {
// accumulate all received headers.
OBEXHeaderSetImpl.appendHeaders(dataHeaders, replyHeaders);
}
// replyHeaders will contain responseCode from last reply
// The Body values are removed by cloneHeaders in getReceivedHeaders()
this.replyHeaders = dataHeaders;
}
protected void processIncommingData(HeaderSet dataHeaders, boolean eof) throws IOException {
byte[] data = (byte[]) dataHeaders.getHeader(OBEXHeaderSetImpl.OBEX_HDR_BODY);
if (data == null) {
data = (byte[]) dataHeaders.getHeader(OBEXHeaderSetImpl.OBEX_HDR_BODY_END);
if (data != null) {
finalBodyReceived = true;
eof = true;
}
}
if (data != null) {
DebugLog.debug("client received Data eof: " + eof + " len: ", data.length);
inputStream.appendData(data, eof);
} else if (eof) {
inputStream.appendData(null, eof);
}
}
/*
* (non-Javadoc)
*
* @see javax.obex.Operation#abort()
*/
public void abort() throws IOException {
validateOperationIsOpen();
if ((!this.operationInProgress) && (!this.operationInContinue)) {
throw new IOException("the transaction has already ended");
}
synchronized (lock) {
if (outputStream != null) {
outputStream.abort();
}
this.inputStream.close();
}
writeAbort();
}
private void writeAbort() throws IOException {
try {
session.writePacket(OBEXOperationCodes.ABORT, null);
requestEnded = true;
byte[] b = session.readPacket();
HeaderSet dataHeaders = OBEXHeaderSetImpl.readHeaders(b[0], b, 3);
if (dataHeaders.getResponseCode() != OBEXOperationCodes.OBEX_RESPONSE_SUCCESS) {
throw new IOException("Fails to abort operation, received "
+ OBEXUtils.toStringObexResponseCodes(dataHeaders.getResponseCode()));
}
} finally {
this.isClosed = true;
closeStream();
}
}
private void closeStream() throws IOException {
try {
receiveOperationEnd();
} finally {
this.operationInProgress = false;
inputStream.close();
closeOutputStream();
}
}
private void receiveOperationEnd() throws IOException {
while (!isClosed() && (operationInContinue)) {
DebugLog.debug("operation expects operation end");
receiveData(this.inputStream);
}
}
private void closeOutputStream() throws IOException {
if (outputStream != null) {
synchronized (lock) {
if (outputStream != null) {
outputStream.close();
}
outputStream = null;
}
}
}
protected void validateOperationIsOpen() throws IOException {
if (isClosed) {
throw new IOException("operation closed");
}
}
/*
* (non-Javadoc)
*
* @see javax.obex.Operation#getReceivedHeaders()
*/
public HeaderSet getReceivedHeaders() throws IOException {
validateOperationIsOpen();
endRequestPhase();
return OBEXHeaderSetImpl.cloneHeaders(this.replyHeaders);
}
/*
* (non-Javadoc)
*
* @see javax.obex.Operation#getResponseCode()
*
* A call will do an implicit close on the Stream and therefore signal that the request is done.
*/
public int getResponseCode() throws IOException {
validateOperationIsOpen();
endRequestPhase();
closeOutputStream();
receiveOperationEnd();
return this.replyHeaders.getResponseCode();
}
public void sendHeaders(HeaderSet headers) throws IOException {
if (headers == null) {
throw new NullPointerException("headers are null");
}
OBEXHeaderSetImpl.validateCreatedHeaderSet(headers);
validateOperationIsOpen();
if (this.requestEnded) {
throw new IOException("the request phase has already ended");
}
if (SHORT_REQUEST_PHASE && (this.startOperationHeaders != null)) {
exchangePacket(this.startOperationHeaders);
this.startOperationHeaders = null;
}
exchangePacket((OBEXHeaderSetImpl) headers);
}
/*
* (non-Javadoc)
*
* @see javax.microedition.io.ContentConnection#getEncoding() <code>getEncoding()</code> will always return
* <code>null</code>
*/
public String getEncoding() {
return null;
}
/*
* (non-Javadoc)
*
* @see javax.microedition.io.ContentConnection#getLength() <code>getLength()</code> will return the length
* specified by the OBEX Length header or -1 if the OBEX Length header was not included.
*/
public long getLength() {
Long len;
try {
len = (Long) replyHeaders.getHeader(HeaderSet.LENGTH);
} catch (IOException e) {
return -1;
}
if (len == null) {
return -1;
}
return len.longValue();
}
/*
* (non-Javadoc)
*
* @see javax.microedition.io.ContentConnection#getType() <code>getType()</code> will return the value specified in
* the OBEX Type header or <code>null</code> if the OBEX Type header was not included.
*/
public String getType() {
try {
return (String) replyHeaders.getHeader(HeaderSet.TYPE);
} catch (IOException e) {
return null;
}
}
public DataInputStream openDataInputStream() throws IOException {
return new DataInputStream(openInputStream());
}
public DataOutputStream openDataOutputStream() throws IOException {
return new DataOutputStream(openOutputStream());
}
/*
* (non-Javadoc)
*
* @see javax.microedition.io.Connection#close()
*/
public void close() throws IOException {
try {
endRequestPhase();
} finally {
closeStream();
if (!this.isClosed) {
this.isClosed = true;
DebugLog.debug("client operation closed");
}
}
}
public boolean isClosed() {
return this.isClosed || this.errorReceived;
}
}