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 javax.obex.ClientOperation;
     36 import javax.obex.ClientSession;
     37 import javax.obex.HeaderSet;
     38 import javax.obex.ObexTransport;
     39 import javax.obex.ResponseCodes;
     40 
     41 import android.content.ContentValues;
     42 import android.content.Context;
     43 import android.net.Uri;
     44 import android.os.Handler;
     45 import android.os.Message;
     46 import android.os.PowerManager;
     47 import android.os.PowerManager.WakeLock;
     48 import android.os.Process;
     49 import android.util.Log;
     50 
     51 import java.io.BufferedInputStream;
     52 import java.io.IOException;
     53 import java.io.InputStream;
     54 import java.io.OutputStream;
     55 import java.lang.Thread;
     56 
     57 /**
     58  * This class runs as an OBEX client
     59  */
     60 public class BluetoothOppObexClientSession implements BluetoothOppObexSession {
     61 
     62     private static final String TAG = "BtOppObexClient";
     63     private static final boolean D = Constants.DEBUG;
     64     private static final boolean V = Constants.VERBOSE;
     65 
     66     private ClientThread mThread;
     67 
     68     private ObexTransport mTransport;
     69 
     70     private Context mContext;
     71 
     72     private volatile boolean mInterrupted;
     73 
     74     private volatile boolean mWaitingForRemote;
     75 
     76     private Handler mCallback;
     77 
     78     public BluetoothOppObexClientSession(Context context, ObexTransport transport) {
     79         if (transport == null) {
     80             throw new NullPointerException("transport is null");
     81         }
     82         mContext = context;
     83         mTransport = transport;
     84     }
     85 
     86     public void start(Handler handler, int numShares) {
     87         if (D) Log.d(TAG, "Start!");
     88         mCallback = handler;
     89         mThread = new ClientThread(mContext, mTransport, numShares);
     90         mThread.start();
     91     }
     92 
     93     public void stop() {
     94         if (D) Log.d(TAG, "Stop!");
     95         if (mThread != null) {
     96             mInterrupted = true;
     97             try {
     98                 mThread.interrupt();
     99                 if (V) Log.v(TAG, "waiting for thread to terminate");
    100                 mThread.join();
    101                 mThread = null;
    102             } catch (InterruptedException e) {
    103                 if (V) Log.v(TAG, "Interrupted waiting for thread to join");
    104             }
    105         }
    106         mCallback = null;
    107     }
    108 
    109     public void addShare(BluetoothOppShareInfo share) {
    110         mThread.addShare(share);
    111     }
    112 
    113     private static int readFully(InputStream is, byte[] buffer, int size) throws IOException {
    114         int done = 0;
    115         while (done < size) {
    116             int got = is.read(buffer, done, size - done);
    117             if (got <= 0) break;
    118             done += got;
    119         }
    120         return done;
    121     }
    122 
    123     private class ClientThread extends Thread {
    124 
    125         private static final int sSleepTime = 500;
    126 
    127         private Context mContext1;
    128 
    129         private BluetoothOppShareInfo mInfo;
    130 
    131         private volatile boolean waitingForShare;
    132 
    133         private ObexTransport mTransport1;
    134 
    135         private ClientSession mCs;
    136 
    137         private WakeLock wakeLock;
    138 
    139         private BluetoothOppSendFileInfo mFileInfo = null;
    140 
    141         private boolean mConnected = false;
    142 
    143         private int mNumShares;
    144 
    145         public ClientThread(Context context, ObexTransport transport, int initialNumShares) {
    146             super("BtOpp ClientThread");
    147             mContext1 = context;
    148             mTransport1 = transport;
    149             waitingForShare = true;
    150             mWaitingForRemote = false;
    151             mNumShares = initialNumShares;
    152             PowerManager pm = (PowerManager)mContext1.getSystemService(Context.POWER_SERVICE);
    153             wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
    154         }
    155 
    156         public void addShare(BluetoothOppShareInfo info) {
    157             mInfo = info;
    158             mFileInfo = processShareInfo();
    159             waitingForShare = false;
    160         }
    161 
    162         @Override
    163         public void run() {
    164             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    165 
    166             if (V) Log.v(TAG, "acquire partial WakeLock");
    167             wakeLock.acquire();
    168 
    169             try {
    170                 Thread.sleep(100);
    171             } catch (InterruptedException e1) {
    172                 if (V) Log.v(TAG, "Client thread was interrupted (1), exiting");
    173                 mInterrupted = true;
    174             }
    175             if (!mInterrupted) {
    176                 connect(mNumShares);
    177             }
    178 
    179             while (!mInterrupted) {
    180                 if (!waitingForShare) {
    181                     doSend();
    182                 } else {
    183                     try {
    184                         if (D) Log.d(TAG, "Client thread waiting for next share, sleep for "
    185                                     + sSleepTime);
    186                         Thread.sleep(sSleepTime);
    187                     } catch (InterruptedException e) {
    188 
    189                     }
    190                 }
    191             }
    192             disconnect();
    193 
    194             if (wakeLock.isHeld()) {
    195                 if (V) Log.v(TAG, "release partial WakeLock");
    196                 wakeLock.release();
    197             }
    198             Message msg = Message.obtain(mCallback);
    199             msg.what = BluetoothOppObexSession.MSG_SESSION_COMPLETE;
    200             msg.obj = mInfo;
    201             msg.sendToTarget();
    202 
    203         }
    204 
    205         private void disconnect() {
    206             try {
    207                 if (mCs != null) {
    208                     mCs.disconnect(null);
    209                 }
    210                 mCs = null;
    211                 if (D) Log.d(TAG, "OBEX session disconnected");
    212             } catch (IOException e) {
    213                 Log.w(TAG, "OBEX session disconnect error" + e);
    214             }
    215             try {
    216                 if (mCs != null) {
    217                     if (D) Log.d(TAG, "OBEX session close mCs");
    218                     mCs.close();
    219                     if (D) Log.d(TAG, "OBEX session closed");
    220                     }
    221             } catch (IOException e) {
    222                 Log.w(TAG, "OBEX session close error" + e);
    223             }
    224             if (mTransport1 != null) {
    225                 try {
    226                     mTransport1.close();
    227                 } catch (IOException e) {
    228                     Log.e(TAG, "mTransport.close error");
    229                 }
    230 
    231             }
    232         }
    233 
    234         private void connect(int numShares) {
    235             if (D) Log.d(TAG, "Create ClientSession with transport " + mTransport1.toString());
    236             try {
    237                 mCs = new ClientSession(mTransport1);
    238                 mConnected = true;
    239             } catch (IOException e1) {
    240                 Log.e(TAG, "OBEX session create error");
    241             }
    242             if (mConnected) {
    243                 mConnected = false;
    244                 HeaderSet hs = new HeaderSet();
    245                 hs.setHeader(HeaderSet.COUNT, (long) numShares);
    246                 synchronized (this) {
    247                     mWaitingForRemote = true;
    248                 }
    249                 try {
    250                     mCs.connect(hs);
    251                     if (D) Log.d(TAG, "OBEX session created");
    252                     mConnected = true;
    253                 } catch (IOException e) {
    254                     Log.e(TAG, "OBEX session connect error");
    255                 }
    256             }
    257             synchronized (this) {
    258                 mWaitingForRemote = false;
    259             }
    260         }
    261 
    262         private void doSend() {
    263 
    264             int status = BluetoothShare.STATUS_SUCCESS;
    265 
    266             /* connection is established too fast to get first mInfo */
    267             while (mFileInfo == null) {
    268                 try {
    269                     Thread.sleep(50);
    270                 } catch (InterruptedException e) {
    271                     status = BluetoothShare.STATUS_CANCELED;
    272                 }
    273             }
    274             if (!mConnected) {
    275                 // Obex connection error
    276                 status = BluetoothShare.STATUS_CONNECTION_ERROR;
    277             }
    278             if (status == BluetoothShare.STATUS_SUCCESS) {
    279                 /* do real send */
    280                 if (mFileInfo.mFileName != null) {
    281                     status = sendFile(mFileInfo);
    282                 } else {
    283                     /* this is invalid request */
    284                     status = mFileInfo.mStatus;
    285                 }
    286                 waitingForShare = true;
    287             } else {
    288                 Constants.updateShareStatus(mContext1, mInfo.mId, status);
    289             }
    290 
    291             if (status == BluetoothShare.STATUS_SUCCESS) {
    292                 Message msg = Message.obtain(mCallback);
    293                 msg.what = BluetoothOppObexSession.MSG_SHARE_COMPLETE;
    294                 msg.obj = mInfo;
    295                 msg.sendToTarget();
    296             } else {
    297                 Message msg = Message.obtain(mCallback);
    298                 msg.what = BluetoothOppObexSession.MSG_SESSION_ERROR;
    299                 mInfo.mStatus = status;
    300                 msg.obj = mInfo;
    301                 msg.sendToTarget();
    302             }
    303         }
    304 
    305         /*
    306          * Validate this ShareInfo
    307          */
    308         private BluetoothOppSendFileInfo processShareInfo() {
    309             if (V) Log.v(TAG, "Client thread processShareInfo() " + mInfo.mId);
    310 
    311             BluetoothOppSendFileInfo fileInfo = BluetoothOppUtility.getSendFileInfo(mInfo.mUri);
    312             if (fileInfo.mFileName == null || fileInfo.mLength == 0) {
    313                 if (V) Log.v(TAG, "BluetoothOppSendFileInfo get invalid file");
    314                     Constants.updateShareStatus(mContext1, mInfo.mId, fileInfo.mStatus);
    315 
    316             } else {
    317                 if (V) {
    318                     Log.v(TAG, "Generate BluetoothOppSendFileInfo:");
    319                     Log.v(TAG, "filename  :" + fileInfo.mFileName);
    320                     Log.v(TAG, "length    :" + fileInfo.mLength);
    321                     Log.v(TAG, "mimetype  :" + fileInfo.mMimetype);
    322                 }
    323 
    324                 ContentValues updateValues = new ContentValues();
    325                 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId);
    326 
    327                 updateValues.put(BluetoothShare.FILENAME_HINT, fileInfo.mFileName);
    328                 updateValues.put(BluetoothShare.TOTAL_BYTES, fileInfo.mLength);
    329                 updateValues.put(BluetoothShare.MIMETYPE, fileInfo.mMimetype);
    330 
    331                 mContext1.getContentResolver().update(contentUri, updateValues, null, null);
    332 
    333             }
    334             return fileInfo;
    335         }
    336 
    337         private int sendFile(BluetoothOppSendFileInfo fileInfo) {
    338             boolean error = false;
    339             int responseCode = -1;
    340             int status = BluetoothShare.STATUS_SUCCESS;
    341             Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId);
    342             ContentValues updateValues;
    343             HeaderSet request;
    344             request = new HeaderSet();
    345             request.setHeader(HeaderSet.NAME, fileInfo.mFileName);
    346             request.setHeader(HeaderSet.TYPE, fileInfo.mMimetype);
    347 
    348             applyRemoteDeviceQuirks(request, mInfo.mDestination, fileInfo.mFileName);
    349 
    350             Constants.updateShareStatus(mContext1, mInfo.mId, BluetoothShare.STATUS_RUNNING);
    351 
    352             request.setHeader(HeaderSet.LENGTH, fileInfo.mLength);
    353             ClientOperation putOperation = null;
    354             OutputStream outputStream = null;
    355             InputStream inputStream = null;
    356             try {
    357                 synchronized (this) {
    358                     mWaitingForRemote = true;
    359                 }
    360                 try {
    361                     if (V) Log.v(TAG, "put headerset for " + fileInfo.mFileName);
    362                     putOperation = (ClientOperation)mCs.put(request);
    363                 } catch (IOException e) {
    364                     status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
    365                     Constants.updateShareStatus(mContext1, mInfo.mId, status);
    366 
    367                     Log.e(TAG, "Error when put HeaderSet ");
    368                     error = true;
    369                 }
    370                 synchronized (this) {
    371                     mWaitingForRemote = false;
    372                 }
    373 
    374                 if (!error) {
    375                     try {
    376                         if (V) Log.v(TAG, "openOutputStream " + fileInfo.mFileName);
    377                         outputStream = putOperation.openOutputStream();
    378                         inputStream = putOperation.openInputStream();
    379                     } catch (IOException e) {
    380                         status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
    381                         Constants.updateShareStatus(mContext1, mInfo.mId, status);
    382                         Log.e(TAG, "Error when openOutputStream");
    383                         error = true;
    384                     }
    385                 }
    386                 if (!error) {
    387                     updateValues = new ContentValues();
    388                     updateValues.put(BluetoothShare.CURRENT_BYTES, 0);
    389                     updateValues.put(BluetoothShare.STATUS, BluetoothShare.STATUS_RUNNING);
    390                     mContext1.getContentResolver().update(contentUri, updateValues, null, null);
    391                 }
    392 
    393                 if (!error) {
    394                     int position = 0;
    395                     int readLength = 0;
    396                     boolean okToProceed = false;
    397                     long timestamp = 0;
    398                     int outputBufferSize = putOperation.getMaxPacketSize();
    399                     byte[] buffer = new byte[outputBufferSize];
    400                     BufferedInputStream a = new BufferedInputStream(fileInfo.mInputStream, 0x4000);
    401 
    402                     if (!mInterrupted && (position != fileInfo.mLength)) {
    403                         readLength = readFully(a, buffer, outputBufferSize);
    404 
    405                         mCallback.sendMessageDelayed(mCallback
    406                                 .obtainMessage(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT),
    407                                 BluetoothOppObexSession.SESSION_TIMEOUT);
    408                         synchronized (this) {
    409                             mWaitingForRemote = true;
    410                         }
    411 
    412                         // first packet will block here
    413                         outputStream.write(buffer, 0, readLength);
    414 
    415                         position += readLength;
    416 
    417                         if (position != fileInfo.mLength) {
    418                             mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
    419                             synchronized (this) {
    420                                 mWaitingForRemote = false;
    421                             }
    422                         } else {
    423                             // if file length is smaller than buffer size, only one packet
    424                             // so block point is here
    425                             outputStream.close();
    426                             mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
    427                             synchronized (this) {
    428                                 mWaitingForRemote = false;
    429                             }
    430                         }
    431                         /* check remote accept or reject */
    432                         responseCode = putOperation.getResponseCode();
    433 
    434                         if (responseCode == ResponseCodes.OBEX_HTTP_CONTINUE
    435                                 || responseCode == ResponseCodes.OBEX_HTTP_OK) {
    436                             if (V) Log.v(TAG, "Remote accept");
    437                             okToProceed = true;
    438                             updateValues = new ContentValues();
    439                             updateValues.put(BluetoothShare.CURRENT_BYTES, position);
    440                             mContext1.getContentResolver().update(contentUri, updateValues, null,
    441                                     null);
    442                         } else {
    443                             Log.i(TAG, "Remote reject, Response code is " + responseCode);
    444                         }
    445                     }
    446 
    447                     while (!mInterrupted && okToProceed && (position != fileInfo.mLength)) {
    448                         {
    449                             if (V) timestamp = System.currentTimeMillis();
    450 
    451                             readLength = a.read(buffer, 0, outputBufferSize);
    452                             outputStream.write(buffer, 0, readLength);
    453 
    454                             /* check remote abort */
    455                             responseCode = putOperation.getResponseCode();
    456                             if (V) Log.v(TAG, "Response code is " + responseCode);
    457                             if (responseCode != ResponseCodes.OBEX_HTTP_CONTINUE
    458                                     && responseCode != ResponseCodes.OBEX_HTTP_OK) {
    459                                 /* abort happens */
    460                                 okToProceed = false;
    461                             } else {
    462                                 position += readLength;
    463                                 if (V) {
    464                                     Log.v(TAG, "Sending file position = " + position
    465                                             + " readLength " + readLength + " bytes took "
    466                                             + (System.currentTimeMillis() - timestamp) + " ms");
    467                                 }
    468                                 updateValues = new ContentValues();
    469                                 updateValues.put(BluetoothShare.CURRENT_BYTES, position);
    470                                 mContext1.getContentResolver().update(contentUri, updateValues,
    471                                         null, null);
    472                             }
    473                         }
    474                     }
    475 
    476                     if (responseCode == ResponseCodes.OBEX_HTTP_FORBIDDEN
    477                             || responseCode == ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE) {
    478                         Log.i(TAG, "Remote reject file " + fileInfo.mFileName + " length "
    479                                 + fileInfo.mLength);
    480                         status = BluetoothShare.STATUS_FORBIDDEN;
    481                     } else if (responseCode == ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE) {
    482                         Log.i(TAG, "Remote reject file type " + fileInfo.mMimetype);
    483                         status = BluetoothShare.STATUS_NOT_ACCEPTABLE;
    484                     } else if (!mInterrupted && position == fileInfo.mLength) {
    485                         Log.i(TAG, "SendFile finished send out file " + fileInfo.mFileName
    486                                 + " length " + fileInfo.mLength);
    487                         outputStream.close();
    488                     } else {
    489                         error = true;
    490                         status = BluetoothShare.STATUS_CANCELED;
    491                         putOperation.abort();
    492                         /* interrupted */
    493                         Log.i(TAG, "SendFile interrupted when send out file " + fileInfo.mFileName
    494                                 + " at " + position + " of " + fileInfo.mLength);
    495                     }
    496                 }
    497             } catch (IOException e) {
    498                 handleSendException(e.toString());
    499             } catch (NullPointerException e) {
    500                 handleSendException(e.toString());
    501             } catch (IndexOutOfBoundsException e) {
    502                 handleSendException(e.toString());
    503             } finally {
    504                 try {
    505                     // Close InputStream and remove SendFileInfo from map
    506                     BluetoothOppUtility.closeSendFileInfo(mInfo.mUri);
    507                     if (!error) {
    508                         responseCode = putOperation.getResponseCode();
    509                         if (responseCode != -1) {
    510                             if (V) Log.v(TAG, "Get response code " + responseCode);
    511                             if (responseCode != ResponseCodes.OBEX_HTTP_OK) {
    512                                 Log.i(TAG, "Response error code is " + responseCode);
    513                                 status = BluetoothShare.STATUS_UNHANDLED_OBEX_CODE;
    514                                 if (responseCode == ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE) {
    515                                     status = BluetoothShare.STATUS_NOT_ACCEPTABLE;
    516                                 }
    517                                 if (responseCode == ResponseCodes.OBEX_HTTP_FORBIDDEN
    518                                         || responseCode == ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE) {
    519                                     status = BluetoothShare.STATUS_FORBIDDEN;
    520                                 }
    521                             }
    522                         } else {
    523                             // responseCode is -1, which means connection error
    524                             status = BluetoothShare.STATUS_CONNECTION_ERROR;
    525                         }
    526                     }
    527 
    528                     Constants.updateShareStatus(mContext1, mInfo.mId, status);
    529 
    530                     if (inputStream != null) {
    531                         inputStream.close();
    532                     }
    533                     if (putOperation != null) {
    534                         putOperation.close();
    535                     }
    536                 } catch (IOException e) {
    537                     Log.e(TAG, "Error when closing stream after send");
    538                 }
    539             }
    540             return status;
    541         }
    542 
    543         private void handleSendException(String exception) {
    544             Log.e(TAG, "Error when sending file: " + exception);
    545             int status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
    546             Constants.updateShareStatus(mContext1, mInfo.mId, status);
    547             mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
    548         }
    549 
    550         @Override
    551         public void interrupt() {
    552             super.interrupt();
    553             synchronized (this) {
    554                 if (mWaitingForRemote) {
    555                     if (V) Log.v(TAG, "Interrupted when waitingForRemote");
    556                     try {
    557                         mTransport1.close();
    558                     } catch (IOException e) {
    559                         Log.e(TAG, "mTransport.close error");
    560                     }
    561                     Message msg = Message.obtain(mCallback);
    562                     msg.what = BluetoothOppObexSession.MSG_SHARE_INTERRUPTED;
    563                     if (mInfo != null) {
    564                         msg.obj = mInfo;
    565                     }
    566                     msg.sendToTarget();
    567                 }
    568             }
    569         }
    570     }
    571 
    572     public static void applyRemoteDeviceQuirks(HeaderSet request, String address, String filename) {
    573         if (address == null) {
    574             return;
    575         }
    576         if (address.startsWith("00:04:48")) {
    577             // Poloroid Pogo
    578             // Rejects filenames with more than one '.'. Rename to '_'.
    579             // for example: 'a.b.jpg' -> 'a_b.jpg'
    580             //              'abc.jpg' NOT CHANGED
    581             char[] c = filename.toCharArray();
    582             boolean firstDot = true;
    583             boolean modified = false;
    584             for (int i = c.length - 1; i >= 0; i--) {
    585                 if (c[i] == '.') {
    586                     if (!firstDot) {
    587                         modified = true;
    588                         c[i] = '_';
    589                     }
    590                     firstDot = false;
    591                 }
    592             }
    593 
    594             if (modified) {
    595                 String newFilename = new String(c);
    596                 request.setHeader(HeaderSet.NAME, newFilename);
    597                 Log.i(TAG, "Sending file \"" + filename + "\" as \"" + newFilename +
    598                         "\" to workaround Poloroid filename quirk");
    599             }
    600         }
    601     }
    602 
    603     public void unblock() {
    604         // Not used for client case
    605     }
    606 
    607 }
    608