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