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 = "BtOpp ObexClient";
     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) {
     87         if (D) Log.d(TAG, "Start!");
     88         mCallback = handler;
     89         mThread = new ClientThread(mContext, mTransport);
     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         public ClientThread(Context context, ObexTransport transport) {
    144             super("BtOpp ClientThread");
    145             mContext1 = context;
    146             mTransport1 = transport;
    147             waitingForShare = true;
    148             mWaitingForRemote = false;
    149 
    150             PowerManager pm = (PowerManager)mContext1.getSystemService(Context.POWER_SERVICE);
    151             wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
    152         }
    153 
    154         public void addShare(BluetoothOppShareInfo info) {
    155             mInfo = info;
    156             mFileInfo = processShareInfo();
    157             waitingForShare = false;
    158         }
    159 
    160         @Override
    161         public void run() {
    162             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    163 
    164             if (V) Log.v(TAG, "acquire partial WakeLock");
    165             wakeLock.acquire();
    166 
    167             try {
    168                 Thread.sleep(100);
    169             } catch (InterruptedException e1) {
    170                 if (V) Log.v(TAG, "Client thread was interrupted (1), exiting");
    171                 mInterrupted = true;
    172             }
    173             if (!mInterrupted) {
    174                 connect();
    175             }
    176 
    177             while (!mInterrupted) {
    178                 if (!waitingForShare) {
    179                     doSend();
    180                 } else {
    181                     try {
    182                         if (D) Log.d(TAG, "Client thread waiting for next share, sleep for "
    183                                     + sSleepTime);
    184                         Thread.sleep(sSleepTime);
    185                     } catch (InterruptedException e) {
    186 
    187                     }
    188                 }
    189             }
    190             disconnect();
    191 
    192             if (wakeLock.isHeld()) {
    193                 if (V) Log.v(TAG, "release partial WakeLock");
    194                 wakeLock.release();
    195             }
    196             Message msg = Message.obtain(mCallback);
    197             msg.what = BluetoothOppObexSession.MSG_SESSION_COMPLETE;
    198             msg.obj = mInfo;
    199             msg.sendToTarget();
    200 
    201         }
    202 
    203         private void disconnect() {
    204             try {
    205                 if (mCs != null) {
    206                     mCs.disconnect(null);
    207                 }
    208                 mCs = null;
    209                 if (D) Log.d(TAG, "OBEX session disconnected");
    210             } catch (IOException e) {
    211                 Log.w(TAG, "OBEX session disconnect error" + e);
    212             }
    213             try {
    214                 if (mCs != null) {
    215                     if (D) Log.d(TAG, "OBEX session close mCs");
    216                     mCs.close();
    217                     if (D) Log.d(TAG, "OBEX session closed");
    218                     }
    219             } catch (IOException e) {
    220                 Log.w(TAG, "OBEX session close error" + e);
    221             }
    222             if (mTransport1 != null) {
    223                 try {
    224                     mTransport1.close();
    225                 } catch (IOException e) {
    226                     Log.e(TAG, "mTransport.close error");
    227                 }
    228 
    229             }
    230         }
    231 
    232         private void connect() {
    233             if (D) Log.d(TAG, "Create ClientSession with transport " + mTransport1.toString());
    234             try {
    235                 mCs = new ClientSession(mTransport1);
    236                 mConnected = true;
    237             } catch (IOException e1) {
    238                 Log.e(TAG, "OBEX session create error");
    239             }
    240             if (mConnected) {
    241                 mConnected = false;
    242                 HeaderSet hs = new HeaderSet();
    243                 synchronized (this) {
    244                     mWaitingForRemote = true;
    245                 }
    246                 try {
    247                     mCs.connect(hs);
    248                     if (D) Log.d(TAG, "OBEX session created");
    249                     mConnected = true;
    250                 } catch (IOException e) {
    251                     Log.e(TAG, "OBEX session connect error");
    252                 }
    253             }
    254             synchronized (this) {
    255                 mWaitingForRemote = false;
    256             }
    257         }
    258 
    259         private void doSend() {
    260 
    261             int status = BluetoothShare.STATUS_SUCCESS;
    262 
    263             /* connection is established too fast to get first mInfo */
    264             while (mFileInfo == null) {
    265                 try {
    266                     Thread.sleep(50);
    267                 } catch (InterruptedException e) {
    268                     status = BluetoothShare.STATUS_CANCELED;
    269                 }
    270             }
    271             if (!mConnected) {
    272                 // Obex connection error
    273                 status = BluetoothShare.STATUS_CONNECTION_ERROR;
    274             }
    275             if (status == BluetoothShare.STATUS_SUCCESS) {
    276                 /* do real send */
    277                 if (mFileInfo.mFileName != null) {
    278                     status = sendFile(mFileInfo);
    279                 } else {
    280                     /* this is invalid request */
    281                     status = mFileInfo.mStatus;
    282                 }
    283                 waitingForShare = true;
    284             } else {
    285                 Constants.updateShareStatus(mContext1, mInfo.mId, status);
    286             }
    287 
    288             if (status == BluetoothShare.STATUS_SUCCESS) {
    289                 Message msg = Message.obtain(mCallback);
    290                 msg.what = BluetoothOppObexSession.MSG_SHARE_COMPLETE;
    291                 msg.obj = mInfo;
    292                 msg.sendToTarget();
    293             } else {
    294                 Message msg = Message.obtain(mCallback);
    295                 msg.what = BluetoothOppObexSession.MSG_SESSION_ERROR;
    296                 mInfo.mStatus = status;
    297                 msg.obj = mInfo;
    298                 msg.sendToTarget();
    299             }
    300         }
    301 
    302         /*
    303          * Validate this ShareInfo
    304          */
    305         private BluetoothOppSendFileInfo processShareInfo() {
    306             if (V) Log.v(TAG, "Client thread processShareInfo() " + mInfo.mId);
    307 
    308             BluetoothOppSendFileInfo fileInfo = BluetoothOppSendFileInfo.generateFileInfo(
    309                     mContext1, mInfo.mUri, mInfo.mMimetype, mInfo.mDestination);
    310             if (fileInfo.mFileName == null || fileInfo.mLength == 0) {
    311                 if (V) Log.v(TAG, "BluetoothOppSendFileInfo get invalid file");
    312                     Constants.updateShareStatus(mContext1, mInfo.mId, fileInfo.mStatus);
    313 
    314             } else {
    315                 if (V) {
    316                     Log.v(TAG, "Generate BluetoothOppSendFileInfo:");
    317                     Log.v(TAG, "filename  :" + fileInfo.mFileName);
    318                     Log.v(TAG, "length    :" + fileInfo.mLength);
    319                     Log.v(TAG, "mimetype  :" + fileInfo.mMimetype);
    320                 }
    321 
    322                 ContentValues updateValues = new ContentValues();
    323                 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId);
    324 
    325                 updateValues.put(BluetoothShare.FILENAME_HINT, fileInfo.mFileName);
    326                 updateValues.put(BluetoothShare.TOTAL_BYTES, fileInfo.mLength);
    327                 updateValues.put(BluetoothShare.MIMETYPE, fileInfo.mMimetype);
    328 
    329                 mContext1.getContentResolver().update(contentUri, updateValues, null, null);
    330 
    331             }
    332             return fileInfo;
    333         }
    334 
    335         private int sendFile(BluetoothOppSendFileInfo fileInfo) {
    336             boolean error = false;
    337             int responseCode = -1;
    338             int status = BluetoothShare.STATUS_SUCCESS;
    339             Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId);
    340             ContentValues updateValues;
    341             HeaderSet request;
    342             request = new HeaderSet();
    343             request.setHeader(HeaderSet.NAME, fileInfo.mFileName);
    344             request.setHeader(HeaderSet.TYPE, fileInfo.mMimetype);
    345 
    346             applyRemoteDeviceQuirks(request, fileInfo);
    347 
    348             Constants.updateShareStatus(mContext1, mInfo.mId, BluetoothShare.STATUS_RUNNING);
    349 
    350             request.setHeader(HeaderSet.LENGTH, fileInfo.mLength);
    351             ClientOperation putOperation = null;
    352             OutputStream outputStream = null;
    353             InputStream inputStream = null;
    354             try {
    355                 synchronized (this) {
    356                     mWaitingForRemote = true;
    357                 }
    358                 try {
    359                     if (V) Log.v(TAG, "put headerset for " + fileInfo.mFileName);
    360                     putOperation = (ClientOperation)mCs.put(request);
    361                 } catch (IOException e) {
    362                     status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
    363                     Constants.updateShareStatus(mContext1, mInfo.mId, status);
    364 
    365                     Log.e(TAG, "Error when put HeaderSet ");
    366                     error = true;
    367                 }
    368                 synchronized (this) {
    369                     mWaitingForRemote = false;
    370                 }
    371 
    372                 if (!error) {
    373                     try {
    374                         if (V) Log.v(TAG, "openOutputStream " + fileInfo.mFileName);
    375                         outputStream = putOperation.openOutputStream();
    376                         inputStream = putOperation.openInputStream();
    377                     } catch (IOException e) {
    378                         status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
    379                         Constants.updateShareStatus(mContext1, mInfo.mId, status);
    380                         Log.e(TAG, "Error when openOutputStream");
    381                         error = true;
    382                     }
    383                 }
    384                 if (!error) {
    385                     updateValues = new ContentValues();
    386                     updateValues.put(BluetoothShare.CURRENT_BYTES, 0);
    387                     updateValues.put(BluetoothShare.STATUS, BluetoothShare.STATUS_RUNNING);
    388                     mContext1.getContentResolver().update(contentUri, updateValues, null, null);
    389                 }
    390 
    391                 if (!error) {
    392                     int position = 0;
    393                     int readLength = 0;
    394                     boolean okToProceed = false;
    395                     long timestamp = 0;
    396                     int outputBufferSize = putOperation.getMaxPacketSize();
    397                     byte[] buffer = new byte[outputBufferSize];
    398                     BufferedInputStream a = new BufferedInputStream(fileInfo.mInputStream, 0x4000);
    399 
    400                     if (!mInterrupted && (position != fileInfo.mLength)) {
    401                         readLength = readFully(a, buffer, outputBufferSize);
    402 
    403                         mCallback.sendMessageDelayed(mCallback
    404                                 .obtainMessage(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT),
    405                                 BluetoothOppObexSession.SESSION_TIMEOUT);
    406                         synchronized (this) {
    407                             mWaitingForRemote = true;
    408                         }
    409 
    410                         // first packet will block here
    411                         outputStream.write(buffer, 0, readLength);
    412 
    413                         position += readLength;
    414 
    415                         if (position != fileInfo.mLength) {
    416                             mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
    417                             synchronized (this) {
    418                                 mWaitingForRemote = false;
    419                             }
    420                         } else {
    421                             // if file length is smaller than buffer size, only one packet
    422                             // so block point is here
    423                             outputStream.close();
    424                             mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
    425                             synchronized (this) {
    426                                 mWaitingForRemote = false;
    427                             }
    428                         }
    429                         /* check remote accept or reject */
    430                         responseCode = putOperation.getResponseCode();
    431 
    432                         if (responseCode == ResponseCodes.OBEX_HTTP_CONTINUE
    433                                 || responseCode == ResponseCodes.OBEX_HTTP_OK) {
    434                             if (V) Log.v(TAG, "Remote accept");
    435                             okToProceed = true;
    436                             updateValues = new ContentValues();
    437                             updateValues.put(BluetoothShare.CURRENT_BYTES, position);
    438                             mContext1.getContentResolver().update(contentUri, updateValues, null,
    439                                     null);
    440                         } else {
    441                             Log.i(TAG, "Remote reject, Response code is " + responseCode);
    442                         }
    443                     }
    444 
    445                     while (!mInterrupted && okToProceed && (position != fileInfo.mLength)) {
    446                         {
    447                             if (V) timestamp = System.currentTimeMillis();
    448 
    449                             readLength = a.read(buffer, 0, outputBufferSize);
    450                             outputStream.write(buffer, 0, readLength);
    451 
    452                             /* check remote abort */
    453                             responseCode = putOperation.getResponseCode();
    454                             if (V) Log.v(TAG, "Response code is " + responseCode);
    455                             if (responseCode != ResponseCodes.OBEX_HTTP_CONTINUE
    456                                     && responseCode != ResponseCodes.OBEX_HTTP_OK) {
    457                                 /* abort happens */
    458                                 okToProceed = false;
    459                             } else {
    460                                 position += readLength;
    461                                 if (V) {
    462                                     Log.v(TAG, "Sending file position = " + position
    463                                             + " readLength " + readLength + " bytes took "
    464                                             + (System.currentTimeMillis() - timestamp) + " ms");
    465                                 }
    466                                 updateValues = new ContentValues();
    467                                 updateValues.put(BluetoothShare.CURRENT_BYTES, position);
    468                                 mContext1.getContentResolver().update(contentUri, updateValues,
    469                                         null, null);
    470                             }
    471                         }
    472                     }
    473 
    474                     if (responseCode == ResponseCodes.OBEX_HTTP_FORBIDDEN
    475                             || responseCode == ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE) {
    476                         Log.i(TAG, "Remote reject file " + fileInfo.mFileName + " length "
    477                                 + fileInfo.mLength);
    478                         status = BluetoothShare.STATUS_FORBIDDEN;
    479                     } else if (responseCode == ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE) {
    480                         Log.i(TAG, "Remote reject file type " + fileInfo.mMimetype);
    481                         status = BluetoothShare.STATUS_NOT_ACCEPTABLE;
    482                     } else if (!mInterrupted && position == fileInfo.mLength) {
    483                         Log.i(TAG, "SendFile finished send out file " + fileInfo.mFileName
    484                                 + " length " + fileInfo.mLength);
    485                         outputStream.close();
    486                     } else {
    487                         error = true;
    488                         status = BluetoothShare.STATUS_CANCELED;
    489                         putOperation.abort();
    490                         /* interrupted */
    491                         Log.i(TAG, "SendFile interrupted when send out file " + fileInfo.mFileName
    492                                 + " at " + position + " of " + fileInfo.mLength);
    493                     }
    494                 }
    495             } catch (IOException e) {
    496                 handleSendException(e.toString());
    497             } catch (NullPointerException e) {
    498                 handleSendException(e.toString());
    499             } catch (IndexOutOfBoundsException e) {
    500                 handleSendException(e.toString());
    501             } finally {
    502                 try {
    503                     fileInfo.mInputStream.close();
    504                     if (!error) {
    505                         responseCode = putOperation.getResponseCode();
    506                         if (responseCode != -1) {
    507                             if (V) Log.v(TAG, "Get response code " + responseCode);
    508                             if (responseCode != ResponseCodes.OBEX_HTTP_OK) {
    509                                 Log.i(TAG, "Response error code is " + responseCode);
    510                                 status = BluetoothShare.STATUS_UNHANDLED_OBEX_CODE;
    511                                 if (responseCode == ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE) {
    512                                     status = BluetoothShare.STATUS_NOT_ACCEPTABLE;
    513                                 }
    514                                 if (responseCode == ResponseCodes.OBEX_HTTP_FORBIDDEN
    515                                         || responseCode == ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE) {
    516                                     status = BluetoothShare.STATUS_FORBIDDEN;
    517                                 }
    518                             }
    519                         } else {
    520                             // responseCode is -1, which means connection error
    521                             status = BluetoothShare.STATUS_CONNECTION_ERROR;
    522                         }
    523                     }
    524 
    525                     Constants.updateShareStatus(mContext1, mInfo.mId, status);
    526 
    527                     if (inputStream != null) {
    528                         inputStream.close();
    529                     }
    530                     if (putOperation != null) {
    531                         putOperation.close();
    532                     }
    533                 } catch (IOException e) {
    534                     Log.e(TAG, "Error when closing stream after send");
    535                 }
    536             }
    537             return status;
    538         }
    539 
    540         private void handleSendException(String exception) {
    541             Log.e(TAG, "Error when sending file: " + exception);
    542             int status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
    543             Constants.updateShareStatus(mContext1, mInfo.mId, status);
    544             mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
    545         }
    546 
    547         @Override
    548         public void interrupt() {
    549             super.interrupt();
    550             synchronized (this) {
    551                 if (mWaitingForRemote) {
    552                     if (V) Log.v(TAG, "Interrupted when waitingForRemote");
    553                     try {
    554                         mTransport1.close();
    555                     } catch (IOException e) {
    556                         Log.e(TAG, "mTransport.close error");
    557                     }
    558                     Message msg = Message.obtain(mCallback);
    559                     msg.what = BluetoothOppObexSession.MSG_SHARE_INTERRUPTED;
    560                     if (mInfo != null) {
    561                         msg.obj = mInfo;
    562                     }
    563                     msg.sendToTarget();
    564                 }
    565             }
    566         }
    567     }
    568 
    569     public static void applyRemoteDeviceQuirks(HeaderSet request, BluetoothOppSendFileInfo info) {
    570         String address = info.mDestAddr;
    571         if (address == null) {
    572             return;
    573         }
    574         if (address.startsWith("00:04:48")) {
    575             // Poloroid Pogo
    576             // Rejects filenames with more than one '.'. Rename to '_'.
    577             // for example: 'a.b.jpg' -> 'a_b.jpg'
    578             //              'abc.jpg' NOT CHANGED
    579             String filename = info.mFileName;
    580 
    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