/* * 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 = "BtOpp Service"; /** 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, "Service 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, "Service 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 retCode; } 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 Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { 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, "Service onDestroy"); super.onDestroy(); getContentResolver().unregisterContentObserver(mObserver); unregisterReceiver(mBluetoothReceiver); mSocketListener.stop(); } /* 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 "); mSocketListener.stop(); mListenStarted = false; synchronized (BluetoothOppService.this) { if (mUpdateThread == null) { stopSelf(); } } 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 = 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 = 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; } // 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(); } } } }