• 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                     if(mSocketListener != null){
223                         mSocketListener.stop();
224                     }
225                     mListenStarted = false;
226                     //Stop Active INBOUND Transfer
227                     if(mServerTransfer != null){
228                        mServerTransfer.onBatchCanceled();
229                        mServerTransfer =null;
230                     }
231                     //Stop Active OUTBOUND Transfer
232                     if(mTransfer != null){
233                        mTransfer.onBatchCanceled();
234                        mTransfer =null;
235                     }
236                     synchronized (BluetoothOppService.this) {
237                         if (mUpdateThread == null) {
238                             stopSelf();
239                         }
240                     }
241                     break;
242                 case START_LISTENER:
243                     if (mAdapter.isEnabled()) {
244                         startSocketListener();
245                     }
246                     break;
247                 case MEDIA_SCANNED:
248                     if (V) Log.v(TAG, "Update mInfo.id " + msg.arg1 + " for data uri= "
249                                 + msg.obj.toString());
250                     ContentValues updateValues = new ContentValues();
251                     Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + msg.arg1);
252                     updateValues.put(Constants.MEDIA_SCANNED, Constants.MEDIA_SCANNED_SCANNED_OK);
253                     updateValues.put(BluetoothShare.URI, msg.obj.toString()); // update
254                     updateValues.put(BluetoothShare.MIMETYPE, getContentResolver().getType(
255                             Uri.parse(msg.obj.toString())));
256                     getContentResolver().update(contentUri, updateValues, null, null);
257                     synchronized (BluetoothOppService.this) {
258                         mMediaScanInProgress = false;
259                     }
260                     break;
261                 case MEDIA_SCANNED_FAILED:
262                     Log.v(TAG, "Update mInfo.id " + msg.arg1 + " for MEDIA_SCANNED_FAILED");
263                     ContentValues updateValues1 = new ContentValues();
264                     Uri contentUri1 = Uri.parse(BluetoothShare.CONTENT_URI + "/" + msg.arg1);
265                     updateValues1.put(Constants.MEDIA_SCANNED,
266                             Constants.MEDIA_SCANNED_SCANNED_FAILED);
267                     getContentResolver().update(contentUri1, updateValues1, null, null);
268                     synchronized (BluetoothOppService.this) {
269                         mMediaScanInProgress = false;
270                     }
271                     break;
272                 case BluetoothOppRfcommListener.MSG_INCOMING_BTOPP_CONNECTION:
273                     if (D) Log.d(TAG, "Get incoming connection");
274                     ObexTransport transport = (ObexTransport)msg.obj;
275                     /*
276                      * Strategy for incoming connections:
277                      * 1. If there is no ongoing transfer, no on-hold connection, start it
278                      * 2. If there is ongoing transfer, hold it for 20 seconds(1 seconds * 20 times)
279                      * 3. If there is on-hold connection, reject directly
280                      */
281                     if (mBatchs.size() == 0 && mPendingConnection == null) {
282                         Log.i(TAG, "Start Obex Server");
283                         createServerSession(transport);
284                     } else {
285                         if (mPendingConnection != null) {
286                             Log.w(TAG, "OPP busy! Reject connection");
287                             try {
288                                 transport.close();
289                             } catch (IOException e) {
290                                 Log.e(TAG, "close tranport error");
291                             }
292                         } else if (Constants.USE_TCP_DEBUG && !Constants.USE_TCP_SIMPLE_SERVER) {
293                             Log.i(TAG, "Start Obex Server in TCP DEBUG mode");
294                             createServerSession(transport);
295                         } else {
296                             Log.i(TAG, "OPP busy! Retry after 1 second");
297                             mIncomingRetries = mIncomingRetries + 1;
298                             mPendingConnection = transport;
299                             Message msg1 = Message.obtain(mHandler);
300                             msg1.what = MSG_INCOMING_CONNECTION_RETRY;
301                             mHandler.sendMessageDelayed(msg1, 1000);
302                         }
303                     }
304                     break;
305                 case MSG_INCOMING_CONNECTION_RETRY:
306                     if (mBatchs.size() == 0) {
307                         Log.i(TAG, "Start Obex Server");
308                         createServerSession(mPendingConnection);
309                         mIncomingRetries = 0;
310                         mPendingConnection = null;
311                     } else {
312                         if (mIncomingRetries == 20) {
313                             Log.w(TAG, "Retried 20 seconds, reject connection");
314                             try {
315                                 mPendingConnection.close();
316                             } catch (IOException e) {
317                                 Log.e(TAG, "close tranport error");
318                             }
319                             mIncomingRetries = 0;
320                             mPendingConnection = null;
321                         } else {
322                             Log.i(TAG, "OPP busy! Retry after 1 second");
323                             mIncomingRetries = mIncomingRetries + 1;
324                             Message msg2 = Message.obtain(mHandler);
325                             msg2.what = MSG_INCOMING_CONNECTION_RETRY;
326                             mHandler.sendMessageDelayed(msg2, 1000);
327                         }
328                     }
329                     break;
330             }
331         }
332     };
333 
startSocketListener()334     private void startSocketListener() {
335 
336         if (V) Log.v(TAG, "start RfcommListener");
337         mSocketListener.start(mHandler);
338         if (V) Log.v(TAG, "RfcommListener started");
339     }
340 
341     @Override
onDestroy()342     public void onDestroy() {
343         if (V) Log.v(TAG, "onDestroy");
344         super.onDestroy();
345         getContentResolver().unregisterContentObserver(mObserver);
346         unregisterReceiver(mBluetoothReceiver);
347         mSocketListener.stop();
348 
349         if(mBatchs != null) {
350             mBatchs.clear();
351         }
352         if(mShares != null) {
353             mShares.clear();
354         }
355         if(mHandler != null) {
356             mHandler.removeCallbacksAndMessages(null);
357         }
358     }
359 
360     /* suppose we auto accept an incoming OPUSH connection */
createServerSession(ObexTransport transport)361     private void createServerSession(ObexTransport transport) {
362         mServerSession = new BluetoothOppObexServerSession(this, transport);
363         mServerSession.preStart();
364         if (D) Log.d(TAG, "Get ServerSession " + mServerSession.toString()
365                     + " for incoming connection" + transport.toString());
366     }
367 
368     private final BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {
369         @Override
370         public void onReceive(Context context, Intent intent) {
371             String action = intent.getAction();
372 
373             if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
374                 switch (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
375                     case BluetoothAdapter.STATE_ON:
376                         if (V) Log.v(TAG,
377                                     "Receiver BLUETOOTH_STATE_CHANGED_ACTION, BLUETOOTH_STATE_ON");
378                         startSocketListener();
379                         break;
380                     case BluetoothAdapter.STATE_TURNING_OFF:
381                         if (V) Log.v(TAG, "Receiver DISABLED_ACTION ");
382                         //FIX: Don't block main thread
383                         /*
384                         mSocketListener.stop();
385                         mListenStarted = false;
386                         synchronized (BluetoothOppService.this) {
387                             if (mUpdateThread == null) {
388                                 stopSelf();
389                             }
390                         }
391                         */
392                         mHandler.sendMessage(mHandler.obtainMessage(STOP_LISTENER));
393 
394                         break;
395                 }
396             }
397         }
398     };
399 
updateFromProvider()400     private void updateFromProvider() {
401         synchronized (BluetoothOppService.this) {
402             mPendingUpdate = true;
403             if (mUpdateThread == null) {
404                 mUpdateThread = new UpdateThread();
405                 mUpdateThread.start();
406             }
407         }
408     }
409 
410     private class UpdateThread extends Thread {
UpdateThread()411         public UpdateThread() {
412             super("Bluetooth Share Service");
413         }
414 
415         @Override
run()416         public void run() {
417             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
418 
419             boolean keepService = false;
420             for (;;) {
421                 synchronized (BluetoothOppService.this) {
422                     if (mUpdateThread != this) {
423                         throw new IllegalStateException(
424                                 "multiple UpdateThreads in BluetoothOppService");
425                     }
426                     if (V) Log.v(TAG, "pendingUpdate is " + mPendingUpdate + " keepUpdateThread is "
427                                 + keepService + " sListenStarted is " + mListenStarted);
428                     if (!mPendingUpdate) {
429                         mUpdateThread = null;
430                         if (!keepService && !mListenStarted) {
431                             stopSelf();
432                             break;
433                         }
434                         return;
435                     }
436                     mPendingUpdate = false;
437                 }
438                 Cursor cursor = getContentResolver().query(BluetoothShare.CONTENT_URI, null, null,
439                         null, BluetoothShare._ID);
440 
441                 if (cursor == null) {
442                     return;
443                 }
444 
445                 cursor.moveToFirst();
446 
447                 int arrayPos = 0;
448 
449                 keepService = false;
450                 boolean isAfterLast = cursor.isAfterLast();
451 
452                 int idColumn = cursor.getColumnIndexOrThrow(BluetoothShare._ID);
453                 /*
454                  * Walk the cursor and the local array to keep them in sync. The
455                  * key to the algorithm is that the ids are unique and sorted
456                  * both in the cursor and in the array, so that they can be
457                  * processed in order in both sources at the same time: at each
458                  * step, both sources point to the lowest id that hasn't been
459                  * processed from that source, and the algorithm processes the
460                  * lowest id from those two possibilities. At each step: -If the
461                  * array contains an entry that's not in the cursor, remove the
462                  * entry, move to next entry in the array. -If the array
463                  * contains an entry that's in the cursor, nothing to do, move
464                  * to next cursor row and next array entry. -If the cursor
465                  * contains an entry that's not in the array, insert a new entry
466                  * in the array, move to next cursor row and next array entry.
467                  */
468                 while (!isAfterLast || arrayPos < mShares.size()) {
469                     if (isAfterLast) {
470                         // We're beyond the end of the cursor but there's still
471                         // some
472                         // stuff in the local array, which can only be junk
473                         if (mShares.size() != 0)
474                             if (V) Log.v(TAG, "Array update: trimming " +
475                                 mShares.get(arrayPos).mId + " @ " + arrayPos);
476 
477                         if (shouldScanFile(arrayPos)) {
478                             scanFile(null, arrayPos);
479                         }
480                         deleteShare(arrayPos); // this advances in the array
481                     } else {
482                         int id = cursor.getInt(idColumn);
483 
484                         if (arrayPos == mShares.size()) {
485                             insertShare(cursor, arrayPos);
486                             if (V) Log.v(TAG, "Array update: inserting " + id + " @ " + arrayPos);
487                             if (shouldScanFile(arrayPos) && (!scanFile(cursor, arrayPos))) {
488                                 keepService = true;
489                             }
490                             if (visibleNotification(arrayPos)) {
491                                 keepService = true;
492                             }
493                             if (needAction(arrayPos)) {
494                                 keepService = true;
495                             }
496 
497                             ++arrayPos;
498                             cursor.moveToNext();
499                             isAfterLast = cursor.isAfterLast();
500                         } else {
501                             int arrayId = 0;
502                             if (mShares.size() != 0)
503                                 arrayId = mShares.get(arrayPos).mId;
504 
505                             if (arrayId < id) {
506                                 if (V) Log.v(TAG, "Array update: removing " + arrayId + " @ "
507                                             + arrayPos);
508                                 if (shouldScanFile(arrayPos)) {
509                                     scanFile(null, arrayPos);
510                                 }
511                                 deleteShare(arrayPos);
512                             } else if (arrayId == id) {
513                                 // This cursor row already exists in the stored
514                                 // array
515                                 updateShare(cursor, arrayPos, userAccepted);
516                                 if (shouldScanFile(arrayPos) && (!scanFile(cursor, arrayPos))) {
517                                     keepService = true;
518                                 }
519                                 if (visibleNotification(arrayPos)) {
520                                     keepService = true;
521                                 }
522                                 if (needAction(arrayPos)) {
523                                     keepService = true;
524                                 }
525 
526                                 ++arrayPos;
527                                 cursor.moveToNext();
528                                 isAfterLast = cursor.isAfterLast();
529                             } else {
530                                 // This cursor entry didn't exist in the stored
531                                 // array
532                                 if (V) Log.v(TAG, "Array update: appending " + id + " @ " + arrayPos);
533                                 insertShare(cursor, arrayPos);
534 
535                                 if (shouldScanFile(arrayPos) && (!scanFile(cursor, arrayPos))) {
536                                     keepService = true;
537                                 }
538                                 if (visibleNotification(arrayPos)) {
539                                     keepService = true;
540                                 }
541                                 if (needAction(arrayPos)) {
542                                     keepService = true;
543                                 }
544                                 ++arrayPos;
545                                 cursor.moveToNext();
546                                 isAfterLast = cursor.isAfterLast();
547                             }
548                         }
549                     }
550                 }
551 
552                 mNotifier.updateNotification();
553 
554                 cursor.close();
555             }
556         }
557 
558     }
559 
insertShare(Cursor cursor, int arrayPos)560     private void insertShare(Cursor cursor, int arrayPos) {
561         String uriString = cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.URI));
562         Uri uri;
563         if (uriString != null) {
564             uri = Uri.parse(uriString);
565             Log.d(TAG, "insertShare parsed URI: " + uri);
566         } else {
567             uri = null;
568             Log.e(TAG, "insertShare found null URI at cursor!");
569         }
570         BluetoothOppShareInfo info = new BluetoothOppShareInfo(
571                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID)),
572                 uri,
573                 cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT)),
574                 cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare._DATA)),
575                 cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.MIMETYPE)),
576                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION)),
577                 cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.DESTINATION)),
578                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.VISIBILITY)),
579                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION)),
580                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.STATUS)),
581                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES)),
582                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES)),
583                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP)),
584                 cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) != Constants.MEDIA_SCANNED_NOT_SCANNED);
585 
586         if (V) {
587             Log.v(TAG, "Service adding new entry");
588             Log.v(TAG, "ID      : " + info.mId);
589             // Log.v(TAG, "URI     : " + ((info.mUri != null) ? "yes" : "no"));
590             Log.v(TAG, "URI     : " + info.mUri);
591             Log.v(TAG, "HINT    : " + info.mHint);
592             Log.v(TAG, "FILENAME: " + info.mFilename);
593             Log.v(TAG, "MIMETYPE: " + info.mMimetype);
594             Log.v(TAG, "DIRECTION: " + info.mDirection);
595             Log.v(TAG, "DESTINAT: " + info.mDestination);
596             Log.v(TAG, "VISIBILI: " + info.mVisibility);
597             Log.v(TAG, "CONFIRM : " + info.mConfirm);
598             Log.v(TAG, "STATUS  : " + info.mStatus);
599             Log.v(TAG, "TOTAL   : " + info.mTotalBytes);
600             Log.v(TAG, "CURRENT : " + info.mCurrentBytes);
601             Log.v(TAG, "TIMESTAMP : " + info.mTimestamp);
602             Log.v(TAG, "SCANNED : " + info.mMediaScanned);
603         }
604 
605         mShares.add(arrayPos, info);
606 
607         /* Mark the info as failed if it's in invalid status */
608         if (info.isObsolete()) {
609             Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_UNKNOWN_ERROR);
610         }
611         /*
612          * Add info into a batch. The logic is
613          * 1) Only add valid and readyToStart info
614          * 2) If there is no batch, create a batch and insert this transfer into batch,
615          * then run the batch
616          * 3) If there is existing batch and timestamp match, insert transfer into batch
617          * 4) If there is existing batch and timestamp does not match, create a new batch and
618          * put in queue
619          */
620 
621         if (info.isReadyToStart()) {
622             if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
623                 /* check if the file exists */
624                 BluetoothOppSendFileInfo sendFileInfo = BluetoothOppUtility.getSendFileInfo(
625                         info.mUri);
626                 if (sendFileInfo == null || sendFileInfo.mInputStream == null) {
627                     Log.e(TAG, "Can't open file for OUTBOUND info " + info.mId);
628                     Constants.updateShareStatus(this, info.mId, BluetoothShare.STATUS_BAD_REQUEST);
629                     BluetoothOppUtility.closeSendFileInfo(info.mUri);
630                     return;
631                 }
632             }
633             if (mBatchs.size() == 0) {
634                 BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info);
635                 newBatch.mId = mBatchId;
636                 mBatchId++;
637                 mBatchs.add(newBatch);
638                 if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
639                     if (V) Log.v(TAG, "Service create new Batch " + newBatch.mId
640                                 + " for OUTBOUND info " + info.mId);
641                     mTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch);
642                 } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) {
643                     if (V) Log.v(TAG, "Service create new Batch " + newBatch.mId
644                                 + " for INBOUND info " + info.mId);
645                     mServerTransfer = new BluetoothOppTransfer(this, mPowerManager, newBatch,
646                             mServerSession);
647                 }
648 
649                 if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND && mTransfer != null) {
650                     if (V) Log.v(TAG, "Service start transfer new Batch " + newBatch.mId
651                                 + " for info " + info.mId);
652                     mTransfer.start();
653                 } else if (info.mDirection == BluetoothShare.DIRECTION_INBOUND
654                         && mServerTransfer != null) {
655                     if (V) Log.v(TAG, "Service start server transfer new Batch " + newBatch.mId
656                                 + " for info " + info.mId);
657                     mServerTransfer.start();
658                 }
659 
660             } else {
661                 int i = findBatchWithTimeStamp(info.mTimestamp);
662                 if (i != -1) {
663                     if (V) Log.v(TAG, "Service add info " + info.mId + " to existing batch "
664                                 + mBatchs.get(i).mId);
665                     mBatchs.get(i).addShare(info);
666                 } else {
667                     // There is ongoing batch
668                     BluetoothOppBatch newBatch = new BluetoothOppBatch(this, info);
669                     newBatch.mId = mBatchId;
670                     mBatchId++;
671                     mBatchs.add(newBatch);
672                     if (V) Log.v(TAG, "Service add new Batch " + newBatch.mId + " for info " +
673                             info.mId);
674                     if (Constants.USE_TCP_DEBUG && !Constants.USE_TCP_SIMPLE_SERVER) {
675                         // only allow  concurrent serverTransfer in debug mode
676                         if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) {
677                             if (V) Log.v(TAG, "TCP_DEBUG start server transfer new Batch " +
678                                     newBatch.mId + " for info " + info.mId);
679                             mServerTransfer = new BluetoothOppTransfer(this, mPowerManager,
680                                     newBatch, mServerSession);
681                             mServerTransfer.start();
682                         }
683                     }
684                 }
685             }
686         }
687     }
688 
updateShare(Cursor cursor, int arrayPos, boolean userAccepted)689     private void updateShare(Cursor cursor, int arrayPos, boolean userAccepted) {
690         BluetoothOppShareInfo info = mShares.get(arrayPos);
691         int statusColumn = cursor.getColumnIndexOrThrow(BluetoothShare.STATUS);
692 
693         info.mId = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID));
694         if (info.mUri != null) {
695             info.mUri = Uri.parse(stringFromCursor(info.mUri.toString(), cursor,
696                     BluetoothShare.URI));
697         } else {
698             Log.w(TAG, "updateShare() called for ID " + info.mId + " with null URI");
699         }
700         info.mHint = stringFromCursor(info.mHint, cursor, BluetoothShare.FILENAME_HINT);
701         info.mFilename = stringFromCursor(info.mFilename, cursor, BluetoothShare._DATA);
702         info.mMimetype = stringFromCursor(info.mMimetype, cursor, BluetoothShare.MIMETYPE);
703         info.mDirection = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION));
704         info.mDestination = stringFromCursor(info.mDestination, cursor, BluetoothShare.DESTINATION);
705         int newVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.VISIBILITY));
706 
707         boolean confirmed = false;
708         int newConfirm = cursor.getInt(cursor
709                 .getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION));
710 
711         if (info.mVisibility == BluetoothShare.VISIBILITY_VISIBLE
712                 && newVisibility != BluetoothShare.VISIBILITY_VISIBLE
713                 && (BluetoothShare.isStatusCompleted(info.mStatus) || newConfirm == BluetoothShare.USER_CONFIRMATION_PENDING)) {
714             mNotifier.mNotificationMgr.cancel(info.mId);
715         }
716 
717         info.mVisibility = newVisibility;
718 
719         if (info.mConfirm == BluetoothShare.USER_CONFIRMATION_PENDING
720                 && newConfirm != BluetoothShare.USER_CONFIRMATION_PENDING) {
721             confirmed = true;
722         }
723         info.mConfirm = cursor.getInt(cursor
724                 .getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION));
725         int newStatus = cursor.getInt(statusColumn);
726 
727         if (!BluetoothShare.isStatusCompleted(info.mStatus)
728                 && BluetoothShare.isStatusCompleted(newStatus)) {
729             mNotifier.mNotificationMgr.cancel(info.mId);
730         }
731 
732         info.mStatus = newStatus;
733         info.mTotalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES));
734         info.mCurrentBytes = cursor.getInt(cursor
735                 .getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES));
736         info.mTimestamp = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP));
737         info.mMediaScanned = (cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) != Constants.MEDIA_SCANNED_NOT_SCANNED);
738 
739         if (confirmed) {
740             if (V) Log.v(TAG, "Service handle info " + info.mId + " confirmed");
741             /* Inbounds transfer get user confirmation, so we start it */
742             int i = findBatchWithTimeStamp(info.mTimestamp);
743             if (i != -1) {
744                 BluetoothOppBatch batch = mBatchs.get(i);
745                 if (mServerTransfer != null && batch.mId == mServerTransfer.getBatchId()) {
746                     mServerTransfer.setConfirmed();
747                 } //TODO need to think about else
748             }
749         }
750         int i = findBatchWithTimeStamp(info.mTimestamp);
751         if (i != -1) {
752             BluetoothOppBatch batch = mBatchs.get(i);
753             if (batch.mStatus == Constants.BATCH_STATUS_FINISHED
754                     || batch.mStatus == Constants.BATCH_STATUS_FAILED) {
755                 if (V) Log.v(TAG, "Batch " + batch.mId + " is finished");
756                 if (batch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
757                     if (mTransfer == null) {
758                         Log.e(TAG, "Unexpected error! mTransfer is null");
759                     } else if (batch.mId == mTransfer.getBatchId()) {
760                         mTransfer.stop();
761                     } else {
762                         Log.e(TAG, "Unexpected error! batch id " + batch.mId
763                                 + " doesn't match mTransfer id " + mTransfer.getBatchId());
764                     }
765                     mTransfer = null;
766                 } else {
767                     if (mServerTransfer == null) {
768                         Log.e(TAG, "Unexpected error! mServerTransfer is null");
769                     } else if (batch.mId == mServerTransfer.getBatchId()) {
770                         mServerTransfer.stop();
771                     } else {
772                         Log.e(TAG, "Unexpected error! batch id " + batch.mId
773                                 + " doesn't match mServerTransfer id "
774                                 + mServerTransfer.getBatchId());
775                     }
776                     mServerTransfer = null;
777                 }
778                 removeBatch(batch);
779             }
780         }
781     }
782 
783     /**
784      * Removes the local copy of the info about a share.
785      */
deleteShare(int arrayPos)786     private void deleteShare(int arrayPos) {
787         BluetoothOppShareInfo info = mShares.get(arrayPos);
788 
789         /*
790          * Delete arrayPos from a batch. The logic is
791          * 1) Search existing batch for the info
792          * 2) cancel the batch
793          * 3) If the batch become empty delete the batch
794          */
795         int i = findBatchWithTimeStamp(info.mTimestamp);
796         if (i != -1) {
797             BluetoothOppBatch batch = mBatchs.get(i);
798             if (batch.hasShare(info)) {
799                 if (V) Log.v(TAG, "Service cancel batch for share " + info.mId);
800                 batch.cancelBatch();
801             }
802             if (batch.isEmpty()) {
803                 if (V) Log.v(TAG, "Service remove batch  " + batch.mId);
804                 removeBatch(batch);
805             }
806         }
807         mShares.remove(arrayPos);
808     }
809 
stringFromCursor(String old, Cursor cursor, String column)810     private String stringFromCursor(String old, Cursor cursor, String column) {
811         int index = cursor.getColumnIndexOrThrow(column);
812         if (old == null) {
813             return cursor.getString(index);
814         }
815         if (mNewChars == null) {
816             mNewChars = new CharArrayBuffer(128);
817         }
818         cursor.copyStringToBuffer(index, mNewChars);
819         int length = mNewChars.sizeCopied;
820         if (length != old.length()) {
821             return cursor.getString(index);
822         }
823         if (mOldChars == null || mOldChars.sizeCopied < length) {
824             mOldChars = new CharArrayBuffer(length);
825         }
826         char[] oldArray = mOldChars.data;
827         char[] newArray = mNewChars.data;
828         old.getChars(0, length, oldArray, 0);
829         for (int i = length - 1; i >= 0; --i) {
830             if (oldArray[i] != newArray[i]) {
831                 return new String(newArray, 0, length);
832             }
833         }
834         return old;
835     }
836 
findBatchWithTimeStamp(long timestamp)837     private int findBatchWithTimeStamp(long timestamp) {
838         for (int i = mBatchs.size() - 1; i >= 0; i--) {
839             if (mBatchs.get(i).mTimestamp == timestamp) {
840                 return i;
841             }
842         }
843         return -1;
844     }
845 
removeBatch(BluetoothOppBatch batch)846     private void removeBatch(BluetoothOppBatch batch) {
847         if (V) Log.v(TAG, "Remove batch " + batch.mId);
848         mBatchs.remove(batch);
849         BluetoothOppBatch nextBatch;
850         if (mBatchs.size() > 0) {
851             for (int i = 0; i < mBatchs.size(); i++) {
852                 // we have a running batch
853                 nextBatch = mBatchs.get(i);
854                 if (nextBatch.mStatus == Constants.BATCH_STATUS_RUNNING) {
855                     return;
856                 } else {
857                     // just finish a transfer, start pending outbound transfer
858                     if (nextBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
859                         if (V) Log.v(TAG, "Start pending outbound batch " + nextBatch.mId);
860                         mTransfer = new BluetoothOppTransfer(this, mPowerManager, nextBatch);
861                         mTransfer.start();
862                         return;
863                     } else if (nextBatch.mDirection == BluetoothShare.DIRECTION_INBOUND
864                             && mServerSession != null) {
865                         // have to support pending inbound transfer
866                         // if an outbound transfer and incoming socket happens together
867                         if (V) Log.v(TAG, "Start pending inbound batch " + nextBatch.mId);
868                         mServerTransfer = new BluetoothOppTransfer(this, mPowerManager, nextBatch,
869                                                                    mServerSession);
870                         mServerTransfer.start();
871                         if (nextBatch.getPendingShare().mConfirm ==
872                                 BluetoothShare.USER_CONFIRMATION_CONFIRMED) {
873                             mServerTransfer.setConfirmed();
874                         }
875                         return;
876                     }
877                 }
878             }
879         }
880     }
881 
needAction(int arrayPos)882     private boolean needAction(int arrayPos) {
883         BluetoothOppShareInfo info = mShares.get(arrayPos);
884         if (BluetoothShare.isStatusCompleted(info.mStatus)) {
885             return false;
886         }
887         return true;
888     }
889 
visibleNotification(int arrayPos)890     private boolean visibleNotification(int arrayPos) {
891         BluetoothOppShareInfo info = mShares.get(arrayPos);
892         return info.hasCompletionNotification();
893     }
894 
scanFile(Cursor cursor, int arrayPos)895     private boolean scanFile(Cursor cursor, int arrayPos) {
896         BluetoothOppShareInfo info = mShares.get(arrayPos);
897         synchronized (BluetoothOppService.this) {
898             if (D) Log.d(TAG, "Scanning file " + info.mFilename);
899             if (!mMediaScanInProgress) {
900                 mMediaScanInProgress = true;
901                 new MediaScannerNotifier(this, info, mHandler);
902                 return true;
903             } else {
904                 return false;
905             }
906         }
907     }
908 
shouldScanFile(int arrayPos)909     private boolean shouldScanFile(int arrayPos) {
910         BluetoothOppShareInfo info = mShares.get(arrayPos);
911         return BluetoothShare.isStatusSuccess(info.mStatus)
912                 && info.mDirection == BluetoothShare.DIRECTION_INBOUND && !info.mMediaScanned &&
913                 info.mConfirm != BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED;
914     }
915 
916     // Run in a background thread at boot.
trimDatabase(ContentResolver contentResolver)917     private static void trimDatabase(ContentResolver contentResolver) {
918         final String INVISIBLE = BluetoothShare.VISIBILITY + "=" +
919                 BluetoothShare.VISIBILITY_HIDDEN;
920 
921         // remove the invisible/complete/outbound shares
922         final String WHERE_INVISIBLE_COMPLETE_OUTBOUND = BluetoothShare.DIRECTION + "="
923                 + BluetoothShare.DIRECTION_OUTBOUND + " AND " + BluetoothShare.STATUS + ">="
924                 + BluetoothShare.STATUS_SUCCESS + " AND " + INVISIBLE;
925         int delNum = contentResolver.delete(BluetoothShare.CONTENT_URI,
926                 WHERE_INVISIBLE_COMPLETE_OUTBOUND, null);
927         if (V) Log.v(TAG, "Deleted complete outbound shares, number =  " + delNum);
928 
929         // remove the invisible/finished/inbound/failed shares
930         final String WHERE_INVISIBLE_COMPLETE_INBOUND_FAILED = BluetoothShare.DIRECTION + "="
931                 + BluetoothShare.DIRECTION_INBOUND + " AND " + BluetoothShare.STATUS + ">"
932                 + BluetoothShare.STATUS_SUCCESS + " AND " + INVISIBLE;
933         delNum = contentResolver.delete(BluetoothShare.CONTENT_URI,
934                 WHERE_INVISIBLE_COMPLETE_INBOUND_FAILED, null);
935         if (V) Log.v(TAG, "Deleted complete inbound failed shares, number = " + delNum);
936 
937         // Only keep the inbound and successful shares for LiverFolder use
938         // Keep the latest 1000 to easy db query
939         final String WHERE_INBOUND_SUCCESS = BluetoothShare.DIRECTION + "="
940                 + BluetoothShare.DIRECTION_INBOUND + " AND " + BluetoothShare.STATUS + "="
941                 + BluetoothShare.STATUS_SUCCESS + " AND " + INVISIBLE;
942         Cursor cursor = contentResolver.query(BluetoothShare.CONTENT_URI, new String[] {
943             BluetoothShare._ID
944         }, WHERE_INBOUND_SUCCESS, null, BluetoothShare._ID); // sort by id
945 
946         if (cursor == null) {
947             return;
948         }
949 
950         int recordNum = cursor.getCount();
951         if (recordNum > Constants.MAX_RECORDS_IN_DATABASE) {
952             int numToDelete = recordNum - Constants.MAX_RECORDS_IN_DATABASE;
953 
954             if (cursor.moveToPosition(numToDelete)) {
955                 int columnId = cursor.getColumnIndexOrThrow(BluetoothShare._ID);
956                 long id = cursor.getLong(columnId);
957                 delNum = contentResolver.delete(BluetoothShare.CONTENT_URI,
958                         BluetoothShare._ID + " < " + id, null);
959                 if (V) Log.v(TAG, "Deleted old inbound success share: " + delNum);
960             }
961         }
962         cursor.close();
963     }
964 
965     private static class MediaScannerNotifier implements MediaScannerConnectionClient {
966 
967         private MediaScannerConnection mConnection;
968 
969         private BluetoothOppShareInfo mInfo;
970 
971         private Context mContext;
972 
973         private Handler mCallback;
974 
MediaScannerNotifier(Context context, BluetoothOppShareInfo info, Handler handler)975         public MediaScannerNotifier(Context context, BluetoothOppShareInfo info, Handler handler) {
976             mContext = context;
977             mInfo = info;
978             mCallback = handler;
979             mConnection = new MediaScannerConnection(mContext, this);
980             if (V) Log.v(TAG, "Connecting to MediaScannerConnection ");
981             mConnection.connect();
982         }
983 
onMediaScannerConnected()984         public void onMediaScannerConnected() {
985             if (V) Log.v(TAG, "MediaScannerConnection onMediaScannerConnected");
986             mConnection.scanFile(mInfo.mFilename, mInfo.mMimetype);
987         }
988 
onScanCompleted(String path, Uri uri)989         public void onScanCompleted(String path, Uri uri) {
990             try {
991                 if (V) {
992                     Log.v(TAG, "MediaScannerConnection onScanCompleted");
993                     Log.v(TAG, "MediaScannerConnection path is " + path);
994                     Log.v(TAG, "MediaScannerConnection Uri is " + uri);
995                 }
996                 if (uri != null) {
997                     Message msg = Message.obtain();
998                     msg.setTarget(mCallback);
999                     msg.what = MEDIA_SCANNED;
1000                     msg.arg1 = mInfo.mId;
1001                     msg.obj = uri;
1002                     msg.sendToTarget();
1003                 } else {
1004                     Message msg = Message.obtain();
1005                     msg.setTarget(mCallback);
1006                     msg.what = MEDIA_SCANNED_FAILED;
1007                     msg.arg1 = mInfo.mId;
1008                     msg.sendToTarget();
1009                 }
1010             } catch (Exception ex) {
1011                 Log.v(TAG, "!!!MediaScannerConnection exception: " + ex);
1012             } finally {
1013                 if (V) Log.v(TAG, "MediaScannerConnection disconnect");
1014                 mConnection.disconnect();
1015             }
1016         }
1017     }
1018 }
1019