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