/* * 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.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevicePicker; import android.bluetooth.BluetoothSocket; 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.Binder; import android.os.Handler; import android.os.Message; import android.os.Process; import android.util.Log; import com.android.bluetooth.BluetoothObexTransport; import com.android.bluetooth.IObexConnectionHandler; import com.android.bluetooth.ObexServerSockets; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.sdp.SdpManager; import com.android.internal.annotations.VisibleForTesting; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Locale; import javax.obex.ObexTransport; /** * Performs the background Bluetooth OPP transfer. It also starts thread to * accept incoming OPP connection. */ public class BluetoothOppService extends ProfileService implements IObexConnectionHandler { private static final boolean D = Constants.DEBUG; private static final boolean V = Constants.VERBOSE; private static final byte[] SUPPORTED_OPP_FORMAT = { 0x01 /* vCard 2.1 */, 0x02 /* vCard 3.0 */, 0x03 /* vCal 1.0 */, 0x04 /* iCal 2.0 */, (byte) 0xFF /* Any type of object */ }; private class BluetoothShareContentObserver extends ContentObserver { 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 boolean mUpdateThreadRunning; private ArrayList mShares; private ArrayList mBatches; 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 boolean mListenStarted; private boolean mMediaScanInProgress; private int mIncomingRetries; private ObexTransport mPendingConnection; private int mOppSdpHandle = -1; boolean mAcceptNewConnections; private AdapterService mAdapterService; private static final String INVISIBLE = BluetoothShare.VISIBILITY + "=" + BluetoothShare.VISIBILITY_HIDDEN; private static final String WHERE_INBOUND_SUCCESS = BluetoothShare.DIRECTION + "=" + BluetoothShare.DIRECTION_INBOUND + " AND " + BluetoothShare.STATUS + "=" + BluetoothShare.STATUS_SUCCESS + " AND " + INVISIBLE; private static final String WHERE_CONFIRM_PENDING_INBOUND = BluetoothShare.DIRECTION + "=" + BluetoothShare.DIRECTION_INBOUND + " AND " + BluetoothShare.USER_CONFIRMATION + "=" + BluetoothShare.USER_CONFIRMATION_PENDING; private static final String WHERE_INVISIBLE_UNCONFIRMED = "(" + BluetoothShare.STATUS + " > " + BluetoothShare.STATUS_SUCCESS + " AND " + INVISIBLE + ") OR (" + WHERE_CONFIRM_PENDING_INBOUND + ")"; private static BluetoothOppService sBluetoothOppService; /* * 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 protected IProfileServiceBinder initBinder() { return new OppBinder(this); } private static class OppBinder extends Binder implements IProfileServiceBinder { OppBinder(BluetoothOppService service) { } @Override public void cleanup() { } } @Override protected void create() { if (V) { Log.v(TAG, "onCreate"); } mShares = new ArrayList(); mBatches = new ArrayList(); mBatchId = 1; final ContentResolver contentResolver = getContentResolver(); new Thread("trimDatabase") { @Override public void run() { trimDatabase(contentResolver); } }.start(); IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); registerReceiver(mBluetoothReceiver, filter); if (V) { BluetoothOppPreference preference = BluetoothOppPreference.getInstance(this); if (preference != null) { preference.dump(); } else { Log.w(TAG, "BluetoothOppPreference.getInstance returned null."); } } } @Override public boolean start() { if (V) { Log.v(TAG, "start()"); } mAdapterService = AdapterService.getAdapterService(); mObserver = new BluetoothShareContentObserver(); getContentResolver().registerContentObserver(BluetoothShare.CONTENT_URI, true, mObserver); mNotifier = new BluetoothOppNotification(this); mNotifier.mNotificationMgr.cancelAll(); mNotifier.updateNotification(); updateFromProvider(); setBluetoothOppService(this); return true; } @Override public boolean stop() { if (sBluetoothOppService == null) { Log.w(TAG, "stop() called before start()"); return true; } setBluetoothOppService(null); mHandler.sendMessage(mHandler.obtainMessage(STOP_LISTENER)); return true; } private void startListener() { if (!mListenStarted) { if (mAdapterService.isEnabled()) { if (V) { Log.v(TAG, "Starting RfcommListener"); } mHandler.sendMessage(mHandler.obtainMessage(START_LISTENER)); mListenStarted = true; } } } @Override public void dump(StringBuilder sb) { super.dump(sb); if (mShares.size() > 0) { println(sb, "Shares:"); for (BluetoothOppShareInfo info : mShares) { String dir = info.mDirection == BluetoothShare.DIRECTION_OUTBOUND ? " -> " : " <- "; SimpleDateFormat format = new SimpleDateFormat("MM-dd HH:mm:ss", Locale.US); Date date = new Date(info.mTimestamp); println(sb, " " + format.format(date) + dir + info.mCurrentBytes + "/" + info.mTotalBytes); } } } /** * Get the current instance of {@link BluetoothOppService} * * @return current instance of {@link BluetoothOppService} */ @VisibleForTesting public static synchronized BluetoothOppService getBluetoothOppService() { if (sBluetoothOppService == null) { Log.w(TAG, "getBluetoothOppService(): service is null"); return null; } if (!sBluetoothOppService.isAvailable()) { Log.w(TAG, "getBluetoothOppService(): service is not available"); return null; } return sBluetoothOppService; } private static synchronized void setBluetoothOppService(BluetoothOppService instance) { if (D) { Log.d(TAG, "setBluetoothOppService(): set to: " + instance); } sBluetoothOppService = instance; } 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 MSG_INCOMING_BTOPP_CONNECTION = 100; private static final int STOP_LISTENER = 200; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case STOP_LISTENER: stopListeners(); mListenStarted = false; //Stop Active INBOUND Transfer if (mServerTransfer != null) { mServerTransfer.onBatchCanceled(); mServerTransfer = null; } //Stop Active OUTBOUND Transfer if (mTransfer != null) { mTransfer.onBatchCanceled(); mTransfer = null; } unregisterReceivers(); synchronized (BluetoothOppService.this) { if (mUpdateThread != null) { mUpdateThread.interrupt(); } } while (mUpdateThread != null && mUpdateThreadRunning) { try { Thread.sleep(50); } catch (Exception e) { Log.e(TAG, "Thread sleep", e); } } synchronized (BluetoothOppService.this) { if (mUpdateThread != null) { try { mUpdateThread.join(); } catch (InterruptedException e) { Log.e(TAG, "Interrupted", e); } mUpdateThread = null; } } mNotifier.cancelNotifications(); break; case START_LISTENER: if (mAdapterService.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 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 (mBatches.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 { 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 (mBatches.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"); } if (mServerSocket != null) { acceptNewConnections(); } 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 ObexServerSockets mServerSocket; private void startSocketListener() { if (D) { Log.d(TAG, "start Socket Listeners"); } stopListeners(); mServerSocket = ObexServerSockets.createInsecure(this); acceptNewConnections(); SdpManager sdpManager = SdpManager.getDefaultManager(); if (sdpManager == null || mServerSocket == null) { Log.e(TAG, "ERROR:serversocket object is NULL sdp manager :" + sdpManager + " mServerSocket:" + mServerSocket); return; } mOppSdpHandle = sdpManager.createOppOpsRecord("OBEX Object Push", mServerSocket.getRfcommChannel(), mServerSocket.getL2capPsm(), 0x0102, SUPPORTED_OPP_FORMAT); if (D) { Log.d(TAG, "mOppSdpHandle :" + mOppSdpHandle); } } @Override protected void cleanup() { if (V) { Log.v(TAG, "onDestroy"); } stopListeners(); if (mBatches != null) { mBatches.clear(); } if (mShares != null) { mShares.clear(); } if (mHandler != null) { mHandler.removeCallbacksAndMessages(null); } } private void unregisterReceivers() { try { if (mObserver != null) { getContentResolver().unregisterContentObserver(mObserver); mObserver = null; } unregisterReceiver(mBluetoothReceiver); } catch (IllegalArgumentException e) { Log.w(TAG, "unregisterReceivers " + e.toString()); } } /* suppose we auto accept an incoming OPUSH connection */ private void createServerSession(ObexTransport transport) { mServerSession = new BluetoothOppObexServerSession(this, transport, this); 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, "Bluetooth state changed: STATE_ON"); } startListener(); // If this is within a sending process, continue the handle // logic to display device picker dialog. synchronized (this) { if (BluetoothOppManager.getInstance(context).mSendingFlag) { // reset the flags BluetoothOppManager.getInstance(context).mSendingFlag = false; Intent in1 = new Intent(BluetoothDevicePicker.ACTION_LAUNCH); in1.putExtra(BluetoothDevicePicker.EXTRA_NEED_AUTH, false); in1.putExtra(BluetoothDevicePicker.EXTRA_FILTER_TYPE, BluetoothDevicePicker.FILTER_TYPE_TRANSFER); in1.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_PACKAGE, Constants.THIS_PACKAGE_NAME); in1.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_CLASS, BluetoothOppReceiver.class.getName()); in1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(in1); } } break; case BluetoothAdapter.STATE_TURNING_OFF: if (V) { Log.v(TAG, "Bluetooth state changed: STATE_TURNING_OFF"); } mHandler.sendMessage(mHandler.obtainMessage(STOP_LISTENER)); break; } } } }; private void updateFromProvider() { synchronized (BluetoothOppService.this) { mPendingUpdate = true; if (mUpdateThread == null) { mUpdateThread = new UpdateThread(); mUpdateThread.start(); mUpdateThreadRunning = true; } } } private class UpdateThread extends Thread { private boolean mIsInterrupted; UpdateThread() { super("Bluetooth Share Service"); mIsInterrupted = false; } @Override public void interrupt() { mIsInterrupted = true; if (D) { Log.d(TAG, "OPP UpdateThread interrupted "); } super.interrupt(); } @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); while (!mIsInterrupted) { synchronized (BluetoothOppService.this) { if (mUpdateThread != this) { mUpdateThreadRunning = false; throw new IllegalStateException( "multiple UpdateThreads in BluetoothOppService"); } if (V) { Log.v(TAG, "pendingUpdate is " + mPendingUpdate + " sListenStarted is " + mListenStarted + " isInterrupted :" + mIsInterrupted); } if (!mPendingUpdate) { mUpdateThread = null; mUpdateThreadRunning = false; return; } mPendingUpdate = false; } Cursor cursor = getContentResolver().query(BluetoothShare.CONTENT_URI, null, null, null, BluetoothShare._ID); if (cursor == null) { mUpdateThreadRunning = false; return; } cursor.moveToFirst(); int arrayPos = 0; 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() && mListenStarted) { 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 (mShares.size() != 0) { if (V) { Log.v(TAG, "Array update: trimming " + mShares.get(arrayPos).mId + " @ " + 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); } ++arrayPos; cursor.moveToNext(); isAfterLast = cursor.isAfterLast(); } else { int arrayId = 0; if (mShares.size() != 0) { arrayId = mShares.get(arrayPos).mId; } if (arrayId < id) { if (V) { Log.v(TAG, "Array update: removing " + arrayId + " @ " + arrayPos); } deleteShare(arrayPos); } else if (arrayId == id) { // This cursor row already exists in the stored array. updateShare(cursor, arrayPos); scanFileIfNeeded(arrayPos); ++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); ++arrayPos; cursor.moveToNext(); isAfterLast = cursor.isAfterLast(); } } } } mNotifier.updateNotification(); cursor.close(); } mUpdateThreadRunning = false; } } private void insertShare(Cursor cursor, int arrayPos) { String uriString = cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.URI)); Uri uri; if (uriString != null) { uri = Uri.parse(uriString); Log.d(TAG, "insertShare parsed URI: " + uri); } else { uri = null; Log.e(TAG, "insertShare found null URI at cursor!"); } BluetoothOppShareInfo info = new BluetoothOppShareInfo( cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID)), 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.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES)), cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES)), cursor.getLong(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 */ BluetoothOppSendFileInfo sendFileInfo = BluetoothOppUtility.getSendFileInfo(info.mUri); if (sendFileInfo == null || sendFileInfo.mInputStream == null) { Log.e(TAG, "Can't open file for OUTBOUND info " + info.mId); Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_BAD_REQUEST); BluetoothOppUtility.closeSendFileInfo(info.mUri); return; } } if (mBatches.size() == 0) { BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info); newBatch.mId = mBatchId; mBatchId++; mBatches.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, 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, 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 " + mBatches .get(i).mId); } mBatches.get(i).addShare(info); } else { // There is ongoing batch BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info); newBatch.mId = mBatchId; mBatchId++; mBatches.add(newBatch); if (V) { Log.v(TAG, "Service add new Batch " + newBatch.mId + " for info " + info.mId); } } } } } private void updateShare(Cursor cursor, int arrayPos) { BluetoothOppShareInfo info = mShares.get(arrayPos); int statusColumn = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS); info.mId = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID)); if (info.mUri != null) { String uriString = stringFromCursor(info.mUri.toString(), cursor, BluetoothShare.URI); if (uriString != null) { info.mUri = Uri.parse(uriString); } } else { Log.w(TAG, "updateShare() called for ID " + info.mId + " with null 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 confirmUpdated = 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) { confirmUpdated = true; } info.mConfirm = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION)); int newStatus = cursor.getInt(statusColumn); if (BluetoothShare.isStatusCompleted(info.mStatus)) { mNotifier.mNotificationMgr.cancel(info.mId); } info.mStatus = newStatus; info.mTotalBytes = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES)); info.mCurrentBytes = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES)); info.mTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP)); info.mMediaScanned = (cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) != Constants.MEDIA_SCANNED_NOT_SCANNED); if (confirmUpdated) { if (V) { Log.v(TAG, "Service handle info " + info.mId + " confirmation updated"); } /* Inbounds transfer user confirmation status changed, update the session server */ int i = findBatchWithTimeStamp(info.mTimestamp); if (i != -1) { BluetoothOppBatch batch = mBatches.get(i); if (mServerTransfer != null && batch.mId == mServerTransfer.getBatchId()) { mServerTransfer.confirmStatusChanged(); } //TODO need to think about else } } int i = findBatchWithTimeStamp(info.mTimestamp); if (i != -1) { BluetoothOppBatch batch = mBatches.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 = 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 = 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 = mBatches.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 = mBatches.size() - 1; i >= 0; i--) { if (mBatches.get(i).mTimestamp == timestamp) { return i; } } return -1; } private void removeBatch(BluetoothOppBatch batch) { if (V) { Log.v(TAG, "Remove batch " + batch.mId); } mBatches.remove(batch); if (mBatches.size() > 0) { for (BluetoothOppBatch nextBatch : mBatches) { // we have a running batch 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, 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, nextBatch, mServerSession); mServerTransfer.start(); if (nextBatch.getPendingShare() != null && nextBatch.getPendingShare().mConfirm == BluetoothShare.USER_CONFIRMATION_CONFIRMED) { mServerTransfer.confirmStatusChanged(); } return; } } } } } private void scanFileIfNeeded(int arrayPos) { BluetoothOppShareInfo info = mShares.get(arrayPos); boolean isFileReceived = BluetoothShare.isStatusSuccess(info.mStatus) && info.mDirection == BluetoothShare.DIRECTION_INBOUND && !info.mMediaScanned && info.mConfirm != BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED; if (!isFileReceived) { return; } synchronized (BluetoothOppService.this) { if (D) { Log.d(TAG, "Scanning file " + info.mFilename); } if (!mMediaScanInProgress) { mMediaScanInProgress = true; new MediaScannerNotifier(this, info, mHandler); } } } // Run in a background thread at boot. private static void trimDatabase(ContentResolver contentResolver) { // remove the invisible/unconfirmed inbound shares int delNum = contentResolver.delete(BluetoothShare.CONTENT_URI, WHERE_INVISIBLE_UNCONFIRMED, null); if (V) { Log.v(TAG, "Deleted shares, number = " + delNum); } // Keep the latest inbound and successful shares. 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; 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(); } @Override public void onMediaScannerConnected() { if (V) { Log.v(TAG, "MediaScannerConnection onMediaScannerConnected"); } mConnection.scanFile(mInfo.mFilename, mInfo.mMimetype); } @Override 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 (NullPointerException ex) { Log.v(TAG, "!!!MediaScannerConnection exception: " + ex); } finally { if (V) { Log.v(TAG, "MediaScannerConnection disconnect"); } mConnection.disconnect(); } } } private void stopListeners() { if (mAdapterService != null && mOppSdpHandle >= 0 && SdpManager.getDefaultManager() != null) { if (D) { Log.d(TAG, "Removing SDP record mOppSdpHandle :" + mOppSdpHandle); } boolean status = SdpManager.getDefaultManager().removeSdpRecord(mOppSdpHandle); Log.d(TAG, "RemoveSDPrecord returns " + status); mOppSdpHandle = -1; } if (mServerSocket != null) { mServerSocket.shutdown(false); mServerSocket = null; } if (D) { Log.d(TAG, "stopListeners: mServerSocket is null"); } } @Override public boolean onConnect(BluetoothDevice device, BluetoothSocket socket) { if (D) { Log.d(TAG, " onConnect BluetoothSocket :" + socket + " \n :device :" + device); } if (!mAcceptNewConnections) { Log.d(TAG, " onConnect BluetoothSocket :" + socket + " rejected"); return false; } BluetoothObexTransport transport = new BluetoothObexTransport(socket); Message msg = mHandler.obtainMessage(MSG_INCOMING_BTOPP_CONNECTION); msg.obj = transport; msg.sendToTarget(); mAcceptNewConnections = false; return true; } @Override public void onAcceptFailed() { Log.d(TAG, " onAcceptFailed:"); mHandler.sendMessage(mHandler.obtainMessage(START_LISTENER)); } /** * Set mAcceptNewConnections to true to allow new connections. */ void acceptNewConnections() { mAcceptNewConnections = true; } }