blob: 04c94068ff495cf2845aaecbafb5daf31be69249 [file] [log] [blame]
/*
* 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 com.android.bluetooth.opp;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.SystemClock;
import android.util.Log;
import android.webkit.MimeTypeMap;
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.BluetoothObexTransport;
import com.android.bluetooth.btservice.MetricsLogger;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import javax.obex.HeaderSet;
import javax.obex.ObexTransport;
import javax.obex.Operation;
import javax.obex.ResponseCodes;
import javax.obex.ServerRequestHandler;
import javax.obex.ServerSession;
/**
* This class runs as an OBEX server
*/
public class BluetoothOppObexServerSession extends ServerRequestHandler
implements BluetoothOppObexSession {
private static final String TAG = "BtOppObexServer";
private static final boolean D = Constants.DEBUG;
private static final boolean V = Constants.VERBOSE;
private ObexTransport mTransport;
private Context mContext;
private Handler mCallback = null;
/* status when server is blocking for user/auto confirmation */
private boolean mServerBlocking = true;
/* the current transfer info */
private BluetoothOppShareInfo mInfo;
/* info id when we insert the record */
private int mLocalShareInfoId;
private int mAccepted = BluetoothShare.USER_CONFIRMATION_PENDING;
private boolean mInterrupted = false;
private ServerSession mSession;
private long mTimestamp;
private BluetoothOppReceiveFileInfo mFileInfo;
private WakeLock mPartialWakeLock;
boolean mTimeoutMsgSent = false;
private BluetoothOppService mBluetoothOppService;
private int mNumFilesAttemptedToReceive;
public BluetoothOppObexServerSession(Context context, ObexTransport transport,
BluetoothOppService service) {
mContext = context;
mTransport = transport;
mBluetoothOppService = service;
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
mPartialWakeLock.setReferenceCounted(false);
}
@Override
public void unblock() {
mServerBlocking = false;
}
/**
* Called when connection is accepted from remote, to retrieve the first
* Header then wait for user confirmation
*/
public void preStart() {
try {
if (D) {
Log.d(TAG, "Create ServerSession with transport " + mTransport.toString());
}
mSession = new ServerSession(mTransport, this, null);
} catch (IOException e) {
Log.e(TAG, "Create server session error" + e);
}
}
/**
* Called from BluetoothOppTransfer to start the "Transfer"
*/
@Override
public void start(Handler handler, int numShares) {
if (D) {
Log.d(TAG, "Start!");
}
mCallback = handler;
}
/**
* Called from BluetoothOppTransfer to cancel the "Transfer" Otherwise,
* server should end by itself.
*/
@Override
public void stop() {
/*
* TODO now we implement in a tough way, just close the socket.
* maybe need nice way
*/
if (D) {
Log.d(TAG, "Stop!");
}
mInterrupted = true;
if (mSession != null) {
try {
mSession.close();
mTransport.close();
} catch (IOException e) {
Log.e(TAG, "close mTransport error" + e);
}
}
mCallback = null;
mSession = null;
}
@Override
public void addShare(BluetoothOppShareInfo info) {
if (D) {
Log.d(TAG, "addShare for id " + info.mId);
}
mInfo = info;
mFileInfo = processShareInfo();
}
@Override
public int onPut(Operation op) {
if (D) {
Log.d(TAG, "onPut " + op.toString());
}
/* For multiple objects, reject further objects after the user denies the first one */
if (mAccepted == BluetoothShare.USER_CONFIRMATION_DENIED) {
return ResponseCodes.OBEX_HTTP_FORBIDDEN;
}
String destination;
if (mTransport instanceof BluetoothObexTransport) {
destination = ((BluetoothObexTransport) mTransport).getRemoteAddress();
} else {
destination = "FF:FF:FF:00:00:00";
}
boolean isAcceptlisted =
BluetoothOppManager.getInstance(mContext).isAcceptlisted(destination);
HeaderSet request;
String name, mimeType;
Long length;
try {
request = op.getReceivedHeader();
if (V) {
Constants.logHeader(request);
}
name = (String) request.getHeader(HeaderSet.NAME);
length = (Long) request.getHeader(HeaderSet.LENGTH);
mimeType = (String) request.getHeader(HeaderSet.TYPE);
} catch (IOException e) {
Log.e(TAG, "onPut: getReceivedHeaders error " + e);
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
}
if (length == 0) {
if (D) {
Log.w(TAG, "length is 0, reject the transfer");
}
return ResponseCodes.OBEX_HTTP_LENGTH_REQUIRED;
}
if (name == null || name.isEmpty()) {
if (D) {
Log.w(TAG, "name is null or empty, reject the transfer");
}
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
}
// First we look for the mime type in the Android map
String extension, type;
int dotIndex = name.lastIndexOf(".");
if (dotIndex < 0 && mimeType == null) {
if (D) {
Log.w(TAG, "There is no file extension or mime type, reject the transfer");
}
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
} else {
extension = name.substring(dotIndex + 1).toLowerCase();
MimeTypeMap map = MimeTypeMap.getSingleton();
type = map.getMimeTypeFromExtension(extension);
if (V) {
Log.v(TAG, "Mimetype guessed from extension " + extension + " is " + type);
}
if (type != null) {
mimeType = type;
} else {
if (mimeType == null) {
if (D) {
Log.w(TAG, "Can't get mimetype, reject the transfer");
}
return ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE;
}
}
mimeType = mimeType.toLowerCase();
}
// Reject anything outside the "acceptlist" plus unspecified MIME Types.
if (mimeType == null || (!isAcceptlisted && !Constants.mimeTypeMatches(mimeType,
Constants.ACCEPTABLE_SHARE_INBOUND_TYPES))) {
if (D) {
Log.w(TAG, "mimeType is null or in unacceptable list, reject the transfer");
}
return ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE;
}
ContentValues values = new ContentValues();
values.put(BluetoothShare.FILENAME_HINT, name);
values.put(BluetoothShare.TOTAL_BYTES, length);
values.put(BluetoothShare.MIMETYPE, mimeType);
values.put(BluetoothShare.DESTINATION, destination);
values.put(BluetoothShare.DIRECTION, BluetoothShare.DIRECTION_INBOUND);
values.put(BluetoothShare.TIMESTAMP, mTimestamp);
// It's not first put if !serverBlocking, so we auto accept it
if (!mServerBlocking && (mAccepted == BluetoothShare.USER_CONFIRMATION_CONFIRMED
|| mAccepted == BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED)) {
values.put(BluetoothShare.USER_CONFIRMATION,
BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED);
}
if (isAcceptlisted) {
values.put(BluetoothShare.USER_CONFIRMATION,
BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED);
}
Uri contentUri = mContext.getContentResolver().insert(BluetoothShare.CONTENT_URI, values);
mLocalShareInfoId = Integer.parseInt(contentUri.getPathSegments().get(1));
if (V) {
Log.v(TAG, "insert contentUri: " + contentUri);
Log.v(TAG, "mLocalShareInfoId = " + mLocalShareInfoId);
}
synchronized (this) {
mPartialWakeLock.acquire();
mServerBlocking = true;
try {
while (mServerBlocking) {
wait(1000);
if (mCallback != null && !mTimeoutMsgSent) {
mCallback.sendMessageDelayed(mCallback.obtainMessage(
BluetoothOppObexSession.MSG_CONNECT_TIMEOUT),
BluetoothOppObexSession.SESSION_TIMEOUT);
mTimeoutMsgSent = true;
if (V) {
Log.v(TAG, "MSG_CONNECT_TIMEOUT sent");
}
}
}
} catch (InterruptedException e) {
if (V) {
Log.v(TAG, "Interrupted in onPut blocking");
}
}
}
if (D) {
Log.d(TAG, "Server unblocked ");
}
synchronized (this) {
if (mCallback != null && mTimeoutMsgSent) {
mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
}
}
/* we should have mInfo now */
/*
* TODO check if this mInfo match the one that we insert before server
* blocking? just to make sure no error happens
*/
if (mInfo.mId != mLocalShareInfoId) {
Log.e(TAG, "Unexpected error!");
}
mAccepted = mInfo.mConfirm;
if (V) {
Log.v(TAG, "after confirm: userAccepted=" + mAccepted);
}
int status = BluetoothShare.STATUS_SUCCESS;
int obexResponse = ResponseCodes.OBEX_HTTP_OK;
if (mAccepted == BluetoothShare.USER_CONFIRMATION_CONFIRMED
|| mAccepted == BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED
|| mAccepted == BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED) {
/* Confirm or auto-confirm */
mNumFilesAttemptedToReceive++;
if (mFileInfo.mFileName == null) {
status = mFileInfo.mStatus;
/* TODO need to check if this line is correct */
mInfo.mStatus = mFileInfo.mStatus;
Constants.updateShareStatus(mContext, mInfo.mId, status);
obexResponse = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
}
if (mFileInfo.mFileName != null && mFileInfo.mInsertUri != null) {
ContentValues updateValues = new ContentValues();
contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId);
updateValues.put(BluetoothShare._DATA, mFileInfo.mFileName);
updateValues.put(BluetoothShare.STATUS, BluetoothShare.STATUS_RUNNING);
updateValues.put(BluetoothShare.URI, mFileInfo.mInsertUri.toString());
mContext.getContentResolver().update(contentUri, updateValues, null, null);
mInfo.mUri = mFileInfo.mInsertUri;
status = receiveFile(mFileInfo, op);
/*
* TODO map status to obex response code
*/
if (status != BluetoothShare.STATUS_SUCCESS) {
obexResponse = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
}
Constants.updateShareStatus(mContext, mInfo.mId, status);
}
if (status == BluetoothShare.STATUS_SUCCESS) {
Message msg = Message.obtain(mCallback, BluetoothOppObexSession.MSG_SHARE_COMPLETE);
msg.obj = mInfo;
msg.sendToTarget();
} else {
if (mCallback != null) {
Message msg =
Message.obtain(mCallback, BluetoothOppObexSession.MSG_SESSION_ERROR);
mInfo.mStatus = status;
msg.obj = mInfo;
msg.sendToTarget();
}
}
} else if (mAccepted == BluetoothShare.USER_CONFIRMATION_DENIED
|| mAccepted == BluetoothShare.USER_CONFIRMATION_TIMEOUT) {
/* user actively deny the inbound transfer */
/*
* Note There is a question: what's next if user deny the first obj?
* Option 1 :continue prompt for next objects
* Option 2 :reject next objects and finish the session
* Now we take option 2:
*/
Log.i(TAG, "Rejected incoming request");
if (mFileInfo.mInsertUri != null) {
mContext.getContentResolver().delete(mFileInfo.mInsertUri, null, null);
}
// set status as local cancel
status = BluetoothShare.STATUS_CANCELED;
Constants.updateShareStatus(mContext, mInfo.mId, status);
obexResponse = ResponseCodes.OBEX_HTTP_FORBIDDEN;
Message msg = Message.obtain(mCallback);
msg.what = BluetoothOppObexSession.MSG_SHARE_INTERRUPTED;
mInfo.mStatus = status;
msg.obj = mInfo;
msg.sendToTarget();
}
return obexResponse;
}
private int receiveFile(BluetoothOppReceiveFileInfo fileInfo, Operation op) {
/*
* implement receive file
*/
int status = -1;
OutputStream os = null;
InputStream is = null;
boolean error = false;
try {
is = op.openInputStream();
} catch (IOException e1) {
Log.e(TAG, "Error when openInputStream");
status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
error = true;
}
Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId);
if (!error) {
ContentValues updateValues = new ContentValues();
updateValues.put(BluetoothShare._DATA, fileInfo.mFileName);
mContext.getContentResolver().update(contentUri, updateValues, null, null);
}
long position = 0;
long percent;
long prevPercent = 0;
if (!error) {
try {
os = mContext.getContentResolver().openOutputStream(fileInfo.mInsertUri);
} catch (FileNotFoundException e) {
Log.e(TAG, "Error when openOutputStream");
error = true;
}
}
if (!error) {
int outputBufferSize = op.getMaxPacketSize();
byte[] b = new byte[outputBufferSize];
int readLength;
long timestamp = 0;
long currentTime;
long prevTimestamp = SystemClock.elapsedRealtime();
try {
while ((!mInterrupted) && (position != fileInfo.mLength)) {
if (V) {
timestamp = SystemClock.elapsedRealtime();
}
readLength = is.read(b);
if (readLength == -1) {
if (D) {
Log.d(TAG, "Receive file reached stream end at position" + position);
}
break;
}
os.write(b, 0, readLength);
position += readLength;
percent = position * 100 / fileInfo.mLength;
currentTime = SystemClock.elapsedRealtime();
if (V) {
Log.v(TAG,
"Receive file position = " + position + " readLength " + readLength
+ " bytes took " + (currentTime - timestamp) + " ms");
}
// Update the Progress Bar only if there is change in percentage
// or once per a period to notify NFC of this transfer is still alive
if (percent > prevPercent
|| currentTime - prevTimestamp > Constants.NFC_ALIVE_CHECK_MS) {
ContentValues updateValues = new ContentValues();
updateValues.put(BluetoothShare.CURRENT_BYTES, position);
mContext.getContentResolver().update(contentUri, updateValues, null, null);
prevPercent = percent;
prevTimestamp = currentTime;
}
}
} catch (IOException e1) {
Log.e(TAG, "Error when receiving file: " + e1);
/* OBEX Abort packet received from remote device */
if ("Abort Received".equals(e1.getMessage())) {
status = BluetoothShare.STATUS_CANCELED;
} else {
status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
}
error = true;
}
}
if (mInterrupted) {
if (D) {
Log.d(TAG, "receiving file interrupted by user.");
}
status = BluetoothShare.STATUS_CANCELED;
} else {
if (position == fileInfo.mLength) {
if (D) {
Log.d(TAG, "Receiving file completed for " + fileInfo.mFileName);
}
status = BluetoothShare.STATUS_SUCCESS;
} else {
if (D) {
Log.d(TAG, "Reading file failed at " + position + " of " + fileInfo.mLength);
}
if (status == -1) {
status = BluetoothShare.STATUS_UNKNOWN_ERROR;
}
}
}
if (os != null) {
try {
os.flush();
os.close();
} catch (IOException e) {
Log.e(TAG, "Error when closing stream after send");
}
}
BluetoothOppUtility.cancelNotification(mContext);
return status;
}
private BluetoothOppReceiveFileInfo processShareInfo() {
if (D) {
Log.d(TAG, "processShareInfo() " + mInfo.mId);
}
BluetoothOppReceiveFileInfo fileInfo =
BluetoothOppReceiveFileInfo.generateFileInfo(mContext, mInfo.mId);
if (V) {
Log.v(TAG, "Generate BluetoothOppReceiveFileInfo:");
Log.v(TAG, "filename :" + fileInfo.mFileName);
Log.v(TAG, "length :" + fileInfo.mLength);
Log.v(TAG, "status :" + fileInfo.mStatus);
}
return fileInfo;
}
@Override
public int onConnect(HeaderSet request, HeaderSet reply) {
if (D) {
Log.d(TAG, "onConnect");
}
if (V) {
Constants.logHeader(request);
}
Long objectCount = null;
try {
byte[] uuid = (byte[]) request.getHeader(HeaderSet.TARGET);
if (V) {
Log.v(TAG, "onConnect(): uuid =" + Arrays.toString(uuid));
}
if (uuid != null) {
return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
}
objectCount = (Long) request.getHeader(HeaderSet.COUNT);
} catch (IOException e) {
Log.e(TAG, e.toString());
return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
}
String destination;
if (mTransport instanceof BluetoothObexTransport) {
destination = ((BluetoothObexTransport) mTransport).getRemoteAddress();
} else {
destination = "FF:FF:FF:00:00:00";
}
boolean isHandover = BluetoothOppManager.getInstance(mContext).isAcceptlisted(destination);
if (isHandover) {
// Notify the handover requester file transfer has started
Intent intent = new Intent(Constants.ACTION_HANDOVER_STARTED);
if (objectCount != null) {
intent.putExtra(Constants.EXTRA_BT_OPP_OBJECT_COUNT, objectCount.intValue());
} else {
intent.putExtra(Constants.EXTRA_BT_OPP_OBJECT_COUNT,
Constants.COUNT_HEADER_UNAVAILABLE);
}
intent.putExtra(Constants.EXTRA_BT_OPP_ADDRESS, destination);
mContext.sendBroadcast(intent, Constants.HANDOVER_STATUS_PERMISSION);
}
mTimestamp = System.currentTimeMillis();
mNumFilesAttemptedToReceive = 0;
return ResponseCodes.OBEX_HTTP_OK;
}
@Override
public void onDisconnect(HeaderSet req, HeaderSet resp) {
if (D) {
Log.d(TAG, "onDisconnect");
}
if (mNumFilesAttemptedToReceive > 0) {
// Log incoming OPP transfer if more than one file is accepted by user
MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.OPP);
}
resp.responseCode = ResponseCodes.OBEX_HTTP_OK;
}
private synchronized void releaseWakeLocks() {
if (mPartialWakeLock.isHeld()) {
mPartialWakeLock.release();
}
}
@Override
public void onClose() {
if (D) {
Log.d(TAG, "onClose");
}
releaseWakeLocks();
mBluetoothOppService.acceptNewConnections();
BluetoothOppUtility.cancelNotification(mContext);
/* onClose could happen even before start() where mCallback is set */
if (mCallback != null) {
Message msg = Message.obtain(mCallback);
msg.what = BluetoothOppObexSession.MSG_SESSION_COMPLETE;
msg.obj = mInfo;
msg.sendToTarget();
}
}
}