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