• 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 static java.util.Objects.requireNonNull;
36 
37 import android.annotation.SuppressLint;
38 import android.app.NotificationManager;
39 import android.bluetooth.BluetoothAdapter;
40 import android.bluetooth.BluetoothDevice;
41 import android.bluetooth.BluetoothProfile;
42 import android.bluetooth.BluetoothProtoEnums;
43 import android.bluetooth.BluetoothSocket;
44 import android.bluetooth.BluetoothUtils;
45 import android.bluetooth.BluetoothUuid;
46 import android.bluetooth.SdpOppOpsRecord;
47 import android.content.BroadcastReceiver;
48 import android.content.ContentValues;
49 import android.content.Context;
50 import android.content.Intent;
51 import android.content.IntentFilter;
52 import android.net.Uri;
53 import android.os.Handler;
54 import android.os.HandlerThread;
55 import android.os.Looper;
56 import android.os.Message;
57 import android.os.ParcelUuid;
58 import android.os.Process;
59 import android.util.Log;
60 
61 import com.android.bluetooth.BluetoothMethodProxy;
62 import com.android.bluetooth.BluetoothObexTransport;
63 import com.android.bluetooth.BluetoothStatsLog;
64 import com.android.bluetooth.Utils;
65 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils;
66 import com.android.internal.annotations.VisibleForTesting;
67 import com.android.obex.ObexTransport;
68 
69 import java.io.IOException;
70 
71 /** This class run an actual Opp transfer session (from connect target device to disconnect) */
72 // Next tag value for ContentProfileErrorReportUtils.report(): 24
73 @SuppressLint("AndroidFrameworkRequiresPermission")
74 public class BluetoothOppTransfer implements BluetoothOppBatch.BluetoothOppBatchListener {
75     private static final String TAG = BluetoothOppTransfer.class.getSimpleName();
76 
77     @VisibleForTesting static final int TRANSPORT_ERROR = 10;
78     @VisibleForTesting static final int TRANSPORT_CONNECTED = 11;
79     @VisibleForTesting static final int SOCKET_ERROR_RETRY = 13;
80 
81     private static final String SOCKET_LINK_KEY_ERROR = "Invalid exchange";
82 
83     private static final Object INSTANCE_LOCK = new Object();
84 
85     private final Context mContext;
86     private final BluetoothAdapter mAdapter;
87     private final BluetoothOppBatch mBatch;
88 
89     @VisibleForTesting BluetoothDevice mDevice;
90     private BluetoothOppObexSession mSession;
91     @VisibleForTesting BluetoothOppShareInfo mCurrentShare;
92     private ObexTransport mTransport;
93     private HandlerThread mHandlerThread;
94     @VisibleForTesting EventHandler mSessionHandler;
95     private long mTimestamp;
96 
97     @VisibleForTesting
98     class OppConnectionReceiver extends BroadcastReceiver {
99         @Override
onReceive(Context context, Intent intent)100         public void onReceive(Context context, Intent intent) {
101             String action = intent.getAction();
102             Log.d(TAG, " Action :" + action);
103             if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
104                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
105                 if (device == null) {
106                     Log.e(TAG, "Device is null");
107                     ContentProfileErrorReportUtils.report(
108                             BluetoothProfile.OPP,
109                             BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
110                             BluetoothStatsLog
111                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
112                             0);
113                     return;
114                 } else if (mCurrentShare == null) {
115                     Log.e(
116                             TAG,
117                             "device : "
118                                     + BluetoothUtils.toAnonymizedAddress(
119                                             Utils.getBrEdrAddress(device))
120                                     + " mBatch :"
121                                     + mBatch
122                                     + " mCurrentShare :"
123                                     + mCurrentShare);
124                     ContentProfileErrorReportUtils.report(
125                             BluetoothProfile.OPP,
126                             BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
127                             BluetoothStatsLog
128                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
129                             1);
130                     return;
131                 }
132                 try {
133                     Log.v(
134                             TAG,
135                             "Device :"
136                                     + BluetoothUtils.toAnonymizedAddress(
137                                             Utils.getBrEdrAddress(device))
138                                     + "- OPP device: "
139                                     + mBatch.mDestination
140                                     + " \n mCurrentShare.mConfirm == "
141                                     + mCurrentShare.mConfirm);
142                     if ((device.equals(mBatch.mDestination))
143                             && (mCurrentShare.mConfirm
144                                     == BluetoothShare.USER_CONFIRMATION_PENDING)) {
145                         Log.v(
146                                 TAG,
147                                 "ACTION_ACL_DISCONNECTED to be processed for batch: " + mBatch.mId);
148                         // Remove the timeout message triggered earlier during Obex Put
149                         mSessionHandler.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
150                         // Now reuse the same message to clean up the session.
151                         BluetoothMethodProxy.getInstance()
152                                 .handlerSendEmptyMessage(
153                                         mSessionHandler,
154                                         BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
155                     }
156                 } catch (Exception e) {
157                     ContentProfileErrorReportUtils.report(
158                             BluetoothProfile.OPP,
159                             BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
160                             BluetoothStatsLog
161                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
162                             2);
163                     Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
164                 }
165             } else if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)) {
166                 ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID);
167                 Log.d(TAG, "Received UUID: " + uuid.toString());
168                 Log.d(TAG, "expected UUID: " + BluetoothUuid.OBEX_OBJECT_PUSH.toString());
169                 if (uuid.equals(BluetoothUuid.OBEX_OBJECT_PUSH)) {
170                     int status = intent.getIntExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, -1);
171                     Log.d(TAG, " -> status: " + status);
172                     BluetoothDevice device =
173                             intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
174                     if (mDevice == null) {
175                         Log.w(TAG, "OPP SDP search, target device is null, ignoring result");
176                         ContentProfileErrorReportUtils.report(
177                                 BluetoothProfile.OPP,
178                                 BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
179                                 BluetoothStatsLog
180                                         .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
181                                 3);
182                         return;
183                     }
184                     String deviceIdentityAddress = Utils.getBrEdrAddress(device);
185                     String transferDeviceIdentityAddress = Utils.getBrEdrAddress(mDevice);
186                     if (deviceIdentityAddress == null
187                             || transferDeviceIdentityAddress == null
188                             || !deviceIdentityAddress.equalsIgnoreCase(
189                                     transferDeviceIdentityAddress)) {
190                         Log.w(TAG, " OPP SDP search for wrong device, ignoring!!");
191                         ContentProfileErrorReportUtils.report(
192                                 BluetoothProfile.OPP,
193                                 BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
194                                 BluetoothStatsLog
195                                         .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
196                                 4);
197                         return;
198                     }
199                     SdpOppOpsRecord record =
200                             intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD);
201                     if (record == null) {
202                         Log.w(TAG, " Invalid SDP , ignoring !!");
203                         ContentProfileErrorReportUtils.report(
204                                 BluetoothProfile.OPP,
205                                 BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
206                                 BluetoothStatsLog
207                                         .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
208                                 5);
209                         markConnectionFailed(null);
210                         return;
211                     }
212                     synchronized (INSTANCE_LOCK) {
213                         mConnectThread =
214                                 new SocketConnectThread(mDevice, false, true, record.getL2capPsm());
215                     }
216                     mConnectThread.start();
217                     mDevice = null;
218                 }
219             }
220         }
221     }
222 
223     private OppConnectionReceiver mBluetoothReceiver;
224 
BluetoothOppTransfer( Context context, BluetoothOppBatch batch, BluetoothOppObexSession session)225     public BluetoothOppTransfer(
226             Context context, BluetoothOppBatch batch, BluetoothOppObexSession session) {
227 
228         mContext = context;
229         mBatch = requireNonNull(batch);
230         mSession = session;
231 
232         mBatch.registerListener(this);
233         mAdapter = BluetoothAdapter.getDefaultAdapter();
234     }
235 
BluetoothOppTransfer(Context context, BluetoothOppBatch batch)236     public BluetoothOppTransfer(Context context, BluetoothOppBatch batch) {
237         this(context, batch, null);
238     }
239 
getBatchId()240     public int getBatchId() {
241         return mBatch.mId;
242     }
243 
244     /*
245      * Receives events from mConnectThread & mSession back in the main thread.
246      */
247     @VisibleForTesting
248     class EventHandler extends Handler {
EventHandler(Looper looper)249         EventHandler(Looper looper) {
250             super(looper);
251         }
252 
253         @Override
handleMessage(Message msg)254         public void handleMessage(Message msg) {
255             switch (msg.what) {
256                 case SOCKET_ERROR_RETRY:
257                     BluetoothDevice device = (BluetoothDevice) msg.obj;
258                     synchronized (INSTANCE_LOCK) {
259                         mConnectThread = new SocketConnectThread(device, true);
260                         mConnectThread.start();
261                     }
262                     break;
263                 case TRANSPORT_ERROR:
264                     /*
265                      * RFCOMM connect fail is for outbound share only! Mark batch
266                      * failed, and all shares in batch failed
267                      */
268                     Log.v(TAG, "receive TRANSPORT_ERROR msg");
269                     synchronized (INSTANCE_LOCK) {
270                         mConnectThread = null;
271                     }
272                     markBatchFailed(BluetoothShare.STATUS_CONNECTION_ERROR);
273                     mBatch.mStatus = Constants.BATCH_STATUS_FAILED;
274 
275                     break;
276                 case TRANSPORT_CONNECTED:
277                     /*
278                      * RFCOMM connected is for outbound share only! Create
279                      * BluetoothOppObexClientSession and start it
280                      */
281                     Log.v(TAG, "Transfer receive TRANSPORT_CONNECTED msg");
282                     synchronized (INSTANCE_LOCK) {
283                         mConnectThread = null;
284                     }
285                     mTransport = (ObexTransport) msg.obj;
286                     startObexSession();
287 
288                     break;
289                 case BluetoothOppObexSession.MSG_SHARE_COMPLETE:
290                     /*
291                      * Put next share if available,or finish the transfer.
292                      * For outbound session, call session.addShare() to send next file,
293                      * or call session.stop().
294                      * For inbounds session, do nothing. If there is next file to receive,it
295                      * will be notified through onShareAdded()
296                      */
297                     BluetoothOppShareInfo info = (BluetoothOppShareInfo) msg.obj;
298                     Log.v(TAG, "receive MSG_SHARE_COMPLETE for info=" + info);
299                     if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
300                         mCurrentShare = mBatch.getPendingShare();
301 
302                         if (mCurrentShare != null) {
303                             /* we have additional share to process */
304                             Log.v(
305                                     TAG,
306                                     "continue session for info "
307                                             + mCurrentShare.mId
308                                             + " from batch "
309                                             + mBatch.mId);
310                             processCurrentShare();
311                         } else {
312                             /* for outbound transfer, all shares are processed */
313                             Log.v(TAG, "Batch " + mBatch.mId + " is done");
314                             mSession.stop();
315                         }
316                     }
317                     break;
318                 case BluetoothOppObexSession.MSG_SESSION_COMPLETE:
319                     /*
320                      * Handle session completed status Set batch status to
321                      * finished
322                      */
323                     cleanUp();
324                     BluetoothOppShareInfo info1 = (BluetoothOppShareInfo) msg.obj;
325                     Log.v(TAG, "receive MSG_SESSION_COMPLETE for batch " + mBatch.mId);
326                     mBatch.mStatus = Constants.BATCH_STATUS_FINISHED;
327                     /*
328                      * trigger content provider again to know batch status change
329                      */
330                     tickShareStatus(info1);
331                     break;
332 
333                 case BluetoothOppObexSession.MSG_SESSION_ERROR:
334                     /* Handle the error state of an Obex session */
335                     Log.v(TAG, "receive MSG_SESSION_ERROR for batch " + mBatch.mId);
336                     cleanUp();
337                     try {
338                         BluetoothOppShareInfo info2 = (BluetoothOppShareInfo) msg.obj;
339                         if (mSession != null) {
340                             mSession.stop();
341                         }
342                         mBatch.mStatus = Constants.BATCH_STATUS_FAILED;
343                         markBatchFailed(info2.mStatus);
344                         tickShareStatus(mCurrentShare);
345                     } catch (Exception e) {
346                         ContentProfileErrorReportUtils.report(
347                                 BluetoothProfile.OPP,
348                                 BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
349                                 BluetoothStatsLog
350                                         .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
351                                 6);
352                         Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
353                     }
354                     break;
355 
356                 case BluetoothOppObexSession.MSG_SHARE_INTERRUPTED:
357                     Log.v(TAG, "receive MSG_SHARE_INTERRUPTED for batch " + mBatch.mId);
358                     BluetoothOppShareInfo info3 = (BluetoothOppShareInfo) msg.obj;
359                     if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
360                         try {
361                             if (mTransport == null) {
362                                 Log.v(TAG, "receive MSG_SHARE_INTERRUPTED but mTransport = null");
363                             } else {
364                                 mTransport.close();
365                             }
366                         } catch (IOException e) {
367                             ContentProfileErrorReportUtils.report(
368                                     BluetoothProfile.OPP,
369                                     BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
370                                     BluetoothStatsLog
371                                             .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
372                                     7);
373                             Log.e(TAG, "failed to close mTransport");
374                         }
375                         Log.v(TAG, "mTransport closed ");
376                         mBatch.mStatus = Constants.BATCH_STATUS_FAILED;
377                         if (info3 != null) {
378                             markBatchFailed(info3.mStatus);
379                         } else {
380                             markBatchFailed(BluetoothShare.STATUS_UNKNOWN_ERROR);
381                         }
382                         tickShareStatus(mCurrentShare);
383                     }
384                     break;
385 
386                 case BluetoothOppObexSession.MSG_CONNECT_TIMEOUT:
387                     Log.v(TAG, "receive MSG_CONNECT_TIMEOUT for batch " + mBatch.mId);
388                     /* for outbound transfer, the block point is BluetoothSocket.write()
389                      * The only way to unblock is to tear down lower transport
390                      * */
391                     if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
392                         try {
393                             if (mTransport == null) {
394                                 Log.v(TAG, "receive MSG_SHARE_INTERRUPTED but mTransport = null");
395                             } else {
396                                 mTransport.close();
397                             }
398                         } catch (IOException e) {
399                             ContentProfileErrorReportUtils.report(
400                                     BluetoothProfile.OPP,
401                                     BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
402                                     BluetoothStatsLog
403                                             .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
404                                     8);
405                             Log.e(TAG, "failed to close mTransport");
406                         }
407                         Log.v(TAG, "mTransport closed ");
408                     } else {
409                         /*
410                          * For inbound transfer, the block point is waiting for
411                          * user confirmation we can interrupt it nicely
412                          */
413 
414                         // Remove incoming file confirm notification
415                         NotificationManager nm =
416                                 mContext.getSystemService(NotificationManager.class);
417                         nm.cancel(mCurrentShare.mId);
418                         // Send intent to UI for timeout handling
419                         Intent in = new Intent(BluetoothShare.USER_CONFIRMATION_TIMEOUT_ACTION);
420                         mContext.sendBroadcast(in);
421 
422                         markShareTimeout(mCurrentShare);
423                     }
424                     break;
425             }
426         }
427     }
428 
markShareTimeout(BluetoothOppShareInfo share)429     private void markShareTimeout(BluetoothOppShareInfo share) {
430         Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + share.mId);
431         ContentValues updateValues = new ContentValues();
432         updateValues.put(
433                 BluetoothShare.USER_CONFIRMATION, BluetoothShare.USER_CONFIRMATION_TIMEOUT);
434         BluetoothMethodProxy.getInstance()
435                 .contentResolverUpdate(
436                         mContext.getContentResolver(), contentUri, updateValues, null, null);
437     }
438 
439     @SuppressLint("WaitNotInLoop")
markBatchFailed(int failReason)440     private void markBatchFailed(int failReason) {
441         synchronized (this) {
442             try {
443                 wait(1000);
444             } catch (InterruptedException e) {
445                 ContentProfileErrorReportUtils.report(
446                         BluetoothProfile.OPP,
447                         BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
448                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
449                         9);
450                 Log.v(TAG, "Interrupted waiting for markBatchFailed");
451             }
452         }
453 
454         Log.d(TAG, "Mark all ShareInfo in the batch as failed");
455         if (mCurrentShare != null) {
456             Log.v(TAG, "Current share has status " + mCurrentShare.mStatus);
457             if (BluetoothShare.isStatusError(mCurrentShare.mStatus)) {
458                 failReason = mCurrentShare.mStatus;
459             }
460             if (mCurrentShare.mDirection == BluetoothShare.DIRECTION_INBOUND
461                     && mCurrentShare.mUri != null) {
462                 BluetoothMethodProxy.getInstance()
463                         .contentResolverDelete(
464                                 mContext.getContentResolver(), mCurrentShare.mUri, null, null);
465             }
466         }
467 
468         if (mBatch == null) {
469             return;
470         }
471         BluetoothOppShareInfo info = mBatch.getPendingShare();
472         while (info != null) {
473             if (info.mStatus < 200) {
474                 info.mStatus = failReason;
475                 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + info.mId);
476                 ContentValues updateValues = new ContentValues();
477                 updateValues.put(BluetoothShare.STATUS, info.mStatus);
478                 /* Update un-processed outbound transfer to show some info */
479                 if (info.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
480                     BluetoothOppSendFileInfo fileInfo =
481                             BluetoothOppUtility.getSendFileInfo(info.mUri);
482                     BluetoothOppUtility.closeSendFileInfo(info.mUri);
483                     if (fileInfo.mFileName != null) {
484                         updateValues.put(BluetoothShare.FILENAME_HINT, fileInfo.mFileName);
485                         updateValues.put(BluetoothShare.TOTAL_BYTES, fileInfo.mLength);
486                         updateValues.put(BluetoothShare.MIMETYPE, fileInfo.mMimetype);
487                     }
488                 } else {
489                     if (info.mStatus < 200 && info.mUri != null) {
490                         BluetoothMethodProxy.getInstance()
491                                 .contentResolverDelete(
492                                         mContext.getContentResolver(), info.mUri, null, null);
493                     }
494                 }
495                 BluetoothMethodProxy.getInstance()
496                         .contentResolverUpdate(
497                                 mContext.getContentResolver(),
498                                 contentUri,
499                                 updateValues,
500                                 null,
501                                 null);
502                 Constants.sendIntentIfCompleted(mContext, contentUri, info.mStatus);
503             }
504             info = mBatch.getPendingShare();
505         }
506     }
507 
508     /*
509      * NOTE
510      * For outbound transfer
511      * 1) Check Bluetooth status
512      * 2) Start handler thread
513      * 3) new a thread to connect to target device
514      * 3.1) Try a few times to do SDP query for target device OPUSH channel
515      * 3.2) Try a few seconds to connect to target socket
516      * 4) After BluetoothSocket is connected,create an instance of RfcommTransport
517      * 5) Create an instance of BluetoothOppClientSession
518      * 6) Start the session and process the first share in batch
519      * For inbound transfer
520      * The transfer already has session and transport setup, just start it
521      * 1) Check Bluetooth status
522      * 2) Start handler thread
523      * 3) Start the session and process the first share in batch
524      */
525 
526     /** Start the transfer */
start()527     public void start() {
528         /* check Bluetooth enable status */
529         /*
530          * normally it's impossible to reach here if BT is disabled. Just check
531          * for safety
532          */
533         if (!BluetoothMethodProxy.getInstance().bluetoothAdapterIsEnabled(mAdapter)) {
534             Log.e(TAG, "Can't start transfer when Bluetooth is disabled for " + mBatch.mId);
535             ContentProfileErrorReportUtils.report(
536                     BluetoothProfile.OPP,
537                     BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
538                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
539                     10);
540             markBatchFailed(BluetoothShare.STATUS_UNKNOWN_ERROR);
541             mBatch.mStatus = Constants.BATCH_STATUS_FAILED;
542             return;
543         }
544         registerConnectionReceiver();
545         if (mHandlerThread == null) {
546             Log.v(TAG, "Create handler thread for batch " + mBatch.mId);
547             mHandlerThread =
548                     new HandlerThread("BtOpp Transfer Handler", Process.THREAD_PRIORITY_BACKGROUND);
549             mHandlerThread.start();
550             mSessionHandler = new EventHandler(mHandlerThread.getLooper());
551 
552             if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
553                 /* for outbound transfer, we do connect first */
554                 startConnectSession();
555             } else if (mBatch.mDirection == BluetoothShare.DIRECTION_INBOUND) {
556                 /*
557                  * for inbound transfer, it's already connected, so we start
558                  * OBEX session directly
559                  */
560                 startObexSession();
561             }
562         }
563     }
564 
565     /** Stop the transfer */
stop()566     public void stop() {
567         Log.v(TAG, "stop");
568         if (mSession != null) {
569             Log.v(TAG, "Stop mSession");
570             mSession.stop();
571         }
572 
573         cleanUp();
574         synchronized (INSTANCE_LOCK) {
575             if (mConnectThread != null) {
576                 try {
577                     mConnectThread.interrupt();
578                     Log.v(TAG, "waiting for connect thread to terminate");
579                     mConnectThread.join();
580                 } catch (InterruptedException e) {
581                     ContentProfileErrorReportUtils.report(
582                             BluetoothProfile.OPP,
583                             BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
584                             BluetoothStatsLog
585                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
586                             11);
587                     Log.v(TAG, "Interrupted waiting for connect thread to join");
588                 }
589                 mConnectThread = null;
590             }
591         }
592         // Prevent concurrent access
593         synchronized (this) {
594             if (mHandlerThread != null) {
595                 mHandlerThread.quit();
596                 mHandlerThread.interrupt();
597                 mHandlerThread = null;
598             }
599         }
600     }
601 
startObexSession()602     private void startObexSession() {
603 
604         mBatch.mStatus = Constants.BATCH_STATUS_RUNNING;
605 
606         mCurrentShare = mBatch.getPendingShare();
607         if (mCurrentShare == null) {
608             /*
609              * TODO catch this error
610              */
611             Log.e(TAG, "Unexpected error happened !");
612             ContentProfileErrorReportUtils.report(
613                     BluetoothProfile.OPP,
614                     BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
615                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
616                     12);
617             return;
618         }
619         Log.v(TAG, "Start session for info " + mCurrentShare.mId + " for batch " + mBatch.mId);
620 
621         if (mBatch.mDirection == BluetoothShare.DIRECTION_OUTBOUND) {
622             Log.v(TAG, "Create Client session with transport " + mTransport.toString());
623             mSession = new BluetoothOppObexClientSession(mContext, mTransport);
624         } else if (mBatch.mDirection == BluetoothShare.DIRECTION_INBOUND) {
625             /*
626              * For inbounds transfer, a server session should already exists
627              * before BluetoothOppTransfer is initialized. We should pass in a
628              * mSession instance.
629              */
630             if (mSession == null) {
631                 /* set current share as error */
632                 Log.e(TAG, "Unexpected error happened !");
633                 ContentProfileErrorReportUtils.report(
634                         BluetoothProfile.OPP,
635                         BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
636                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
637                         13);
638                 markBatchFailed(BluetoothShare.STATUS_UNKNOWN_ERROR);
639                 mBatch.mStatus = Constants.BATCH_STATUS_FAILED;
640                 return;
641             }
642             Log.v(TAG, "Transfer has Server session" + mSession.toString());
643         }
644 
645         mSession.start(mSessionHandler, mBatch.getNumShares());
646         processCurrentShare();
647     }
648 
registerConnectionReceiver()649     private void registerConnectionReceiver() {
650         /*
651          * OBEX channel need to be monitored for unexpected ACL disconnection
652          * such as Remote Battery removal
653          */
654         synchronized (this) {
655             try {
656                 if (mBluetoothReceiver == null) {
657                     mBluetoothReceiver = new OppConnectionReceiver();
658                     IntentFilter filter = new IntentFilter();
659                     filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
660                     filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
661                     filter.addAction(BluetoothDevice.ACTION_SDP_RECORD);
662                     mContext.registerReceiver(mBluetoothReceiver, filter);
663                     Log.v(TAG, "Registered mBluetoothReceiver");
664                 }
665             } catch (IllegalArgumentException e) {
666                 ContentProfileErrorReportUtils.report(
667                         BluetoothProfile.OPP,
668                         BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
669                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
670                         14);
671                 Log.e(TAG, "mBluetoothReceiver Registered already ", e);
672             }
673         }
674     }
675 
processCurrentShare()676     private void processCurrentShare() {
677         /* This transfer need user confirm */
678         Log.v(TAG, "processCurrentShare" + mCurrentShare.mId);
679         mSession.addShare(mCurrentShare);
680         if (mCurrentShare.mConfirm == BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED) {
681             confirmStatusChanged();
682         }
683     }
684 
685     /** Set transfer confirmed status. It should only be called for inbound transfer */
686     @SuppressWarnings("SynchronizeOnNonFinalField")
confirmStatusChanged()687     public void confirmStatusChanged() {
688         /* unblock server session */
689         final Thread notifyThread =
690                 new Thread("Server Unblock thread") {
691                     @Override
692                     public void run() {
693                         synchronized (mSession) {
694                             mSession.unblock();
695                             mSession.notify();
696                         }
697                     }
698                 };
699         Log.v(TAG, "confirmStatusChanged to unblock mSession" + mSession.toString());
700         notifyThread.start();
701     }
702 
startConnectSession()703     private void startConnectSession() {
704         mDevice = mBatch.mDestination;
705         if (!mBatch.mDestination.sdpSearch(BluetoothUuid.OBEX_OBJECT_PUSH)) {
706             Log.d(TAG, "SDP failed, start rfcomm connect directly");
707             /* update bd address as sdp could not be started */
708             mDevice = null;
709             /* SDP failed, start rfcomm connect directly */
710             mConnectThread = new SocketConnectThread(mBatch.mDestination, false, false, -1);
711             mConnectThread.start();
712         }
713     }
714 
715     @VisibleForTesting SocketConnectThread mConnectThread;
716 
717     @VisibleForTesting
718     class SocketConnectThread extends Thread {
719         @VisibleForTesting final BluetoothDevice mDevice;
720 
721         private int mL2cChannel = 0;
722 
723         private BluetoothSocket mBtSocket = null;
724 
725         private boolean mRetry = false;
726 
727         private boolean mSdpInitiated = false;
728 
729         @VisibleForTesting boolean mIsInterrupted = false;
730 
731         /* create a Rfcomm/L2CAP Socket */
SocketConnectThread(BluetoothDevice device, boolean retry)732         SocketConnectThread(BluetoothDevice device, boolean retry) {
733             super("Socket Connect Thread");
734             this.mDevice = device;
735             mRetry = retry;
736             mSdpInitiated = false;
737         }
738 
739         /* create a Rfcomm/L2CAP Socket */
SocketConnectThread( BluetoothDevice device, boolean retry, boolean sdpInitiated, int l2capChannel)740         SocketConnectThread(
741                 BluetoothDevice device, boolean retry, boolean sdpInitiated, int l2capChannel) {
742             super("Socket Connect Thread");
743             this.mDevice = device;
744             mRetry = retry;
745             mSdpInitiated = sdpInitiated;
746             mL2cChannel = l2capChannel;
747         }
748 
749         @Override
interrupt()750         public void interrupt() {
751             Log.d(TAG, "start interrupt :" + mBtSocket);
752             mIsInterrupted = true;
753             if (mBtSocket != null) {
754                 try {
755                     mBtSocket.close();
756                 } catch (IOException e) {
757                     ContentProfileErrorReportUtils.report(
758                             BluetoothProfile.OPP,
759                             BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
760                             BluetoothStatsLog
761                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
762                             15);
763                     Log.v(TAG, "Error when close socket");
764                 }
765             }
766         }
767 
connectRfcommSocket()768         private void connectRfcommSocket() {
769             Log.v(TAG, "connectRfcommSocket");
770             try {
771                 if (mIsInterrupted) {
772                     Log.d(TAG, "connectRfcommSocket interrupted");
773                     markConnectionFailed(mBtSocket);
774                     return;
775                 }
776                 mBtSocket =
777                         mDevice.createInsecureRfcommSocketToServiceRecord(
778                                 BluetoothUuid.OBEX_OBJECT_PUSH.getUuid());
779             } catch (IOException e1) {
780                 ContentProfileErrorReportUtils.report(
781                         BluetoothProfile.OPP,
782                         BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
783                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
784                         16);
785                 Log.e(TAG, "Rfcomm socket create error", e1);
786                 markConnectionFailed(mBtSocket);
787                 return;
788             }
789             try {
790                 mBtSocket.connect();
791 
792                 Log.v(
793                         TAG,
794                         "Rfcomm socket connection attempt took "
795                                 + (System.currentTimeMillis() - mTimestamp)
796                                 + " ms");
797                 BluetoothObexTransport transport;
798                 transport = new BluetoothObexTransport(mBtSocket);
799 
800                 BluetoothOppPreference.getInstance(mContext)
801                         .setName(mDevice, Utils.getName(mDevice));
802 
803                 Log.v(TAG, "Send transport message " + transport.toString());
804 
805                 mSessionHandler.obtainMessage(TRANSPORT_CONNECTED, transport).sendToTarget();
806             } catch (IOException e) {
807                 ContentProfileErrorReportUtils.report(
808                         BluetoothProfile.OPP,
809                         BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
810                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
811                         17);
812                 Log.e(TAG, "Rfcomm socket connect exception", e);
813                 // If the devices were paired before, but unpaired on the
814                 // remote end, it will return an error for the auth request
815                 // for the socket connection. Link keys will get exchanged
816                 // again, but we need to retry. There is no good way to
817                 // inform this socket asking it to retry apart from a blind
818                 // delayed retry.
819                 if (!mRetry && e.getMessage().equals(SOCKET_LINK_KEY_ERROR)) {
820                     Message msg =
821                             mSessionHandler.obtainMessage(SOCKET_ERROR_RETRY, -1, -1, mDevice);
822                     mSessionHandler.sendMessageDelayed(msg, 1500);
823                 } else {
824                     markConnectionFailed(mBtSocket);
825                 }
826             }
827         }
828 
829         @Override
run()830         public void run() {
831             mTimestamp = System.currentTimeMillis();
832             Log.d(TAG, "sdp initiated = " + mSdpInitiated + " l2cChannel :" + mL2cChannel);
833             // check if sdp initiated successfully for l2cap or not. If not
834             // connect
835             // directly to rfcomm
836             if (!mSdpInitiated || mL2cChannel < 0) {
837                 /* sdp failed for some reason, connect on rfcomm */
838                 Log.d(TAG, "sdp not initiated, connecting on rfcomm");
839                 connectRfcommSocket();
840                 return;
841             }
842 
843             /* Reset the flag */
844             mSdpInitiated = false;
845 
846             /* Use BluetoothSocket to connect */
847             try {
848                 if (mIsInterrupted) {
849                     Log.e(TAG, "btSocket connect interrupted ");
850                     ContentProfileErrorReportUtils.report(
851                             BluetoothProfile.OPP,
852                             BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
853                             BluetoothStatsLog
854                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
855                             18);
856                     markConnectionFailed(mBtSocket);
857                     return;
858                 } else {
859                     mBtSocket = mDevice.createInsecureL2capSocket(mL2cChannel);
860                 }
861             } catch (IOException e1) {
862                 ContentProfileErrorReportUtils.report(
863                         BluetoothProfile.OPP,
864                         BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
865                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
866                         19);
867                 Log.e(TAG, "L2cap socket create error", e1);
868                 connectRfcommSocket();
869                 return;
870             }
871             try {
872                 mBtSocket.connect();
873                 Log.v(
874                         TAG,
875                         "L2cap socket connection attempt took "
876                                 + (System.currentTimeMillis() - mTimestamp)
877                                 + " ms");
878                 BluetoothObexTransport transport;
879                 transport = new BluetoothObexTransport(mBtSocket);
880                 BluetoothOppPreference.getInstance(mContext)
881                         .setName(mDevice, Utils.getName(mDevice));
882                 Log.v(TAG, "Send transport message " + transport.toString());
883                 mSessionHandler.obtainMessage(TRANSPORT_CONNECTED, transport).sendToTarget();
884             } catch (IOException e) {
885                 ContentProfileErrorReportUtils.report(
886                         BluetoothProfile.OPP,
887                         BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
888                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
889                         20);
890                 Log.e(TAG, "L2cap socket connect exception", e);
891                 try {
892                     mBtSocket.close();
893                 } catch (IOException e3) {
894                     ContentProfileErrorReportUtils.report(
895                             BluetoothProfile.OPP,
896                             BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
897                             BluetoothStatsLog
898                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
899                             21);
900                     Log.e(TAG, "Bluetooth socket close error ", e3);
901                 }
902                 connectRfcommSocket();
903                 return;
904             }
905         }
906     }
907 
markConnectionFailed(BluetoothSocket s)908     private void markConnectionFailed(BluetoothSocket s) {
909         Log.v(TAG, "markConnectionFailed " + s);
910         try {
911             if (s != null) {
912                 s.close();
913             }
914         } catch (IOException e) {
915             ContentProfileErrorReportUtils.report(
916                     BluetoothProfile.OPP,
917                     BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
918                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
919                     22);
920             Log.e(TAG, "Error when close socket");
921         }
922         BluetoothMethodProxy.getInstance()
923                 .handlerSendEmptyMessage(mSessionHandler, TRANSPORT_ERROR);
924     }
925 
926     /* update a trivial field of a share to notify Provider the batch status change */
tickShareStatus(BluetoothOppShareInfo share)927     private void tickShareStatus(BluetoothOppShareInfo share) {
928         if (share == null) {
929             Log.d(TAG, "Share is null");
930             return;
931         }
932         Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + share.mId);
933         ContentValues updateValues = new ContentValues();
934         updateValues.put(BluetoothShare.DIRECTION, share.mDirection);
935         BluetoothMethodProxy.getInstance()
936                 .contentResolverUpdate(
937                         mContext.getContentResolver(), contentUri, updateValues, null, null);
938     }
939 
940     /*
941      * Note: For outbound transfer We don't implement this method now. If later
942      * we want to support merging a later added share into an existing session,
943      * we could implement here For inbounds transfer add share means it's
944      * multiple receive in the same session, we should handle it to fill it into
945      * mSession
946      */
947 
948     /** Process when a share is added to current transfer */
949     @Override
onShareAdded(int id)950     public void onShareAdded(int id) {
951         BluetoothOppShareInfo info = mBatch.getPendingShare();
952         if (info.mDirection == BluetoothShare.DIRECTION_INBOUND) {
953             mCurrentShare = mBatch.getPendingShare();
954             /*
955              * TODO what if it's not auto confirmed?
956              */
957             if (mCurrentShare != null
958                     && (mCurrentShare.mConfirm == BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED
959                             || mCurrentShare.mConfirm
960                                     == BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED)) {
961                 /* have additional auto confirmed share to process */
962                 Log.v(
963                         TAG,
964                         "Transfer continue session for info "
965                                 + mCurrentShare.mId
966                                 + " from batch "
967                                 + mBatch.mId);
968                 processCurrentShare();
969                 confirmStatusChanged();
970             }
971         }
972     }
973 
974     /*
975      * NOTE We don't implement this method now. Now delete a single share from
976      * the batch means the whole batch should be canceled. If later we want to
977      * support single cancel, we could implement here For outbound transfer, if
978      * the share is currently in transfer, cancel it For inbounds transfer,
979      * delete share means the current receiving file should be canceled.
980      */
981 
982     /** Process when a share is deleted from current transfer */
983     @Override
onShareDeleted(int id)984     public void onShareDeleted(int id) {}
985 
986     /** Process when current transfer is canceled */
987     @Override
onBatchCanceled()988     public void onBatchCanceled() {
989         Log.v(TAG, "Transfer on Batch canceled");
990 
991         this.stop();
992         mBatch.mStatus = Constants.BATCH_STATUS_FINISHED;
993     }
994 
cleanUp()995     private void cleanUp() {
996         synchronized (this) {
997             try {
998                 if (mBluetoothReceiver != null) {
999                     mContext.unregisterReceiver(mBluetoothReceiver);
1000                     mBluetoothReceiver = null;
1001                 }
1002             } catch (Exception e) {
1003                 ContentProfileErrorReportUtils.report(
1004                         BluetoothProfile.OPP,
1005                         BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER,
1006                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
1007                         23);
1008                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
1009             }
1010         }
1011     }
1012 }
1013