blob: 750487bd9f4b5fc2c1366e180edc310f1515b436 [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 com.google.android.collect.Lists;
import javax.obex.ObexTransport;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.CharArrayBuffer;
import android.database.ContentObserver;
import android.database.Cursor;
import android.media.MediaScannerConnection;
import android.media.MediaScannerConnection.MediaScannerConnectionClient;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
import android.util.Log;
import android.os.Process;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
/**
* Performs the background Bluetooth OPP transfer. It also starts thread to
* accept incoming OPP connection.
*/
public class BluetoothOppService extends Service {
private static final boolean D = Constants.DEBUG;
private static final boolean V = Constants.VERBOSE;
private boolean userAccepted = false;
private class BluetoothShareContentObserver extends ContentObserver {
public BluetoothShareContentObserver() {
super(new Handler());
}
@Override
public void onChange(boolean selfChange) {
if (V) Log.v(TAG, "ContentObserver received notification");
updateFromProvider();
}
}
private static final String TAG = "BtOppService";
/** Observer to get notified when the content observer's data changes */
private BluetoothShareContentObserver mObserver;
/** Class to handle Notification Manager updates */
private BluetoothOppNotification mNotifier;
private boolean mPendingUpdate;
private UpdateThread mUpdateThread;
private ArrayList<BluetoothOppShareInfo> mShares;
private ArrayList<BluetoothOppBatch> mBatchs;
private BluetoothOppTransfer mTransfer;
private BluetoothOppTransfer mServerTransfer;
private int mBatchId;
/**
* Array used when extracting strings from content provider
*/
private CharArrayBuffer mOldChars;
/**
* Array used when extracting strings from content provider
*/
private CharArrayBuffer mNewChars;
private BluetoothAdapter mAdapter;
private PowerManager mPowerManager;
private BluetoothOppRfcommListener mSocketListener;
private boolean mListenStarted = false;
private boolean mMediaScanInProgress;
private int mIncomingRetries = 0;
private ObexTransport mPendingConnection = null;
/*
* TODO No support for queue incoming from multiple devices.
* Make an array list of server session to support receiving queue from
* multiple devices
*/
private BluetoothOppObexServerSession mServerSession;
@Override
public IBinder onBind(Intent arg0) {
throw new UnsupportedOperationException("Cannot bind to Bluetooth OPP Service");
}
@Override
public void onCreate() {
super.onCreate();
if (V) Log.v(TAG, "onCreate");
mAdapter = BluetoothAdapter.getDefaultAdapter();
mSocketListener = new BluetoothOppRfcommListener(mAdapter);
mShares = Lists.newArrayList();
mBatchs = Lists.newArrayList();
mObserver = new BluetoothShareContentObserver();
getContentResolver().registerContentObserver(BluetoothShare.CONTENT_URI, true, mObserver);
mBatchId = 1;
mNotifier = new BluetoothOppNotification(this);
mNotifier.mNotificationMgr.cancelAll();
mNotifier.updateNotification();
final ContentResolver contentResolver = getContentResolver();
new Thread("trimDatabase") {
public void run() {
trimDatabase(contentResolver);
}
}.start();
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(mBluetoothReceiver, filter);
synchronized (BluetoothOppService.this) {
if (mAdapter == null) {
Log.w(TAG, "Local BT device is not enabled");
} else {
startListener();
}
}
if (V) BluetoothOppPreference.getInstance(this).dump();
updateFromProvider();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (V) Log.v(TAG, "onStartCommand");
//int retCode = super.onStartCommand(intent, flags, startId);
//if (retCode == START_STICKY) {
if (mAdapter == null) {
Log.w(TAG, "Local BT device is not enabled");
} else {
startListener();
}
updateFromProvider();
//}
return START_NOT_STICKY;
}
private void startListener() {
if (!mListenStarted) {
if (mAdapter.isEnabled()) {
if (V) Log.v(TAG, "Starting RfcommListener");
mHandler.sendMessage(mHandler.obtainMessage(START_LISTENER));
mListenStarted = true;
}
}
}
private static final int START_LISTENER = 1;
private static final int MEDIA_SCANNED = 2;
private static final int MEDIA_SCANNED_FAILED = 3;
private static final int MSG_INCOMING_CONNECTION_RETRY = 4;
private static final int STOP_LISTENER = 200;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case STOP_LISTENER:
mSocketListener.stop();
mListenStarted = false;
synchronized (BluetoothOppService.this) {
if (mUpdateThread == null) {
stopSelf();
}
}
break;
case START_LISTENER:
if (mAdapter.isEnabled()) {
startSocketListener();
}
break;
case MEDIA_SCANNED:
if (V) Log.v(TAG, "Update mInfo.id " + msg.arg1 + " for data uri= "
+ msg.obj.toString());
ContentValues updateValues = new ContentValues();
Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + msg.arg1);
updateValues.put(Constants.MEDIA_SCANNED, Constants.MEDIA_SCANNED_SCANNED_OK);
updateValues.put(BluetoothShare.URI, msg.obj.toString()); // update
updateValues.put(BluetoothShare.MIMETYPE, getContentResolver().getType(
Uri.parse(msg.obj.toString())));
getContentResolver().update(contentUri, updateValues, null, null);
synchronized (BluetoothOppService.this) {
mMediaScanInProgress = false;
}
break;
case MEDIA_SCANNED_FAILED:
Log.v(TAG, "Update mInfo.id " + msg.arg1 + " for MEDIA_SCANNED_FAILED");
ContentValues updateValues1 = new ContentValues();
Uri contentUri1 = Uri.parse(BluetoothShare.CONTENT_URI + "/" + msg.arg1);
updateValues1.put(Constants.MEDIA_SCANNED,
Constants.MEDIA_SCANNED_SCANNED_FAILED);
getContentResolver().update(contentUri1, updateValues1, null, null);
synchronized (BluetoothOppService.this) {
mMediaScanInProgress = false;
}
break;
case BluetoothOppRfcommListener.MSG_INCOMING_BTOPP_CONNECTION:
if (D) Log.d(TAG, "Get incoming connection");
ObexTransport transport = (ObexTransport)msg.obj;
/*
* Strategy for incoming connections:
* 1. If there is no ongoing transfer, no on-hold connection, start it
* 2. If there is ongoing transfer, hold it for 20 seconds(1 seconds * 20 times)
* 3. If there is on-hold connection, reject directly
*/
if (mBatchs.size() == 0 && mPendingConnection == null) {
Log.i(TAG, "Start Obex Server");
createServerSession(transport);
} else {
if (mPendingConnection != null) {
Log.w(TAG, "OPP busy! Reject connection");
try {
transport.close();
} catch (IOException e) {
Log.e(TAG, "close tranport error");
}
} else if (Constants.USE_TCP_DEBUG && !Constants.USE_TCP_SIMPLE_SERVER) {
Log.i(TAG, "Start Obex Server in TCP DEBUG mode");
createServerSession(transport);
} else {
Log.i(TAG, "OPP busy! Retry after 1 second");
mIncomingRetries = mIncomingRetries + 1;
mPendingConnection = transport;
Message msg1 = Message.obtain(mHandler);
msg1.what = MSG_INCOMING_CONNECTION_RETRY;
mHandler.sendMessageDelayed(msg1, 1000);
}
}
break;
case MSG_INCOMING_CONNECTION_RETRY:
if (mBatchs.size() == 0) {
Log.i(TAG, "Start Obex Server");
createServerSession(mPendingConnection);
mIncomingRetries = 0;
mPendingConnection = null;
} else {
if (mIncomingRetries == 20) {
Log.w(TAG, "Retried 20 seconds, reject connection");
try {
mPendingConnection.close();
} catch (IOException e) {
Log.e(TAG, "close tranport error");
}
mIncomingRetries = 0;
mPendingConnection = null;
} else {
Log.i(TAG, "OPP busy! Retry after 1 second");
mIncomingRetries = mIncomingRetries + 1;
Message msg2 = Message.obtain(mHandler);
msg2.what = MSG_INCOMING_CONNECTION_RETRY;
mHandler.sendMessageDelayed(msg2, 1000);
}
}
break;
}
}
};
private void startSocketListener() {
if (V) Log.v(TAG, "start RfcommListener");
mSocketListener.start(mHandler);
if (V) Log.v(TAG, "RfcommListener started");
}
@Override
public void onDestroy() {
if (V) Log.v(TAG, "onDestroy");
super.onDestroy();
getContentResolver().unregisterContentObserver(mObserver);
unregisterReceiver(mBluetoothReceiver);
mSocketListener.stop();
_cleanup();
}
/* Cleanup all local references. Called during onDestroy */
private void _cleanup() {
if(mBatchs != null) {
mBatchs.clear();
mBatchs = null;
}
if(mShares != null) {
mShares.clear();
mShares = null;
}
if(mObserver != null)
mObserver = null;
if(mNotifier != null)
mNotifier = null;
if(mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
mHandler = null;
}
if(mSocketListener != null)
mSocketListener = null;
if(mPowerManager != null)
mPowerManager = null;
if(mPendingConnection != null)
mPendingConnection = null;
if(mTransfer != null) {
mTransfer.cleanup();
mTransfer = null;
}
if(mServerTransfer!= null) {
mServerTransfer.cleanup();
mServerTransfer = null;
}
if(mAdapter != null)
mAdapter = null;
}
/* suppose we auto accept an incoming OPUSH connection */
private void createServerSession(ObexTransport transport) {
mServerSession = new BluetoothOppObexServerSession(this, transport);
mServerSession.preStart();
if (D) Log.d(TAG, "Get ServerSession " + mServerSession.toString()
+ " for incoming connection" + transport.toString());
}
private final BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
switch (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
case BluetoothAdapter.STATE_ON:
if (V) Log.v(TAG,
"Receiver BLUETOOTH_STATE_CHANGED_ACTION, BLUETOOTH_STATE_ON");
startSocketListener();
break;
case BluetoothAdapter.STATE_TURNING_OFF:
if (V) Log.v(TAG, "Receiver DISABLED_ACTION ");
//FIX: Don't block main thread
/*
mSocketListener.stop();
mListenStarted = false;
synchronized (BluetoothOppService.this) {
if (mUpdateThread == null) {
stopSelf();
}
}
*/
mHandler.sendMessage(mHandler.obtainMessage(STOP_LISTENER));
break;
}
}
}
};
private void updateFromProvider() {
synchronized (BluetoothOppService.this) {
mPendingUpdate = true;
if (mUpdateThread == null) {
mUpdateThread = new UpdateThread();
mUpdateThread.start();
}
}
}
private class UpdateThread extends Thread {
public UpdateThread() {
super("Bluetooth Share Service");
}
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
boolean keepService = false;
for (;;) {
synchronized (BluetoothOppService.this) {
if (mUpdateThread != this) {
throw new IllegalStateException(
"multiple UpdateThreads in BluetoothOppService");
}
if (V) Log.v(TAG, "pendingUpdate is " + mPendingUpdate + " keepUpdateThread is "
+ keepService + " sListenStarted is " + mListenStarted);
if (!mPendingUpdate) {
mUpdateThread = null;
if (!keepService && !mListenStarted) {
stopSelf();
break;
}
return;
}
mPendingUpdate = false;
}
Cursor cursor = getContentResolver().query(BluetoothShare.CONTENT_URI, null, null,
null, BluetoothShare._ID);
if (cursor == null) {
return;
}
cursor.moveToFirst();
int arrayPos = 0;
keepService = false;
boolean isAfterLast = cursor.isAfterLast();
int idColumn = cursor.getColumnIndexOrThrow(BluetoothShare._ID);
/*
* Walk the cursor and the local array to keep them in sync. The
* key to the algorithm is that the ids are unique and sorted
* both in the cursor and in the array, so that they can be
* processed in order in both sources at the same time: at each
* step, both sources point to the lowest id that hasn't been
* processed from that source, and the algorithm processes the
* lowest id from those two possibilities. At each step: -If the
* array contains an entry that's not in the cursor, remove the
* entry, move to next entry in the array. -If the array
* contains an entry that's in the cursor, nothing to do, move
* to next cursor row and next array entry. -If the cursor
* contains an entry that's not in the array, insert a new entry
* in the array, move to next cursor row and next array entry.
*/
while (!isAfterLast || arrayPos < mShares.size()) {
if (isAfterLast) {
// We're beyond the end of the cursor but there's still
// some
// stuff in the local array, which can only be junk
if (V) Log.v(TAG, "Array update: trimming " +
mShares.get(arrayPos).mId + " @ " + arrayPos);
if (shouldScanFile(arrayPos)) {
scanFile(null, arrayPos);
}
deleteShare(arrayPos); // this advances in the array
} else {
int id = cursor.getInt(idColumn);
if (arrayPos == mShares.size()) {
insertShare(cursor, arrayPos);
if (V) Log.v(TAG, "Array update: inserting " + id + " @ " + arrayPos);
if (shouldScanFile(arrayPos) && (!scanFile(cursor, arrayPos))) {
keepService = true;
}
if (visibleNotification(arrayPos)) {
keepService = true;
}
if (needAction(arrayPos)) {
keepService = true;
}
++arrayPos;
cursor.moveToNext();
isAfterLast = cursor.isAfterLast();
} else {
int arrayId = mShares.get(arrayPos).mId;
if (arrayId < id) {
if (V) Log.v(TAG, "Array update: removing " + arrayId + " @ "
+ arrayPos);
if (shouldScanFile(arrayPos)) {
scanFile(null, arrayPos);
}
deleteShare(arrayPos);
} else if (arrayId == id) {
// This cursor row already exists in the stored
// array
updateShare(cursor, arrayPos, userAccepted);
if (shouldScanFile(arrayPos) && (!scanFile(cursor, arrayPos))) {
keepService = true;
}
if (visibleNotification(arrayPos)) {
keepService = true;
}
if (needAction(arrayPos)) {
keepService = true;
}
++arrayPos;
cursor.moveToNext();
isAfterLast = cursor.isAfterLast();
} else {
// This cursor entry didn't exist in the stored
// array
if (V) Log.v(TAG, "Array update: appending " + id + " @ " + arrayPos);
insertShare(cursor, arrayPos);
if (shouldScanFile(arrayPos) && (!scanFile(cursor, arrayPos))) {
keepService = true;
}
if (visibleNotification(arrayPos)) {
keepService = true;
}
if (needAction(arrayPos)) {
keepService = true;
}
++arrayPos;
cursor.moveToNext();
isAfterLast = cursor.isAfterLast();
}
}
}
}
mNotifier.updateNotification();
cursor.close();
}
}
}
private void insertShare(Cursor cursor, int arrayPos) {
BluetoothOppShareInfo info = new BluetoothOppShareInfo(
cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID)),
cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.URI)),
cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT)),
cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare._DATA)),
cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.MIMETYPE)),
cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION)),
cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.DESTINATION)),
cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.VISIBILITY)),
cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION)),
cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.STATUS)),
cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES)),
cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES)),
cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP)),
cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) != Constants.MEDIA_SCANNED_NOT_SCANNED);
if (V) {
Log.v(TAG, "Service adding new entry");
Log.v(TAG, "ID : " + info.mId);
// Log.v(TAG, "URI : " + ((info.mUri != null) ? "yes" : "no"));
Log.v(TAG, "URI : " + info.mUri);
Log.v(TAG, "HINT : " + info.mHint);
Log.v(TAG, "FILENAME: " + info.mFilename);
Log.v(TAG, "MIMETYPE: " + info.mMimetype);
Log.v(TAG, "DIRECTION: " + info.mDirection);
Log.v(TAG, "DESTINAT: " + info.mDestination);
Log.v(TAG, "VISIBILI: " + info.mVisibility);
Log.v(TAG, "CONFIRM : " + info.mConfirm);
Log.v(TAG, "STATUS : " + info.mStatus);
Log.v(TAG, "TOTAL : " + info.mTotalBytes);
Log.v(TAG, "CURRENT : " + info.mCurrentBytes);
Log.v(TAG, "TIMESTAMP : " + info.mTimestamp);
Log.v(TAG, "SCANNED : " + info.mMediaScanned);
}
mShares.add(arrayPos, info);
/* Mark the info as failed if it's in invalid status */
if (info.isObsolete()) {
Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_UNKNOWN_ERROR);
}
/*
* Add info into a batch. The logic is
* 1) Only add valid and readyToStart info
* 2) If there is no batch, create a batch and insert this transfer into batch,
* then run the batch
* 3) If there is existing batch and timestamp match, insert transfer into batch
* 4) If there is existing batch and timestamp does not match, create a new batch and
* put in queue
*/
if (info.isReadyToStart()) {
if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
/* check if the file exists */
InputStream i;
try {
i = getContentResolver().openInputStream(Uri.parse(info.mUri));
} catch (FileNotFoundException e) {
Log.e(TAG, "Can't open file for OUTBOUND info " + info.mId);
Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_BAD_REQUEST);
return;
} catch (SecurityException e) {
Log.e(TAG, "Exception:" + e.toString() + " for OUTBOUND info " + info.mId);
Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_BAD_REQUEST);
return;
}
try {
i.close();
} catch (IOException ex) {
Log.e(TAG, "IO error when close file for OUTBOUND info " + info.mId);
return;
}
}
if (mBatchs.size() == 0) {
BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info);
newBatch.mId = mBatchId;
mBatchId++;
mBatchs.add(newBatch);
if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
if (V) Log.v(TAG, "Service create new Batch " + newBatch.mId
+ " for OUTBOUND info " + info.mId);
mTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch);
} else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) {
if (V) Log.v(TAG, "Service create new Batch " + newBatch.mId
+ " for INBOUND info " + info.mId);
mServerTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch,
mServerSession);
}
if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND && mTransfer != null) {
if (V) Log.v(TAG, "Service start transfer new Batch " + newBatch.mId
+ " for info " + info.mId);
mTransfer.start();
} else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND
&& mServerTransfer != null) {
if (V) Log.v(TAG, "Service start server transfer new Batch " + newBatch.mId
+ " for info " + info.mId);
mServerTransfer.start();
}
} else {
int i = findBatchWithTimeStamp(info.mTimestamp);
if (i != -1) {
if (V) Log.v(TAG, "Service add info " + info.mId + " to existing batch "
+ mBatchs.get(i).mId);
mBatchs.get(i).addShare(info);
} else {
// There is ongoing batch
BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info);
newBatch.mId = mBatchId;
mBatchId++;
mBatchs.add(newBatch);
if (V) Log.v(TAG, "Service add new Batch " + newBatch.mId + " for info " +
info.mId);
if (Constants.USE_TCP_DEBUG && !Constants.USE_TCP_SIMPLE_SERVER) {
// only allow concurrent serverTransfer in debug mode
if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) {
if (V) Log.v(TAG, "TCP_DEBUG start server transfer new Batch " +
newBatch.mId + " for info " + info.mId);
mServerTransfer = new BluetoothOppTransfer(this, mPowerManager,
newBatch, mServerSession);
mServerTransfer.start();
}
}
}
}
}
}
private void updateShare(Cursor cursor, int arrayPos, boolean userAccepted) {
BluetoothOppShareInfo info = mShares.get(arrayPos);
int statusColumn = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS);
info.mId = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID));
info.mUri = stringFromCursor(info.mUri, cursor, BluetoothShare.URI);
info.mHint = stringFromCursor(info.mHint, cursor, BluetoothShare.FILENAME_HINT);
info.mFilename = stringFromCursor(info.mFilename, cursor, BluetoothShare._DATA);
info.mMimetype = stringFromCursor(info.mMimetype, cursor, BluetoothShare.MIMETYPE);
info.mDirection = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION));
info.mDestination = stringFromCursor(info.mDestination, cursor, BluetoothShare.DESTINATION);
int newVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.VISIBILITY));
boolean confirmed = false;
int newConfirm = cursor.getInt(cursor
.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION));
if (info.mVisibility == BluetoothShare.VISIBILITY_VISIBLE
&& newVisibility != BluetoothShare.VISIBILITY_VISIBLE
&& (BluetoothShare.isStatusCompleted(info.mStatus) || newConfirm == BluetoothShare.USER_CONFIRMATION_PENDING)) {
mNotifier.mNotificationMgr.cancel(info.mId);
}
info.mVisibility = newVisibility;
if (info.mConfirm == BluetoothShare.USER_CONFIRMATION_PENDING
&& newConfirm != BluetoothShare.USER_CONFIRMATION_PENDING) {
confirmed = true;
}
info.mConfirm = cursor.getInt(cursor
.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION));
int newStatus = cursor.getInt(statusColumn);
if (!BluetoothShare.isStatusCompleted(info.mStatus)
&& BluetoothShare.isStatusCompleted(newStatus)) {
mNotifier.mNotificationMgr.cancel(info.mId);
}
info.mStatus = newStatus;
info.mTotalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES));
info.mCurrentBytes = cursor.getInt(cursor
.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES));
info.mTimestamp = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP));
info.mMediaScanned = (cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) != Constants.MEDIA_SCANNED_NOT_SCANNED);
if (confirmed) {
if (V) Log.v(TAG, "Service handle info " + info.mId + " confirmed");
/* Inbounds transfer get user confirmation, so we start it */
int i = findBatchWithTimeStamp(info.mTimestamp);
if (i != -1) {
BluetoothOppBatch batch = mBatchs.get(i);
if (mServerTransfer != null && batch.mId == mServerTransfer.getBatchId()) {
mServerTransfer.setConfirmed();
} //TODO need to think about else
}
}
int i = findBatchWithTimeStamp(info.mTimestamp);
if (i != -1) {
BluetoothOppBatch batch = mBatchs.get(i);
if (batch.mStatus == Constants.BATCH_STATUS_FINISHED
|| batch.mStatus == Constants.BATCH_STATUS_FAILED) {
if (V) Log.v(TAG, "Batch " + batch.mId + " is finished");
if (batch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
if (mTransfer == null) {
Log.e(TAG, "Unexpected error! mTransfer is null");
} else if (batch.mId == mTransfer.getBatchId()) {
mTransfer.stop();
} else {
Log.e(TAG, "Unexpected error! batch id " + batch.mId
+ " doesn't match mTransfer id " + mTransfer.getBatchId());
}
mTransfer.cleanup();
mTransfer = null;
} else {
if (mServerTransfer == null) {
Log.e(TAG, "Unexpected error! mServerTransfer is null");
} else if (batch.mId == mServerTransfer.getBatchId()) {
mServerTransfer.stop();
} else {
Log.e(TAG, "Unexpected error! batch id " + batch.mId
+ " doesn't match mServerTransfer id "
+ mServerTransfer.getBatchId());
}
mServerTransfer.cleanup();
mServerTransfer = null;
}
removeBatch(batch);
}
}
}
/**
* Removes the local copy of the info about a share.
*/
private void deleteShare(int arrayPos) {
BluetoothOppShareInfo info = mShares.get(arrayPos);
/*
* Delete arrayPos from a batch. The logic is
* 1) Search existing batch for the info
* 2) cancel the batch
* 3) If the batch become empty delete the batch
*/
int i = findBatchWithTimeStamp(info.mTimestamp);
if (i != -1) {
BluetoothOppBatch batch = mBatchs.get(i);
if (batch.hasShare(info)) {
if (V) Log.v(TAG, "Service cancel batch for share " + info.mId);
batch.cancelBatch();
}
if (batch.isEmpty()) {
if (V) Log.v(TAG, "Service remove batch " + batch.mId);
removeBatch(batch);
}
}
mShares.remove(arrayPos);
}
private String stringFromCursor(String old, Cursor cursor, String column) {
int index = cursor.getColumnIndexOrThrow(column);
if (old == null) {
return cursor.getString(index);
}
if (mNewChars == null) {
mNewChars = new CharArrayBuffer(128);
}
cursor.copyStringToBuffer(index, mNewChars);
int length = mNewChars.sizeCopied;
if (length != old.length()) {
return cursor.getString(index);
}
if (mOldChars == null || mOldChars.sizeCopied < length) {
mOldChars = new CharArrayBuffer(length);
}
char[] oldArray = mOldChars.data;
char[] newArray = mNewChars.data;
old.getChars(0, length, oldArray, 0);
for (int i = length - 1; i >= 0; --i) {
if (oldArray[i] != newArray[i]) {
return new String(newArray, 0, length);
}
}
return old;
}
private int findBatchWithTimeStamp(long timestamp) {
for (int i = mBatchs.size() - 1; i >= 0; i--) {
if (mBatchs.get(i).mTimestamp == timestamp) {
return i;
}
}
return -1;
}
private void removeBatch(BluetoothOppBatch batch) {
if (V) Log.v(TAG, "Remove batch " + batch.mId);
mBatchs.remove(batch);
BluetoothOppBatch nextBatch;
if (mBatchs.size() > 0) {
for (int i = 0; i < mBatchs.size(); i++) {
// we have a running batch
nextBatch = mBatchs.get(i);
if (nextBatch.mStatus == Constants.BATCH_STATUS_RUNNING) {
return;
} else {
// just finish a transfer, start pending outbound transfer
if (nextBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
if (V) Log.v(TAG, "Start pending outbound batch " + nextBatch.mId);
mTransfer = new BluetoothOppTransfer(this, mPowerManager, nextBatch);
mTransfer.start();
return;
} else if (nextBatch.mDirection == BluetoothShare.DIRECTION_INBOUND
&& mServerSession != null) {
// have to support pending inbound transfer
// if an outbound transfer and incoming socket happens together
if (V) Log.v(TAG, "Start pending inbound batch " + nextBatch.mId);
mServerTransfer = new BluetoothOppTransfer(this, mPowerManager, nextBatch,
mServerSession);
mServerTransfer.start();
if (nextBatch.getPendingShare().mConfirm ==
BluetoothShare.USER_CONFIRMATION_CONFIRMED) {
mServerTransfer.setConfirmed();
}
return;
}
}
}
}
}
private boolean needAction(int arrayPos) {
BluetoothOppShareInfo info = mShares.get(arrayPos);
if (BluetoothShare.isStatusCompleted(info.mStatus)) {
return false;
}
return true;
}
private boolean visibleNotification(int arrayPos) {
BluetoothOppShareInfo info = mShares.get(arrayPos);
return info.hasCompletionNotification();
}
private boolean scanFile(Cursor cursor, int arrayPos) {
BluetoothOppShareInfo info = mShares.get(arrayPos);
synchronized (BluetoothOppService.this) {
if (D) Log.d(TAG, "Scanning file " + info.mFilename);
if (!mMediaScanInProgress) {
mMediaScanInProgress = true;
new MediaScannerNotifier(this, info, mHandler);
return true;
} else {
return false;
}
}
}
private boolean shouldScanFile(int arrayPos) {
BluetoothOppShareInfo info = mShares.get(arrayPos);
return BluetoothShare.isStatusSuccess(info.mStatus)
&& info.mDirection == BluetoothShare.DIRECTION_INBOUND && !info.mMediaScanned &&
info.mConfirm != BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED;
}
// Run in a background thread at boot.
private static void trimDatabase(ContentResolver contentResolver) {
final String INVISIBLE = BluetoothShare.VISIBILITY + "=" +
BluetoothShare.VISIBILITY_HIDDEN;
// remove the invisible/complete/outbound shares
final String WHERE_INVISIBLE_COMPLETE_OUTBOUND = BluetoothShare.DIRECTION + "="
+ BluetoothShare.DIRECTION_OUTBOUND + " AND " + BluetoothShare.STATUS + ">="
+ BluetoothShare.STATUS_SUCCESS + " AND " + INVISIBLE;
int delNum = contentResolver.delete(BluetoothShare.CONTENT_URI,
WHERE_INVISIBLE_COMPLETE_OUTBOUND, null);
if (V) Log.v(TAG, "Deleted complete outbound shares, number = " + delNum);
// remove the invisible/finished/inbound/failed shares
final String WHERE_INVISIBLE_COMPLETE_INBOUND_FAILED = BluetoothShare.DIRECTION + "="
+ BluetoothShare.DIRECTION_INBOUND + " AND " + BluetoothShare.STATUS + ">"
+ BluetoothShare.STATUS_SUCCESS + " AND " + INVISIBLE;
delNum = contentResolver.delete(BluetoothShare.CONTENT_URI,
WHERE_INVISIBLE_COMPLETE_INBOUND_FAILED, null);
if (V) Log.v(TAG, "Deleted complete inbound failed shares, number = " + delNum);
// Only keep the inbound and successful shares for LiverFolder use
// Keep the latest 1000 to easy db query
final String WHERE_INBOUND_SUCCESS = BluetoothShare.DIRECTION + "="
+ BluetoothShare.DIRECTION_INBOUND + " AND " + BluetoothShare.STATUS + "="
+ BluetoothShare.STATUS_SUCCESS + " AND " + INVISIBLE;
Cursor cursor = contentResolver.query(BluetoothShare.CONTENT_URI, new String[] {
BluetoothShare._ID
}, WHERE_INBOUND_SUCCESS, null, BluetoothShare._ID); // sort by id
if (cursor == null) {
return;
}
int recordNum = cursor.getCount();
if (recordNum > Constants.MAX_RECORDS_IN_DATABASE) {
int numToDelete = recordNum - Constants.MAX_RECORDS_IN_DATABASE;
if (cursor.moveToPosition(numToDelete)) {
int columnId = cursor.getColumnIndexOrThrow(BluetoothShare._ID);
long id = cursor.getLong(columnId);
delNum = contentResolver.delete(BluetoothShare.CONTENT_URI,
BluetoothShare._ID + " < " + id, null);
if (V) Log.v(TAG, "Deleted old inbound success share: " + delNum);
}
}
cursor.close();
}
private static class MediaScannerNotifier implements MediaScannerConnectionClient {
private MediaScannerConnection mConnection;
private BluetoothOppShareInfo mInfo;
private Context mContext;
private Handler mCallback;
public MediaScannerNotifier(Context context, BluetoothOppShareInfo info, Handler handler) {
mContext = context;
mInfo = info;
mCallback = handler;
mConnection = new MediaScannerConnection(mContext, this);
if (V) Log.v(TAG, "Connecting to MediaScannerConnection ");
mConnection.connect();
}
public void onMediaScannerConnected() {
if (V) Log.v(TAG, "MediaScannerConnection onMediaScannerConnected");
mConnection.scanFile(mInfo.mFilename, mInfo.mMimetype);
}
public void onScanCompleted(String path, Uri uri) {
try {
if (V) {
Log.v(TAG, "MediaScannerConnection onScanCompleted");
Log.v(TAG, "MediaScannerConnection path is " + path);
Log.v(TAG, "MediaScannerConnection Uri is " + uri);
}
if (uri != null) {
Message msg = Message.obtain();
msg.setTarget(mCallback);
msg.what = MEDIA_SCANNED;
msg.arg1 = mInfo.mId;
msg.obj = uri;
msg.sendToTarget();
} else {
Message msg = Message.obtain();
msg.setTarget(mCallback);
msg.what = MEDIA_SCANNED_FAILED;
msg.arg1 = mInfo.mId;
msg.sendToTarget();
}
} catch (Exception ex) {
Log.v(TAG, "!!!MediaScannerConnection exception: " + ex);
} finally {
if (V) Log.v(TAG, "MediaScannerConnection disconnect");
mConnection.disconnect();
}
}
}
}