• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2008-2009, Motorola, Inc.
3  *
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  *
9  * - Redistributions of source code must retain the above copyright notice,
10  * this list of conditions and the following disclaimer.
11  *
12  * - Redistributions in binary form must reproduce the above copyright notice,
13  * this list of conditions and the following disclaimer in the documentation
14  * and/or other materials provided with the distribution.
15  *
16  * - Neither the name of the Motorola, Inc. nor the names of its contributors
17  * may be used to endorse or promote products derived from this software
18  * without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30  * POSSIBILITY OF SUCH DAMAGE.
31  */
32 
33 package com.android.bluetooth.opp;
34 
35 import 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 
BluetoothOppObexClientSession(Context context, ObexTransport transport)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 
start(Handler handler, int numShares)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 
stop()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 
addShare(BluetoothOppShareInfo share)109     public void addShare(BluetoothOppShareInfo share) {
110         mThread.addShare(share);
111     }
112 
readFully(InputStream is, byte[] buffer, int size)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 
ClientThread(Context context, ObexTransport transport, int initialNumShares)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 
addShare(BluetoothOppShareInfo info)156         public void addShare(BluetoothOppShareInfo info) {
157             mInfo = info;
158             mFileInfo = processShareInfo();
159             waitingForShare = false;
160         }
161 
162         @Override
run()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 
disconnect()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 
connect(int numShares)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 
doSend()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          */
processShareInfo()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 
sendFile(BluetoothOppSendFileInfo fileInfo)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 
handleSendException(String exception)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
interrupt()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 
applyRemoteDeviceQuirks(HeaderSet request, String address, String filename)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 
unblock()603     public void unblock() {
604         // Not used for client case
605     }
606 
607 }
608