Home | History | Annotate | Download | only in opp
      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 
    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 
    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      */
    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      */
    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      */
    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 
    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
    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 
    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 
    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
    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
    536     public void onDisconnect(HeaderSet req, HeaderSet resp) {
    537         if (D) Log.d(TAG, "onDisconnect");
    538         resp.responseCode = ResponseCodes.OBEX_HTTP_OK;
    539     }
    540 
    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
    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