• 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 java.io.BufferedOutputStream;
36 import java.io.File;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.util.Arrays;
40 
41 import android.app.NotificationManager;
42 import android.content.ContentValues;
43 import android.content.Context;
44 import android.content.Intent;
45 import android.net.Uri;
46 import android.os.Handler;
47 import android.os.Message;
48 import android.os.PowerManager;
49 import android.os.PowerManager.WakeLock;
50 import android.util.Log;
51 import android.webkit.MimeTypeMap;
52 
53 import javax.obex.HeaderSet;
54 import javax.obex.ObexTransport;
55 import javax.obex.Operation;
56 import javax.obex.ResponseCodes;
57 import javax.obex.ServerRequestHandler;
58 import javax.obex.ServerSession;
59 
60 import com.android.bluetooth.BluetoothObexTransport;
61 import com.android.bluetooth.ObexServerSockets;
62 
63 /**
64  * This class runs as an OBEX server
65  */
66 public class BluetoothOppObexServerSession extends ServerRequestHandler implements
67         BluetoothOppObexSession {
68 
69     private static final String TAG = "BtOppObexServer";
70     private static final boolean D = Constants.DEBUG;
71     private static final boolean V = Constants.VERBOSE;
72 
73     private ObexTransport mTransport;
74 
75     private Context mContext;
76 
77     private Handler mCallback = null;
78 
79     /* status when server is blocking for user/auto confirmation */
80     private boolean mServerBlocking = true;
81 
82     /* the current transfer info */
83     private BluetoothOppShareInfo mInfo;
84 
85     /* info id when we insert the record */
86     private int mLocalShareInfoId;
87 
88     private int mAccepted = BluetoothShare.USER_CONFIRMATION_PENDING;
89 
90     private boolean mInterrupted = false;
91 
92     private ServerSession mSession;
93 
94     private long mTimestamp;
95 
96     private BluetoothOppReceiveFileInfo mFileInfo;
97 
98     private WakeLock mPartialWakeLock;
99 
100     boolean mTimeoutMsgSent = false;
101 
102     private ObexServerSockets mServerSocket;
103 
BluetoothOppObexServerSession( Context context, ObexTransport transport, ObexServerSockets serverSocket)104     public BluetoothOppObexServerSession(
105             Context context, ObexTransport transport, ObexServerSockets serverSocket) {
106         mContext = context;
107         mTransport = transport;
108         mServerSocket = serverSocket;
109         PowerManager pm = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE);
110         mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
111         mPartialWakeLock.setReferenceCounted(false);
112     }
113 
unblock()114     public void unblock() {
115         mServerBlocking = false;
116     }
117 
118     /**
119      * Called when connection is accepted from remote, to retrieve the first
120      * Header then wait for user confirmation
121      */
preStart()122     public void preStart() {
123         try {
124             if (D) Log.d(TAG, "Create ServerSession with transport " + mTransport.toString());
125             mSession = new ServerSession(mTransport, this, null);
126         } catch (IOException e) {
127             Log.e(TAG, "Create server session error" + e);
128         }
129     }
130 
131     /**
132      * Called from BluetoothOppTransfer to start the "Transfer"
133      */
start(Handler handler, int numShares)134     public void start(Handler handler, int numShares) {
135         if (D) Log.d(TAG, "Start!");
136         mCallback = handler;
137 
138     }
139 
140     /**
141      * Called from BluetoothOppTransfer to cancel the "Transfer" Otherwise,
142      * server should end by itself.
143      */
stop()144     public void stop() {
145         /*
146          * TODO now we implement in a tough way, just close the socket.
147          * maybe need nice way
148          */
149         if (D) Log.d(TAG, "Stop!");
150         mInterrupted = true;
151         if (mSession != null) {
152             try {
153                 mSession.close();
154                 mTransport.close();
155             } catch (IOException e) {
156                 Log.e(TAG, "close mTransport error" + e);
157             }
158         }
159         mCallback = null;
160         mSession = null;
161     }
162 
addShare(BluetoothOppShareInfo info)163     public void addShare(BluetoothOppShareInfo info) {
164         if (D) Log.d(TAG, "addShare for id " + info.mId);
165         mInfo = info;
166         mFileInfo = processShareInfo();
167     }
168 
169     @Override
onPut(Operation op)170     public int onPut(Operation op) {
171         if (D) Log.d(TAG, "onPut " + op.toString());
172         HeaderSet request;
173         String name, mimeType;
174         Long length;
175 
176         int obexResponse = ResponseCodes.OBEX_HTTP_OK;
177 
178         /**
179          * For multiple objects, reject further objects after user deny the
180          * first one
181          */
182         if (mAccepted == BluetoothShare.USER_CONFIRMATION_DENIED) {
183             return ResponseCodes.OBEX_HTTP_FORBIDDEN;
184         }
185 
186         String destination;
187         if (mTransport instanceof BluetoothObexTransport) {
188             destination = ((BluetoothObexTransport)mTransport).getRemoteAddress();
189         } else {
190             destination = "FF:FF:FF:00:00:00";
191         }
192         boolean isWhitelisted = BluetoothOppManager.getInstance(mContext).
193                 isWhitelisted(destination);
194 
195         try {
196             boolean pre_reject = false;
197 
198             request = op.getReceivedHeader();
199             if (V) Constants.logHeader(request);
200             name = (String)request.getHeader(HeaderSet.NAME);
201             length = (Long)request.getHeader(HeaderSet.LENGTH);
202             mimeType = (String)request.getHeader(HeaderSet.TYPE);
203 
204             if (length == 0) {
205                 if (D) Log.w(TAG, "length is 0, reject the transfer");
206                 pre_reject = true;
207                 obexResponse = ResponseCodes.OBEX_HTTP_LENGTH_REQUIRED;
208             }
209 
210             if (name == null || name.equals("")) {
211                 if (D) Log.w(TAG, "name is null or empty, reject the transfer");
212                 pre_reject = true;
213                 obexResponse = ResponseCodes.OBEX_HTTP_BAD_REQUEST;
214             }
215 
216             if (!pre_reject) {
217                 /* first we look for Mimetype in Android map */
218                 String extension, type;
219                 int dotIndex = name.lastIndexOf(".");
220                 if (dotIndex < 0 && mimeType == null) {
221                     if (D) Log.w(TAG, "There is no file extension or mime type," +
222                             "reject the transfer");
223                     pre_reject = true;
224                     obexResponse = ResponseCodes.OBEX_HTTP_BAD_REQUEST;
225                 } else {
226                     extension = name.substring(dotIndex + 1).toLowerCase();
227                     MimeTypeMap map = MimeTypeMap.getSingleton();
228                     type = map.getMimeTypeFromExtension(extension);
229                     if (V) Log.v(TAG, "Mimetype guessed from extension " + extension + " is " + type);
230                     if (type != null) {
231                         mimeType = type;
232 
233                     } else {
234                         if (mimeType == null) {
235                             if (D) Log.w(TAG, "Can't get mimetype, reject the transfer");
236                             pre_reject = true;
237                             obexResponse = ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE;
238                         }
239                     }
240                     if (mimeType != null) {
241                         mimeType = mimeType.toLowerCase();
242                     }
243                 }
244             }
245 
246             // Reject policy: anything outside the "white list" plus unspecified
247             // MIME Types. Also reject everything in the "black list".
248             if (!pre_reject
249                     && (mimeType == null
250                             || (!isWhitelisted && !Constants.mimeTypeMatches(mimeType,
251                                     Constants.ACCEPTABLE_SHARE_INBOUND_TYPES))
252                             || Constants.mimeTypeMatches(mimeType,
253                                     Constants.UNACCEPTABLE_SHARE_INBOUND_TYPES))) {
254                 if (D) Log.w(TAG, "mimeType is null or in unacceptable list, reject the transfer");
255                 pre_reject = true;
256                 obexResponse = ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE;
257             }
258 
259             if (pre_reject && obexResponse != ResponseCodes.OBEX_HTTP_OK) {
260                 // some bad implemented client won't send disconnect
261                 return obexResponse;
262             }
263 
264         } catch (IOException e) {
265             Log.e(TAG, "get getReceivedHeaders error " + e);
266             return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
267         }
268 
269         ContentValues values = new ContentValues();
270 
271         values.put(BluetoothShare.FILENAME_HINT, name);
272 
273         values.put(BluetoothShare.TOTAL_BYTES, length);
274 
275         values.put(BluetoothShare.MIMETYPE, mimeType);
276 
277         values.put(BluetoothShare.DESTINATION, destination);
278 
279         values.put(BluetoothShare.DIRECTION, BluetoothShare.DIRECTION_INBOUND);
280         values.put(BluetoothShare.TIMESTAMP, mTimestamp);
281 
282         /** It's not first put if !serverBlocking, so we auto accept it */
283         if (!mServerBlocking && (mAccepted == BluetoothShare.USER_CONFIRMATION_CONFIRMED ||
284                 mAccepted == BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED)) {
285             values.put(BluetoothShare.USER_CONFIRMATION,
286                     BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED);
287         }
288 
289         if (isWhitelisted) {
290             values.put(BluetoothShare.USER_CONFIRMATION,
291                     BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED);
292 
293         }
294 
295         Uri contentUri = mContext.getContentResolver().insert(BluetoothShare.CONTENT_URI, values);
296         mLocalShareInfoId = Integer.parseInt(contentUri.getPathSegments().get(1));
297 
298         if (V) Log.v(TAG, "insert contentUri: " + contentUri);
299         if (V) Log.v(TAG, "mLocalShareInfoId = " + mLocalShareInfoId);
300 
301         synchronized (this) {
302             mPartialWakeLock.acquire();
303             mServerBlocking = true;
304             try {
305 
306                 while (mServerBlocking) {
307                     wait(1000);
308                     if (mCallback != null && !mTimeoutMsgSent) {
309                         mCallback.sendMessageDelayed(mCallback
310                                 .obtainMessage(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT),
311                                 BluetoothOppObexSession.SESSION_TIMEOUT);
312                         mTimeoutMsgSent = true;
313                         if (V) Log.v(TAG, "MSG_CONNECT_TIMEOUT sent");
314                     }
315                 }
316             } catch (InterruptedException e) {
317                 if (V) Log.v(TAG, "Interrupted in onPut blocking");
318             }
319         }
320         if (D) Log.d(TAG, "Server unblocked ");
321         synchronized (this) {
322             if (mCallback != null && mTimeoutMsgSent) {
323                 mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
324             }
325         }
326 
327         /* we should have mInfo now */
328 
329         /*
330          * TODO check if this mInfo match the one that we insert before server
331          * blocking? just to make sure no error happens
332          */
333         if (mInfo.mId != mLocalShareInfoId) {
334             Log.e(TAG, "Unexpected error!");
335         }
336         mAccepted = mInfo.mConfirm;
337 
338         if (V) Log.v(TAG, "after confirm: userAccepted=" + mAccepted);
339         int status = BluetoothShare.STATUS_SUCCESS;
340 
341         if (mAccepted == BluetoothShare.USER_CONFIRMATION_CONFIRMED
342                 || mAccepted == BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED
343                 || mAccepted == BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED) {
344             /* Confirm or auto-confirm */
345 
346             if (mFileInfo.mFileName == null) {
347                 status = mFileInfo.mStatus;
348                 /* TODO need to check if this line is correct */
349                 mInfo.mStatus = mFileInfo.mStatus;
350                 Constants.updateShareStatus(mContext, mInfo.mId, status);
351                 obexResponse = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
352 
353             }
354 
355             if (mFileInfo.mFileName != null) {
356 
357                 ContentValues updateValues = new ContentValues();
358                 contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId);
359                 updateValues.put(BluetoothShare._DATA, mFileInfo.mFileName);
360                 updateValues.put(BluetoothShare.STATUS, BluetoothShare.STATUS_RUNNING);
361                 mContext.getContentResolver().update(contentUri, updateValues, null, null);
362 
363                 status = receiveFile(mFileInfo, op);
364                 /*
365                  * TODO map status to obex response code
366                  */
367                 if (status != BluetoothShare.STATUS_SUCCESS) {
368                     obexResponse = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
369                 }
370                 Constants.updateShareStatus(mContext, mInfo.mId, status);
371             }
372 
373             if (status == BluetoothShare.STATUS_SUCCESS) {
374                 Message msg = Message.obtain(mCallback, BluetoothOppObexSession.MSG_SHARE_COMPLETE);
375                 msg.obj = mInfo;
376                 msg.sendToTarget();
377             } else {
378                 if (mCallback != null) {
379                     Message msg = Message.obtain(mCallback,
380                             BluetoothOppObexSession.MSG_SESSION_ERROR);
381                     mInfo.mStatus = status;
382                     msg.obj = mInfo;
383                     msg.sendToTarget();
384                 }
385             }
386         } else if (mAccepted == BluetoothShare.USER_CONFIRMATION_DENIED
387                 || mAccepted == BluetoothShare.USER_CONFIRMATION_TIMEOUT) {
388             /* user actively deny the inbound transfer */
389             /*
390              * Note There is a question: what's next if user deny the first obj?
391              * Option 1 :continue prompt for next objects
392              * Option 2 :reject next objects and finish the session
393              * Now we take option 2:
394              */
395 
396             Log.i(TAG, "Rejected incoming request");
397             if (mFileInfo.mFileName != null) {
398                 try {
399                     mFileInfo.mOutputStream.close();
400                 } catch (IOException e) {
401                     Log.e(TAG, "error close file stream");
402                 }
403                 new File(mFileInfo.mFileName).delete();
404             }
405             // set status as local cancel
406             status = BluetoothShare.STATUS_CANCELED;
407             Constants.updateShareStatus(mContext, mInfo.mId, status);
408             obexResponse = ResponseCodes.OBEX_HTTP_FORBIDDEN;
409 
410             Message msg = Message.obtain(mCallback);
411             msg.what = BluetoothOppObexSession.MSG_SHARE_INTERRUPTED;
412             mInfo.mStatus = status;
413             msg.obj = mInfo;
414             msg.sendToTarget();
415         }
416         return obexResponse;
417     }
418 
receiveFile(BluetoothOppReceiveFileInfo fileInfo, Operation op)419     private int receiveFile(BluetoothOppReceiveFileInfo fileInfo, Operation op) {
420         /*
421          * implement receive file
422          */
423         int status = -1;
424         BufferedOutputStream bos = null;
425 
426         InputStream is = null;
427         boolean error = false;
428         try {
429             is = op.openInputStream();
430         } catch (IOException e1) {
431             Log.e(TAG, "Error when openInputStream");
432             status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
433             error = true;
434         }
435 
436         Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId);
437 
438         if (!error) {
439             ContentValues updateValues = new ContentValues();
440             updateValues.put(BluetoothShare._DATA, fileInfo.mFileName);
441             mContext.getContentResolver().update(contentUri, updateValues, null, null);
442         }
443 
444         long position = 0;
445         long percent = 0;
446         long prevPercent = 0;
447 
448         if (!error) {
449             bos = new BufferedOutputStream(fileInfo.mOutputStream, 0x10000);
450         }
451 
452         if (!error) {
453             int outputBufferSize = op.getMaxPacketSize();
454             byte[] b = new byte[outputBufferSize];
455             int readLength = 0;
456             long timestamp = 0;
457             try {
458                 while ((!mInterrupted) && (position != fileInfo.mLength)) {
459 
460                     if (V) timestamp = System.currentTimeMillis();
461 
462                     readLength = is.read(b);
463 
464                     if (readLength == -1) {
465                         if (D) Log.d(TAG, "Receive file reached stream end at position" + position);
466                         break;
467                     }
468 
469                     bos.write(b, 0, readLength);
470                     position += readLength;
471                     percent = position * 100 / fileInfo.mLength;
472 
473                     if (V) {
474                         Log.v(TAG, "Receive file position = " + position + " readLength "
475                                 + readLength + " bytes took "
476                                 + (System.currentTimeMillis() - timestamp) + " ms");
477                     }
478 
479                     // Update the Progress Bar only if there is change in percentage
480                     if (percent > prevPercent) {
481                         ContentValues updateValues = new ContentValues();
482                         updateValues.put(BluetoothShare.CURRENT_BYTES, position);
483                         mContext.getContentResolver().update(contentUri, updateValues, null, null);
484                         prevPercent = percent;
485                     }
486                 }
487             } catch (IOException e1) {
488                 Log.e(TAG, "Error when receiving file: " + e1);
489                 /* OBEX Abort packet received from remote device */
490                 if ("Abort Received".equals(e1.getMessage())) {
491                     status = BluetoothShare.STATUS_CANCELED;
492                 } else {
493                     status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
494                 }
495                 error = true;
496             }
497         }
498 
499         if (mInterrupted) {
500             if (D) Log.d(TAG, "receiving file interrupted by user.");
501             status = BluetoothShare.STATUS_CANCELED;
502         } else {
503             if (position == fileInfo.mLength) {
504                 if (D) Log.d(TAG, "Receiving file completed for " + fileInfo.mFileName);
505                 status = BluetoothShare.STATUS_SUCCESS;
506             } else {
507                 if (D) Log.d(TAG, "Reading file failed at " + position + " of " + fileInfo.mLength);
508                 if (status == -1) {
509                     status = BluetoothShare.STATUS_UNKNOWN_ERROR;
510                 }
511             }
512         }
513 
514         if (bos != null) {
515             try {
516                 bos.close();
517             } catch (IOException e) {
518                 Log.e(TAG, "Error when closing stream after send");
519             }
520         }
521         return status;
522     }
523 
processShareInfo()524     private BluetoothOppReceiveFileInfo processShareInfo() {
525         if (D) Log.d(TAG, "processShareInfo() " + mInfo.mId);
526         BluetoothOppReceiveFileInfo fileInfo = BluetoothOppReceiveFileInfo.generateFileInfo(
527                 mContext, mInfo.mId);
528         if (V) {
529             Log.v(TAG, "Generate BluetoothOppReceiveFileInfo:");
530             Log.v(TAG, "filename  :" + fileInfo.mFileName);
531             Log.v(TAG, "length    :" + fileInfo.mLength);
532             Log.v(TAG, "status    :" + fileInfo.mStatus);
533         }
534         return fileInfo;
535     }
536 
537     @Override
onConnect(HeaderSet request, HeaderSet reply)538     public int onConnect(HeaderSet request, HeaderSet reply) {
539 
540         if (D) Log.d(TAG, "onConnect");
541         if (V) Constants.logHeader(request);
542         Long objectCount = null;
543         try {
544             byte[] uuid = (byte[])request.getHeader(HeaderSet.TARGET);
545             if (V) Log.v(TAG, "onConnect(): uuid =" + Arrays.toString(uuid));
546             if(uuid != null) {
547                  return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
548             }
549 
550             objectCount = (Long) request.getHeader(HeaderSet.COUNT);
551         } catch (IOException e) {
552             Log.e(TAG, e.toString());
553             return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
554         }
555         String destination;
556         if (mTransport instanceof BluetoothObexTransport) {
557             destination = ((BluetoothObexTransport)mTransport).getRemoteAddress();
558         } else {
559             destination = "FF:FF:FF:00:00:00";
560         }
561         boolean isHandover = BluetoothOppManager.getInstance(mContext).
562                 isWhitelisted(destination);
563         if (isHandover) {
564             // Notify the handover requester file transfer has started
565             Intent intent = new Intent(Constants.ACTION_HANDOVER_STARTED);
566             if (objectCount != null) {
567                 intent.putExtra(Constants.EXTRA_BT_OPP_OBJECT_COUNT, objectCount.intValue());
568             } else {
569                 intent.putExtra(Constants.EXTRA_BT_OPP_OBJECT_COUNT,
570                         Constants.COUNT_HEADER_UNAVAILABLE);
571             }
572             intent.putExtra(Constants.EXTRA_BT_OPP_ADDRESS, destination);
573             mContext.sendBroadcast(intent, Constants.HANDOVER_STATUS_PERMISSION);
574         }
575         mTimestamp = System.currentTimeMillis();
576         return ResponseCodes.OBEX_HTTP_OK;
577     }
578 
579     @Override
onDisconnect(HeaderSet req, HeaderSet resp)580     public void onDisconnect(HeaderSet req, HeaderSet resp) {
581         if (D) Log.d(TAG, "onDisconnect");
582         resp.responseCode = ResponseCodes.OBEX_HTTP_OK;
583     }
584 
releaseWakeLocks()585     private synchronized void releaseWakeLocks() {
586         if (mPartialWakeLock.isHeld()) {
587             mPartialWakeLock.release();
588         }
589     }
590 
591     @Override
onClose()592     public void onClose() {
593         if (D) Log.d(TAG, "onClose");
594         releaseWakeLocks();
595 
596         if (mServerSocket != null) {
597             if (D) Log.d(TAG, "prepareForNewConnect");
598             mServerSocket.prepareForNewConnect();
599         }
600 
601         NotificationManager nm =
602                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
603         nm.cancel(BluetoothOppNotification.NOTIFICATION_ID_PROGRESS);
604 
605         /* onClose could happen even before start() where mCallback is set */
606         if (mCallback != null) {
607             Message msg = Message.obtain(mCallback);
608             msg.what = BluetoothOppObexSession.MSG_SESSION_COMPLETE;
609             msg.obj = mInfo;
610             msg.sendToTarget();
611         }
612     }
613 }
614