• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2008-2009, Motorola, Inc.
3  *
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  *
9  * - Redistributions of source code must retain the above copyright notice,
10  * this list of conditions and the following disclaimer.
11  *
12  * - Redistributions in binary form must reproduce the above copyright notice,
13  * this list of conditions and the following disclaimer in the documentation
14  * and/or other materials provided with the distribution.
15  *
16  * - Neither the name of the Motorola, Inc. nor the names of its contributors
17  * may be used to endorse or promote products derived from this software
18  * without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30  * POSSIBILITY OF SUCH DAMAGE.
31  */
32 
33 package com.android.bluetooth.opp;
34 
35 import com.google.android.collect.Lists;
36 import javax.obex.ObexTransport;
37 
38 import android.app.Service;
39 import android.bluetooth.BluetoothAdapter;
40 import android.content.BroadcastReceiver;
41 import android.content.ContentResolver;
42 import android.content.ContentValues;
43 import android.content.Context;
44 import android.content.Intent;
45 import android.content.IntentFilter;
46 import android.database.CharArrayBuffer;
47 import android.database.ContentObserver;
48 import android.database.Cursor;
49 import android.media.MediaScannerConnection;
50 import android.media.MediaScannerConnection.MediaScannerConnectionClient;
51 import android.net.Uri;
52 import android.os.Handler;
53 import android.os.IBinder;
54 import android.os.Message;
55 import android.os.PowerManager;
56 import android.util.Log;
57 import android.os.Process;
58 
59 import java.io.FileNotFoundException;
60 import java.io.IOException;
61 import java.io.InputStream;
62 import java.util.ArrayList;
63 
64 /**
65  * Performs the background Bluetooth OPP transfer. It also starts thread to
66  * accept incoming OPP connection.
67  */
68 
69 public class BluetoothOppService extends Service {
70     private static final boolean D = Constants.DEBUG;
71     private static final boolean V = Constants.VERBOSE;
72 
73     private boolean userAccepted = false;
74 
75     private class BluetoothShareContentObserver extends ContentObserver {
76 
BluetoothShareContentObserver()77         public BluetoothShareContentObserver() {
78             super(new Handler());
79         }
80 
81         @Override
onChange(boolean selfChange)82         public void onChange(boolean selfChange) {
83             if (V) Log.v(TAG, "ContentObserver received notification");
84             updateFromProvider();
85         }
86     }
87 
88     private static final String TAG = "BtOppService";
89 
90     /** Observer to get notified when the content observer's data changes */
91     private BluetoothShareContentObserver mObserver;
92 
93     /** Class to handle Notification Manager updates */
94     private BluetoothOppNotification mNotifier;
95 
96     private boolean mPendingUpdate;
97 
98     private UpdateThread mUpdateThread;
99 
100     private ArrayList<BluetoothOppShareInfo> mShares;
101 
102     private ArrayList<BluetoothOppBatch> mBatchs;
103 
104     private BluetoothOppTransfer mTransfer;
105 
106     private BluetoothOppTransfer mServerTransfer;
107 
108     private int mBatchId;
109 
110     /**
111      * Array used when extracting strings from content provider
112      */
113     private CharArrayBuffer mOldChars;
114 
115     /**
116      * Array used when extracting strings from content provider
117      */
118     private CharArrayBuffer mNewChars;
119 
120     private BluetoothAdapter mAdapter;
121 
122     private PowerManager mPowerManager;
123 
124     private BluetoothOppRfcommListener mSocketListener;
125 
126     private boolean mListenStarted = false;
127 
128     private boolean mMediaScanInProgress;
129 
130     private int mIncomingRetries = 0;
131 
132     private ObexTransport mPendingConnection = null;
133 
134     /*
135      * TODO No support for queue incoming from multiple devices.
136      * Make an array list of server session to support receiving queue from
137      * multiple devices
138      */
139     private BluetoothOppObexServerSession mServerSession;
140 
141     @Override
onBind(Intent arg0)142     public IBinder onBind(Intent arg0) {
143         throw new UnsupportedOperationException("Cannot bind to Bluetooth OPP Service");
144     }
145 
146     @Override
onCreate()147     public void onCreate() {
148         super.onCreate();
149         if (V) Log.v(TAG, "onCreate");
150         mAdapter = BluetoothAdapter.getDefaultAdapter();
151         mSocketListener = new BluetoothOppRfcommListener(mAdapter);
152         mShares = Lists.newArrayList();
153         mBatchs = Lists.newArrayList();
154         mObserver = new BluetoothShareContentObserver();
155         getContentResolver().registerContentObserver(BluetoothShare.CONTENT_URI, true, mObserver);
156         mBatchId = 1;
157         mNotifier = new BluetoothOppNotification(this);
158         mNotifier.mNotificationMgr.cancelAll();
159         mNotifier.updateNotification();
160 
161         final ContentResolver contentResolver = getContentResolver();
162         new Thread("trimDatabase") {
163             public void run() {
164                 trimDatabase(contentResolver);
165             }
166         }.start();
167 
168         IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
169         registerReceiver(mBluetoothReceiver, filter);
170 
171         synchronized (BluetoothOppService.this) {
172             if (mAdapter == null) {
173                 Log.w(TAG, "Local BT device is not enabled");
174             } else {
175                 startListener();
176             }
177         }
178         if (V) BluetoothOppPreference.getInstance(this).dump();
179         updateFromProvider();
180     }
181 
182     @Override
onStartCommand(Intent intent, int flags, int startId)183     public int onStartCommand(Intent intent, int flags, int startId) {
184         if (V) Log.v(TAG, "onStartCommand");
185         //int retCode = super.onStartCommand(intent, flags, startId);
186         //if (retCode == START_STICKY) {
187             if (mAdapter == null) {
188                 Log.w(TAG, "Local BT device is not enabled");
189             } else {
190                 startListener();
191             }
192             updateFromProvider();
193         //}
194         return START_NOT_STICKY;
195     }
196 
startListener()197     private void startListener() {
198         if (!mListenStarted) {
199             if (mAdapter.isEnabled()) {
200                 if (V) Log.v(TAG, "Starting RfcommListener");
201                 mHandler.sendMessage(mHandler.obtainMessage(START_LISTENER));
202                 mListenStarted = true;
203             }
204         }
205     }
206 
207     private static final int START_LISTENER = 1;
208 
209     private static final int MEDIA_SCANNED = 2;
210 
211     private static final int MEDIA_SCANNED_FAILED = 3;
212 
213     private static final int MSG_INCOMING_CONNECTION_RETRY = 4;
214 
215     private static final int STOP_LISTENER = 200;
216 
217     private Handler mHandler = new Handler() {
218         @Override
219         public void handleMessage(Message msg) {
220             switch (msg.what) {
221                 case STOP_LISTENER:
222                     mSocketListener.stop();
223                     mListenStarted = false;
224                     synchronized (BluetoothOppService.this) {
225                         if (mUpdateThread == null) {
226                             stopSelf();
227                         }
228                     }
229                     break;
230                 case START_LISTENER:
231                     if (mAdapter.isEnabled()) {
232                         startSocketListener();
233                     }
234                     break;
235                 case MEDIA_SCANNED:
236                     if (V) Log.v(TAG, "Update mInfo.id " + msg.arg1 + " for data uri= "
237                                 + msg.obj.toString());
238                     ContentValues updateValues = new ContentValues();
239                     Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + msg.arg1);
240                     updateValues.put(Constants.MEDIA_SCANNED, Constants.MEDIA_SCANNED_SCANNED_OK);
241                     updateValues.put(BluetoothShare.URI, msg.obj.toString()); // update
242                     updateValues.put(BluetoothShare.MIMETYPE, getContentResolver().getType(
243                             Uri.parse(msg.obj.toString())));
244                     getContentResolver().update(contentUri, updateValues, null, null);
245                     synchronized (BluetoothOppService.this) {
246                         mMediaScanInProgress = false;
247                     }
248                     break;
249                 case MEDIA_SCANNED_FAILED:
250                     Log.v(TAG, "Update mInfo.id " + msg.arg1 + " for MEDIA_SCANNED_FAILED");
251                     ContentValues updateValues1 = new ContentValues();
252                     Uri contentUri1 = Uri.parse(BluetoothShare.CONTENT_URI + "/" + msg.arg1);
253                     updateValues1.put(Constants.MEDIA_SCANNED,
254                             Constants.MEDIA_SCANNED_SCANNED_FAILED);
255                     getContentResolver().update(contentUri1, updateValues1, null, null);
256                     synchronized (BluetoothOppService.this) {
257                         mMediaScanInProgress = false;
258                     }
259                     break;
260                 case BluetoothOppRfcommListener.MSG_INCOMING_BTOPP_CONNECTION:
261                     if (D) Log.d(TAG, "Get incoming connection");
262                     ObexTransport transport = (ObexTransport)msg.obj;
263                     /*
264                      * Strategy for incoming connections:
265                      * 1. If there is no ongoing transfer, no on-hold connection, start it
266                      * 2. If there is ongoing transfer, hold it for 20 seconds(1 seconds * 20 times)
267                      * 3. If there is on-hold connection, reject directly
268                      */
269                     if (mBatchs.size() == 0 && mPendingConnection == null) {
270                         Log.i(TAG, "Start Obex Server");
271                         createServerSession(transport);
272                     } else {
273                         if (mPendingConnection != null) {
274                             Log.w(TAG, "OPP busy! Reject connection");
275                             try {
276                                 transport.close();
277                             } catch (IOException e) {
278                                 Log.e(TAG, "close tranport error");
279                             }
280                         } else if (Constants.USE_TCP_DEBUG && !Constants.USE_TCP_SIMPLE_SERVER) {
281                             Log.i(TAG, "Start Obex Server in TCP DEBUG mode");
282                             createServerSession(transport);
283                         } else {
284                             Log.i(TAG, "OPP busy! Retry after 1 second");
285                             mIncomingRetries = mIncomingRetries + 1;
286                             mPendingConnection = transport;
287                             Message msg1 = Message.obtain(mHandler);
288                             msg1.what = MSG_INCOMING_CONNECTION_RETRY;
289                             mHandler.sendMessageDelayed(msg1, 1000);
290                         }
291                     }
292                     break;
293                 case MSG_INCOMING_CONNECTION_RETRY:
294                     if (mBatchs.size() == 0) {
295                         Log.i(TAG, "Start Obex Server");
296                         createServerSession(mPendingConnection);
297                         mIncomingRetries = 0;
298                         mPendingConnection = null;
299                     } else {
300                         if (mIncomingRetries == 20) {
301                             Log.w(TAG, "Retried 20 seconds, reject connection");
302                             try {
303                                 mPendingConnection.close();
304                             } catch (IOException e) {
305                                 Log.e(TAG, "close tranport error");
306                             }
307                             mIncomingRetries = 0;
308                             mPendingConnection = null;
309                         } else {
310                             Log.i(TAG, "OPP busy! Retry after 1 second");
311                             mIncomingRetries = mIncomingRetries + 1;
312                             Message msg2 = Message.obtain(mHandler);
313                             msg2.what = MSG_INCOMING_CONNECTION_RETRY;
314                             mHandler.sendMessageDelayed(msg2, 1000);
315                         }
316                     }
317                     break;
318             }
319         }
320     };
321 
startSocketListener()322     private void startSocketListener() {
323 
324         if (V) Log.v(TAG, "start RfcommListener");
325         mSocketListener.start(mHandler);
326         if (V) Log.v(TAG, "RfcommListener started");
327     }
328 
329     @Override
onDestroy()330     public void onDestroy() {
331         if (V) Log.v(TAG, "onDestroy");
332         super.onDestroy();
333         getContentResolver().unregisterContentObserver(mObserver);
334         unregisterReceiver(mBluetoothReceiver);
335         mSocketListener.stop();
336 
337         if(mBatchs != null) {
338             mBatchs.clear();
339         }
340         if(mShares != null) {
341             mShares.clear();
342         }
343         if(mHandler != null) {
344             mHandler.removeCallbacksAndMessages(null);
345         }
346     }
347 
348     /* suppose we auto accept an incoming OPUSH connection */
createServerSession(ObexTransport transport)349     private void createServerSession(ObexTransport transport) {
350         mServerSession = new BluetoothOppObexServerSession(this, transport);
351         mServerSession.preStart();
352         if (D) Log.d(TAG, "Get ServerSession " + mServerSession.toString()
353                     + " for incoming connection" + transport.toString());
354     }
355 
356     private final BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {
357         @Override
358         public void onReceive(Context context, Intent intent) {
359             String action = intent.getAction();
360 
361             if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
362                 switch (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
363                     case BluetoothAdapter.STATE_ON:
364                         if (V) Log.v(TAG,
365                                     "Receiver BLUETOOTH_STATE_CHANGED_ACTION, BLUETOOTH_STATE_ON");
366                         startSocketListener();
367                         break;
368                     case BluetoothAdapter.STATE_TURNING_OFF:
369                         if (V) Log.v(TAG, "Receiver DISABLED_ACTION ");
370                         //FIX: Don't block main thread
371                         /*
372                         mSocketListener.stop();
373                         mListenStarted = false;
374                         synchronized (BluetoothOppService.this) {
375                             if (mUpdateThread == null) {
376                                 stopSelf();
377                             }
378                         }
379                         */
380                         mHandler.sendMessage(mHandler.obtainMessage(STOP_LISTENER));
381 
382                         break;
383                 }
384             }
385         }
386     };
387 
updateFromProvider()388     private void updateFromProvider() {
389         synchronized (BluetoothOppService.this) {
390             mPendingUpdate = true;
391             if (mUpdateThread == null) {
392                 mUpdateThread = new UpdateThread();
393                 mUpdateThread.start();
394             }
395         }
396     }
397 
398     private class UpdateThread extends Thread {
UpdateThread()399         public UpdateThread() {
400             super("Bluetooth Share Service");
401         }
402 
403         @Override
run()404         public void run() {
405             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
406 
407             boolean keepService = false;
408             for (;;) {
409                 synchronized (BluetoothOppService.this) {
410                     if (mUpdateThread != this) {
411                         throw new IllegalStateException(
412                                 "multiple UpdateThreads in BluetoothOppService");
413                     }
414                     if (V) Log.v(TAG, "pendingUpdate is " + mPendingUpdate + " keepUpdateThread is "
415                                 + keepService + " sListenStarted is " + mListenStarted);
416                     if (!mPendingUpdate) {
417                         mUpdateThread = null;
418                         if (!keepService && !mListenStarted) {
419                             stopSelf();
420                             break;
421                         }
422                         return;
423                     }
424                     mPendingUpdate = false;
425                 }
426                 Cursor cursor = getContentResolver().query(BluetoothShare.CONTENT_URI, null, null,
427                         null, BluetoothShare._ID);
428 
429                 if (cursor == null) {
430                     return;
431                 }
432 
433                 cursor.moveToFirst();
434 
435                 int arrayPos = 0;
436 
437                 keepService = false;
438                 boolean isAfterLast = cursor.isAfterLast();
439 
440                 int idColumn = cursor.getColumnIndexOrThrow(BluetoothShare._ID);
441                 /*
442                  * Walk the cursor and the local array to keep them in sync. The
443                  * key to the algorithm is that the ids are unique and sorted
444                  * both in the cursor and in the array, so that they can be
445                  * processed in order in both sources at the same time: at each
446                  * step, both sources point to the lowest id that hasn't been
447                  * processed from that source, and the algorithm processes the
448                  * lowest id from those two possibilities. At each step: -If the
449                  * array contains an entry that's not in the cursor, remove the
450                  * entry, move to next entry in the array. -If the array
451                  * contains an entry that's in the cursor, nothing to do, move
452                  * to next cursor row and next array entry. -If the cursor
453                  * contains an entry that's not in the array, insert a new entry
454                  * in the array, move to next cursor row and next array entry.
455                  */
456                 while (!isAfterLast || arrayPos < mShares.size()) {
457                     if (isAfterLast) {
458                         // We're beyond the end of the cursor but there's still
459                         // some
460                         // stuff in the local array, which can only be junk
461                         if (V) Log.v(TAG, "Array update: trimming " +
462                                 mShares.get(arrayPos).mId + " @ " + arrayPos);
463 
464                         if (shouldScanFile(arrayPos)) {
465                             scanFile(null, arrayPos);
466                         }
467                         deleteShare(arrayPos); // this advances in the array
468                     } else {
469                         int id = cursor.getInt(idColumn);
470 
471                         if (arrayPos == mShares.size()) {
472                             insertShare(cursor, arrayPos);
473                             if (V) Log.v(TAG, "Array update: inserting " + id + " @ " + arrayPos);
474                             if (shouldScanFile(arrayPos) && (!scanFile(cursor, arrayPos))) {
475                                 keepService = true;
476                             }
477                             if (visibleNotification(arrayPos)) {
478                                 keepService = true;
479                             }
480                             if (needAction(arrayPos)) {
481                                 keepService = true;
482                             }
483 
484                             ++arrayPos;
485                             cursor.moveToNext();
486                             isAfterLast = cursor.isAfterLast();
487                         } else {
488                             int arrayId = mShares.get(arrayPos).mId;
489 
490                             if (arrayId < id) {
491                                 if (V) Log.v(TAG, "Array update: removing " + arrayId + " @ "
492                                             + arrayPos);
493                                 if (shouldScanFile(arrayPos)) {
494                                     scanFile(null, arrayPos);
495                                 }
496                                 deleteShare(arrayPos);
497                             } else if (arrayId == id) {
498                                 // This cursor row already exists in the stored
499                                 // array
500                                 updateShare(cursor, arrayPos, userAccepted);
501                                 if (shouldScanFile(arrayPos) && (!scanFile(cursor, arrayPos))) {
502                                     keepService = true;
503                                 }
504                                 if (visibleNotification(arrayPos)) {
505                                     keepService = true;
506                                 }
507                                 if (needAction(arrayPos)) {
508                                     keepService = true;
509                                 }
510 
511                                 ++arrayPos;
512                                 cursor.moveToNext();
513                                 isAfterLast = cursor.isAfterLast();
514                             } else {
515                                 // This cursor entry didn't exist in the stored
516                                 // array
517                                 if (V) Log.v(TAG, "Array update: appending " + id + " @ " + arrayPos);
518                                 insertShare(cursor, arrayPos);
519 
520                                 if (shouldScanFile(arrayPos) && (!scanFile(cursor, arrayPos))) {
521                                     keepService = true;
522                                 }
523                                 if (visibleNotification(arrayPos)) {
524                                     keepService = true;
525                                 }
526                                 if (needAction(arrayPos)) {
527                                     keepService = true;
528                                 }
529                                 ++arrayPos;
530                                 cursor.moveToNext();
531                                 isAfterLast = cursor.isAfterLast();
532                             }
533                         }
534                     }
535                 }
536 
537                 mNotifier.updateNotification();
538 
539                 cursor.close();
540             }
541         }
542 
543     }
544 
insertShare(Cursor cursor, int arrayPos)545     private void insertShare(Cursor cursor, int arrayPos) {
546         String uriString = cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.URI));
547         Uri uri;
548         if (uriString != null) {
549             uri = Uri.parse(uriString);
550             Log.d(TAG, "insertShare parsed URI: " + uri);
551         } else {
552             uri = null;
553             Log.e(TAG, "insertShare found null URI at cursor!");
554         }
555         BluetoothOppShareInfo info = new BluetoothOppShareInfo(
556                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID)),
557                 uri,
558                 cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT)),
559                 cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare._DATA)),
560                 cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.MIMETYPE)),
561                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION)),
562                 cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.DESTINATION)),
563                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.VISIBILITY)),
564                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION)),
565                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.STATUS)),
566                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES)),
567                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES)),
568                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP)),
569                 cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) != Constants.MEDIA_SCANNED_NOT_SCANNED);
570 
571         if (V) {
572             Log.v(TAG, "Service adding new entry");
573             Log.v(TAG, "ID      : " + info.mId);
574             // Log.v(TAG, "URI     : " + ((info.mUri != null) ? "yes" : "no"));
575             Log.v(TAG, "URI     : " + info.mUri);
576             Log.v(TAG, "HINT    : " + info.mHint);
577             Log.v(TAG, "FILENAME: " + info.mFilename);
578             Log.v(TAG, "MIMETYPE: " + info.mMimetype);
579             Log.v(TAG, "DIRECTION: " + info.mDirection);
580             Log.v(TAG, "DESTINAT: " + info.mDestination);
581             Log.v(TAG, "VISIBILI: " + info.mVisibility);
582             Log.v(TAG, "CONFIRM : " + info.mConfirm);
583             Log.v(TAG, "STATUS  : " + info.mStatus);
584             Log.v(TAG, "TOTAL   : " + info.mTotalBytes);
585             Log.v(TAG, "CURRENT : " + info.mCurrentBytes);
586             Log.v(TAG, "TIMESTAMP : " + info.mTimestamp);
587             Log.v(TAG, "SCANNED : " + info.mMediaScanned);
588         }
589 
590         mShares.add(arrayPos, info);
591 
592         /* Mark the info as failed if it's in invalid status */
593         if (info.isObsolete()) {
594             Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_UNKNOWN_ERROR);
595         }
596         /*
597          * Add info into a batch. The logic is
598          * 1) Only add valid and readyToStart info
599          * 2) If there is no batch, create a batch and insert this transfer into batch,
600          * then run the batch
601          * 3) If there is existing batch and timestamp match, insert transfer into batch
602          * 4) If there is existing batch and timestamp does not match, create a new batch and
603          * put in queue
604          */
605 
606         if (info.isReadyToStart()) {
607             if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
608                 /* check if the file exists */
609                 BluetoothOppSendFileInfo sendFileInfo = BluetoothOppUtility.getSendFileInfo(
610                         info.mUri);
611                 if (sendFileInfo == null || sendFileInfo.mInputStream == null) {
612                     Log.e(TAG, "Can't open file for OUTBOUND info " + info.mId);
613                     Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_BAD_REQUEST);
614                     BluetoothOppUtility.closeSendFileInfo(info.mUri);
615                     return;
616                 }
617             }
618             if (mBatchs.size() == 0) {
619                 BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info);
620                 newBatch.mId = mBatchId;
621                 mBatchId++;
622                 mBatchs.add(newBatch);
623                 if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
624                     if (V) Log.v(TAG, "Service create new Batch " + newBatch.mId
625                                 + " for OUTBOUND info " + info.mId);
626                     mTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch);
627                 } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) {
628                     if (V) Log.v(TAG, "Service create new Batch " + newBatch.mId
629                                 + " for INBOUND info " + info.mId);
630                     mServerTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch,
631                             mServerSession);
632                 }
633 
634                 if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND && mTransfer != null) {
635                     if (V) Log.v(TAG, "Service start transfer new Batch " + newBatch.mId
636                                 + " for info " + info.mId);
637                     mTransfer.start();
638                 } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND
639                         && mServerTransfer != null) {
640                     if (V) Log.v(TAG, "Service start server transfer new Batch " + newBatch.mId
641                                 + " for info " + info.mId);
642                     mServerTransfer.start();
643                 }
644 
645             } else {
646                 int i = findBatchWithTimeStamp(info.mTimestamp);
647                 if (i != -1) {
648                     if (V) Log.v(TAG, "Service add info " + info.mId + " to existing batch "
649                                 + mBatchs.get(i).mId);
650                     mBatchs.get(i).addShare(info);
651                 } else {
652                     // There is ongoing batch
653                     BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info);
654                     newBatch.mId = mBatchId;
655                     mBatchId++;
656                     mBatchs.add(newBatch);
657                     if (V) Log.v(TAG, "Service add new Batch " + newBatch.mId + " for info " +
658                             info.mId);
659                     if (Constants.USE_TCP_DEBUG && !Constants.USE_TCP_SIMPLE_SERVER) {
660                         // only allow  concurrent serverTransfer in debug mode
661                         if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) {
662                             if (V) Log.v(TAG, "TCP_DEBUG start server transfer new Batch " +
663                                     newBatch.mId + " for info " + info.mId);
664                             mServerTransfer = new BluetoothOppTransfer(this, mPowerManager,
665                                     newBatch, mServerSession);
666                             mServerTransfer.start();
667                         }
668                     }
669                 }
670             }
671         }
672     }
673 
updateShare(Cursor cursor, int arrayPos, boolean userAccepted)674     private void updateShare(Cursor cursor, int arrayPos, boolean userAccepted) {
675         BluetoothOppShareInfo info = mShares.get(arrayPos);
676         int statusColumn = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS);
677 
678         info.mId = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID));
679         if (info.mUri != null) {
680             info.mUri = Uri.parse(stringFromCursor(info.mUri.toString(), cursor,
681                     BluetoothShare.URI));
682         } else {
683             Log.w(TAG, "updateShare() called for ID " + info.mId + " with null URI");
684         }
685         info.mHint = stringFromCursor(info.mHint, cursor, BluetoothShare.FILENAME_HINT);
686         info.mFilename = stringFromCursor(info.mFilename, cursor, BluetoothShare._DATA);
687         info.mMimetype = stringFromCursor(info.mMimetype, cursor, BluetoothShare.MIMETYPE);
688         info.mDirection = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION));
689         info.mDestination = stringFromCursor(info.mDestination, cursor, BluetoothShare.DESTINATION);
690         int newVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.VISIBILITY));
691 
692         boolean confirmed = false;
693         int newConfirm = cursor.getInt(cursor
694                 .getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION));
695 
696         if (info.mVisibility == BluetoothShare.VISIBILITY_VISIBLE
697                 && newVisibility != BluetoothShare.VISIBILITY_VISIBLE
698                 && (BluetoothShare.isStatusCompleted(info.mStatus) || newConfirm == BluetoothShare.USER_CONFIRMATION_PENDING)) {
699             mNotifier.mNotificationMgr.cancel(info.mId);
700         }
701 
702         info.mVisibility = newVisibility;
703 
704         if (info.mConfirm == BluetoothShare.USER_CONFIRMATION_PENDING
705                 && newConfirm != BluetoothShare.USER_CONFIRMATION_PENDING) {
706             confirmed = true;
707         }
708         info.mConfirm = cursor.getInt(cursor
709                 .getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION));
710         int newStatus = cursor.getInt(statusColumn);
711 
712         if (!BluetoothShare.isStatusCompleted(info.mStatus)
713                 && BluetoothShare.isStatusCompleted(newStatus)) {
714             mNotifier.mNotificationMgr.cancel(info.mId);
715         }
716 
717         info.mStatus = newStatus;
718         info.mTotalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES));
719         info.mCurrentBytes = cursor.getInt(cursor
720                 .getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES));
721         info.mTimestamp = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP));
722         info.mMediaScanned = (cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) != Constants.MEDIA_SCANNED_NOT_SCANNED);
723 
724         if (confirmed) {
725             if (V) Log.v(TAG, "Service handle info " + info.mId + " confirmed");
726             /* Inbounds transfer get user confirmation, so we start it */
727             int i = findBatchWithTimeStamp(info.mTimestamp);
728             if (i != -1) {
729                 BluetoothOppBatch batch = mBatchs.get(i);
730                 if (mServerTransfer != null && batch.mId == mServerTransfer.getBatchId()) {
731                     mServerTransfer.setConfirmed();
732                 } //TODO need to think about else
733             }
734         }
735         int i = findBatchWithTimeStamp(info.mTimestamp);
736         if (i != -1) {
737             BluetoothOppBatch batch = mBatchs.get(i);
738             if (batch.mStatus == Constants.BATCH_STATUS_FINISHED
739                     || batch.mStatus == Constants.BATCH_STATUS_FAILED) {
740                 if (V) Log.v(TAG, "Batch " + batch.mId + " is finished");
741                 if (batch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
742                     if (mTransfer == null) {
743                         Log.e(TAG, "Unexpected error! mTransfer is null");
744                     } else if (batch.mId == mTransfer.getBatchId()) {
745                         mTransfer.stop();
746                     } else {
747                         Log.e(TAG, "Unexpected error! batch id " + batch.mId
748                                 + " doesn't match mTransfer id " + mTransfer.getBatchId());
749                     }
750                     mTransfer = null;
751                 } else {
752                     if (mServerTransfer == null) {
753                         Log.e(TAG, "Unexpected error! mServerTransfer is null");
754                     } else if (batch.mId == mServerTransfer.getBatchId()) {
755                         mServerTransfer.stop();
756                     } else {
757                         Log.e(TAG, "Unexpected error! batch id " + batch.mId
758                                 + " doesn't match mServerTransfer id "
759                                 + mServerTransfer.getBatchId());
760                     }
761                     mServerTransfer = null;
762                 }
763                 removeBatch(batch);
764             }
765         }
766     }
767 
768     /**
769      * Removes the local copy of the info about a share.
770      */
deleteShare(int arrayPos)771     private void deleteShare(int arrayPos) {
772         BluetoothOppShareInfo info = mShares.get(arrayPos);
773 
774         /*
775          * Delete arrayPos from a batch. The logic is
776          * 1) Search existing batch for the info
777          * 2) cancel the batch
778          * 3) If the batch become empty delete the batch
779          */
780         int i = findBatchWithTimeStamp(info.mTimestamp);
781         if (i != -1) {
782             BluetoothOppBatch batch = mBatchs.get(i);
783             if (batch.hasShare(info)) {
784                 if (V) Log.v(TAG, "Service cancel batch for share " + info.mId);
785                 batch.cancelBatch();
786             }
787             if (batch.isEmpty()) {
788                 if (V) Log.v(TAG, "Service remove batch  " + batch.mId);
789                 removeBatch(batch);
790             }
791         }
792         mShares.remove(arrayPos);
793     }
794 
stringFromCursor(String old, Cursor cursor, String column)795     private String stringFromCursor(String old, Cursor cursor, String column) {
796         int index = cursor.getColumnIndexOrThrow(column);
797         if (old == null) {
798             return cursor.getString(index);
799         }
800         if (mNewChars == null) {
801             mNewChars = new CharArrayBuffer(128);
802         }
803         cursor.copyStringToBuffer(index, mNewChars);
804         int length = mNewChars.sizeCopied;
805         if (length != old.length()) {
806             return cursor.getString(index);
807         }
808         if (mOldChars == null || mOldChars.sizeCopied < length) {
809             mOldChars = new CharArrayBuffer(length);
810         }
811         char[] oldArray = mOldChars.data;
812         char[] newArray = mNewChars.data;
813         old.getChars(0, length, oldArray, 0);
814         for (int i = length - 1; i >= 0; --i) {
815             if (oldArray[i] != newArray[i]) {
816                 return new String(newArray, 0, length);
817             }
818         }
819         return old;
820     }
821 
findBatchWithTimeStamp(long timestamp)822     private int findBatchWithTimeStamp(long timestamp) {
823         for (int i = mBatchs.size() - 1; i >= 0; i--) {
824             if (mBatchs.get(i).mTimestamp == timestamp) {
825                 return i;
826             }
827         }
828         return -1;
829     }
830 
removeBatch(BluetoothOppBatch batch)831     private void removeBatch(BluetoothOppBatch batch) {
832         if (V) Log.v(TAG, "Remove batch " + batch.mId);
833         mBatchs.remove(batch);
834         BluetoothOppBatch nextBatch;
835         if (mBatchs.size() > 0) {
836             for (int i = 0; i < mBatchs.size(); i++) {
837                 // we have a running batch
838                 nextBatch = mBatchs.get(i);
839                 if (nextBatch.mStatus == Constants.BATCH_STATUS_RUNNING) {
840                     return;
841                 } else {
842                     // just finish a transfer, start pending outbound transfer
843                     if (nextBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
844                         if (V) Log.v(TAG, "Start pending outbound batch " + nextBatch.mId);
845                         mTransfer = new BluetoothOppTransfer(this, mPowerManager, nextBatch);
846                         mTransfer.start();
847                         return;
848                     } else if (nextBatch.mDirection == BluetoothShare.DIRECTION_INBOUND
849                             && mServerSession != null) {
850                         // have to support pending inbound transfer
851                         // if an outbound transfer and incoming socket happens together
852                         if (V) Log.v(TAG, "Start pending inbound batch " + nextBatch.mId);
853                         mServerTransfer = new BluetoothOppTransfer(this, mPowerManager, nextBatch,
854                                                                    mServerSession);
855                         mServerTransfer.start();
856                         if (nextBatch.getPendingShare().mConfirm ==
857                                 BluetoothShare.USER_CONFIRMATION_CONFIRMED) {
858                             mServerTransfer.setConfirmed();
859                         }
860                         return;
861                     }
862                 }
863             }
864         }
865     }
866 
needAction(int arrayPos)867     private boolean needAction(int arrayPos) {
868         BluetoothOppShareInfo info = mShares.get(arrayPos);
869         if (BluetoothShare.isStatusCompleted(info.mStatus)) {
870             return false;
871         }
872         return true;
873     }
874 
visibleNotification(int arrayPos)875     private boolean visibleNotification(int arrayPos) {
876         BluetoothOppShareInfo info = mShares.get(arrayPos);
877         return info.hasCompletionNotification();
878     }
879 
scanFile(Cursor cursor, int arrayPos)880     private boolean scanFile(Cursor cursor, int arrayPos) {
881         BluetoothOppShareInfo info = mShares.get(arrayPos);
882         synchronized (BluetoothOppService.this) {
883             if (D) Log.d(TAG, "Scanning file " + info.mFilename);
884             if (!mMediaScanInProgress) {
885                 mMediaScanInProgress = true;
886                 new MediaScannerNotifier(this, info, mHandler);
887                 return true;
888             } else {
889                 return false;
890             }
891         }
892     }
893 
shouldScanFile(int arrayPos)894     private boolean shouldScanFile(int arrayPos) {
895         BluetoothOppShareInfo info = mShares.get(arrayPos);
896         return BluetoothShare.isStatusSuccess(info.mStatus)
897                 && info.mDirection == BluetoothShare.DIRECTION_INBOUND && !info.mMediaScanned &&
898                 info.mConfirm != BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED;
899     }
900 
901     // Run in a background thread at boot.
trimDatabase(ContentResolver contentResolver)902     private static void trimDatabase(ContentResolver contentResolver) {
903         final String INVISIBLE = BluetoothShare.VISIBILITY + "=" +
904                 BluetoothShare.VISIBILITY_HIDDEN;
905 
906         // remove the invisible/complete/outbound shares
907         final String WHERE_INVISIBLE_COMPLETE_OUTBOUND = BluetoothShare.DIRECTION + "="
908                 + BluetoothShare.DIRECTION_OUTBOUND + " AND " + BluetoothShare.STATUS + ">="
909                 + BluetoothShare.STATUS_SUCCESS + " AND " + INVISIBLE;
910         int delNum = contentResolver.delete(BluetoothShare.CONTENT_URI,
911                 WHERE_INVISIBLE_COMPLETE_OUTBOUND, null);
912         if (V) Log.v(TAG, "Deleted complete outbound shares, number =  " + delNum);
913 
914         // remove the invisible/finished/inbound/failed shares
915         final String WHERE_INVISIBLE_COMPLETE_INBOUND_FAILED = BluetoothShare.DIRECTION + "="
916                 + BluetoothShare.DIRECTION_INBOUND + " AND " + BluetoothShare.STATUS + ">"
917                 + BluetoothShare.STATUS_SUCCESS + " AND " + INVISIBLE;
918         delNum = contentResolver.delete(BluetoothShare.CONTENT_URI,
919                 WHERE_INVISIBLE_COMPLETE_INBOUND_FAILED, null);
920         if (V) Log.v(TAG, "Deleted complete inbound failed shares, number = " + delNum);
921 
922         // Only keep the inbound and successful shares for LiverFolder use
923         // Keep the latest 1000 to easy db query
924         final String WHERE_INBOUND_SUCCESS = BluetoothShare.DIRECTION + "="
925                 + BluetoothShare.DIRECTION_INBOUND + " AND " + BluetoothShare.STATUS + "="
926                 + BluetoothShare.STATUS_SUCCESS + " AND " + INVISIBLE;
927         Cursor cursor = contentResolver.query(BluetoothShare.CONTENT_URI, new String[] {
928             BluetoothShare._ID
929         }, WHERE_INBOUND_SUCCESS, null, BluetoothShare._ID); // sort by id
930 
931         if (cursor == null) {
932             return;
933         }
934 
935         int recordNum = cursor.getCount();
936         if (recordNum > Constants.MAX_RECORDS_IN_DATABASE) {
937             int numToDelete = recordNum - Constants.MAX_RECORDS_IN_DATABASE;
938 
939             if (cursor.moveToPosition(numToDelete)) {
940                 int columnId = cursor.getColumnIndexOrThrow(BluetoothShare._ID);
941                 long id = cursor.getLong(columnId);
942                 delNum = contentResolver.delete(BluetoothShare.CONTENT_URI,
943                         BluetoothShare._ID + " < " + id, null);
944                 if (V) Log.v(TAG, "Deleted old inbound success share: " + delNum);
945             }
946         }
947         cursor.close();
948     }
949 
950     private static class MediaScannerNotifier implements MediaScannerConnectionClient {
951 
952         private MediaScannerConnection mConnection;
953 
954         private BluetoothOppShareInfo mInfo;
955 
956         private Context mContext;
957 
958         private Handler mCallback;
959 
MediaScannerNotifier(Context context, BluetoothOppShareInfo info, Handler handler)960         public MediaScannerNotifier(Context context, BluetoothOppShareInfo info, Handler handler) {
961             mContext = context;
962             mInfo = info;
963             mCallback = handler;
964             mConnection = new MediaScannerConnection(mContext, this);
965             if (V) Log.v(TAG, "Connecting to MediaScannerConnection ");
966             mConnection.connect();
967         }
968 
onMediaScannerConnected()969         public void onMediaScannerConnected() {
970             if (V) Log.v(TAG, "MediaScannerConnection onMediaScannerConnected");
971             mConnection.scanFile(mInfo.mFilename, mInfo.mMimetype);
972         }
973 
onScanCompleted(String path, Uri uri)974         public void onScanCompleted(String path, Uri uri) {
975             try {
976                 if (V) {
977                     Log.v(TAG, "MediaScannerConnection onScanCompleted");
978                     Log.v(TAG, "MediaScannerConnection path is " + path);
979                     Log.v(TAG, "MediaScannerConnection Uri is " + uri);
980                 }
981                 if (uri != null) {
982                     Message msg = Message.obtain();
983                     msg.setTarget(mCallback);
984                     msg.what = MEDIA_SCANNED;
985                     msg.arg1 = mInfo.mId;
986                     msg.obj = uri;
987                     msg.sendToTarget();
988                 } else {
989                     Message msg = Message.obtain();
990                     msg.setTarget(mCallback);
991                     msg.what = MEDIA_SCANNED_FAILED;
992                     msg.arg1 = mInfo.mId;
993                     msg.sendToTarget();
994                 }
995             } catch (Exception ex) {
996                 Log.v(TAG, "!!!MediaScannerConnection exception: " + ex);
997             } finally {
998                 if (V) Log.v(TAG, "MediaScannerConnection disconnect");
999                 mConnection.disconnect();
1000             }
1001         }
1002     }
1003 }
1004