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