• 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             long position = 0;
341             int status = BluetoothShare.STATUS_SUCCESS;
342             Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId);
343             ContentValues updateValues;
344             HeaderSet request;
345             request = new HeaderSet();
346             request.setHeader(HeaderSet.NAME, fileInfo.mFileName);
347             request.setHeader(HeaderSet.TYPE, fileInfo.mMimetype);
348 
349             applyRemoteDeviceQuirks(request, mInfo.mDestination, fileInfo.mFileName);
350 
351             Constants.updateShareStatus(mContext1, mInfo.mId, BluetoothShare.STATUS_RUNNING);
352 
353             request.setHeader(HeaderSet.LENGTH, fileInfo.mLength);
354             ClientOperation putOperation = null;
355             OutputStream outputStream = null;
356             InputStream inputStream = null;
357             try {
358                 synchronized (this) {
359                     mWaitingForRemote = true;
360                 }
361                 try {
362                     if (V) Log.v(TAG, "put headerset for " + fileInfo.mFileName);
363                     putOperation = (ClientOperation)mCs.put(request);
364                 } catch (IOException e) {
365                     status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
366                     Constants.updateShareStatus(mContext1, mInfo.mId, status);
367 
368                     Log.e(TAG, "Error when put HeaderSet ");
369                     error = true;
370                 }
371                 synchronized (this) {
372                     mWaitingForRemote = false;
373                 }
374 
375                 if (!error) {
376                     try {
377                         if (V) Log.v(TAG, "openOutputStream " + fileInfo.mFileName);
378                         outputStream = putOperation.openOutputStream();
379                         inputStream = putOperation.openInputStream();
380                     } catch (IOException e) {
381                         status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
382                         Constants.updateShareStatus(mContext1, mInfo.mId, status);
383                         Log.e(TAG, "Error when openOutputStream");
384                         error = true;
385                     }
386                 }
387                 if (!error) {
388                     updateValues = new ContentValues();
389                     updateValues.put(BluetoothShare.CURRENT_BYTES, 0);
390                     updateValues.put(BluetoothShare.STATUS, BluetoothShare.STATUS_RUNNING);
391                     mContext1.getContentResolver().update(contentUri, updateValues, null, null);
392                 }
393 
394                 if (!error) {
395                     int readLength = 0;
396                     long percent = 0;
397                     long prevPercent = 0;
398                     boolean okToProceed = false;
399                     long timestamp = 0;
400                     int outputBufferSize = putOperation.getMaxPacketSize();
401                     byte[] buffer = new byte[outputBufferSize];
402                     BufferedInputStream a = new BufferedInputStream(fileInfo.mInputStream, 0x4000);
403 
404                     if (!mInterrupted && (position != fileInfo.mLength)) {
405                         readLength = readFully(a, buffer, outputBufferSize);
406 
407                         mCallback.sendMessageDelayed(mCallback
408                                 .obtainMessage(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT),
409                                 BluetoothOppObexSession.SESSION_TIMEOUT);
410                         synchronized (this) {
411                             mWaitingForRemote = true;
412                         }
413 
414                         // first packet will block here
415                         outputStream.write(buffer, 0, readLength);
416 
417                         position += readLength;
418                         /* check remote accept or reject */
419                         responseCode = putOperation.getResponseCode();
420 
421                         mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
422                         synchronized (this) {
423                             mWaitingForRemote = false;
424                         }
425 
426                         if (responseCode == ResponseCodes.OBEX_HTTP_CONTINUE
427                                 || responseCode == ResponseCodes.OBEX_HTTP_OK) {
428                             if (V) Log.v(TAG, "Remote accept");
429                             okToProceed = true;
430                             updateValues = new ContentValues();
431                             updateValues.put(BluetoothShare.CURRENT_BYTES, position);
432                             mContext1.getContentResolver().update(contentUri, updateValues, null,
433                                     null);
434                         } else {
435                             Log.i(TAG, "Remote reject, Response code is " + responseCode);
436                         }
437                     }
438 
439                     while (!mInterrupted && okToProceed && (position < fileInfo.mLength)) {
440                         if (V) timestamp = System.currentTimeMillis();
441 
442                         readLength = a.read(buffer, 0, outputBufferSize);
443                         outputStream.write(buffer, 0, readLength);
444 
445                         /* check remote abort */
446                         responseCode = putOperation.getResponseCode();
447                         if (V) Log.v(TAG, "Response code is " + responseCode);
448                         if (responseCode != ResponseCodes.OBEX_HTTP_CONTINUE
449                                 && responseCode != ResponseCodes.OBEX_HTTP_OK) {
450                             /* abort happens */
451                             okToProceed = false;
452                         } else {
453                             position += readLength;
454                             if (V) {
455                                 Log.v(TAG, "Sending file position = " + position
456                                         + " readLength " + readLength + " bytes took "
457                                         + (System.currentTimeMillis() - timestamp) + " ms");
458                             }
459                             // Update the Progress Bar only if there is change in percentage
460                             percent = position * 100 / fileInfo.mLength;
461                             if (percent > prevPercent) {
462                                 updateValues = new ContentValues();
463                                 updateValues.put(BluetoothShare.CURRENT_BYTES, position);
464                                 mContext1.getContentResolver().update(contentUri, updateValues,
465                                         null, null);
466                                 prevPercent = percent;
467                             }
468                         }
469                     }
470 
471                     if (responseCode == ResponseCodes.OBEX_HTTP_FORBIDDEN
472                             || responseCode == ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE) {
473                         Log.i(TAG, "Remote reject file " + fileInfo.mFileName + " length "
474                                 + fileInfo.mLength);
475                         status = BluetoothShare.STATUS_FORBIDDEN;
476                     } else if (responseCode == ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE) {
477                         Log.i(TAG, "Remote reject file type " + fileInfo.mMimetype);
478                         status = BluetoothShare.STATUS_NOT_ACCEPTABLE;
479                     } else if (!mInterrupted && position == fileInfo.mLength) {
480                         Log.i(TAG, "SendFile finished send out file " + fileInfo.mFileName
481                                 + " length " + fileInfo.mLength);
482                     } else {
483                         error = true;
484                         status = BluetoothShare.STATUS_CANCELED;
485                         putOperation.abort();
486                         /* interrupted */
487                         Log.i(TAG, "SendFile interrupted when send out file " + fileInfo.mFileName
488                                 + " at " + position + " of " + fileInfo.mLength);
489                     }
490                 }
491             } catch (IOException e) {
492                 handleSendException(e.toString());
493             } catch (NullPointerException e) {
494                 handleSendException(e.toString());
495             } catch (IndexOutOfBoundsException e) {
496                 handleSendException(e.toString());
497             } finally {
498                 try {
499                     if (outputStream != null) {
500                         outputStream.close();
501                     }
502 
503                     // Close InputStream and remove SendFileInfo from map
504                     BluetoothOppUtility.closeSendFileInfo(mInfo.mUri);
505                     if (!error) {
506                         responseCode = putOperation.getResponseCode();
507                         if (responseCode != -1) {
508                             if (V) Log.v(TAG, "Get response code " + responseCode);
509                             if (responseCode != ResponseCodes.OBEX_HTTP_OK) {
510                                 Log.i(TAG, "Response error code is " + responseCode);
511                                 status = BluetoothShare.STATUS_UNHANDLED_OBEX_CODE;
512                                 if (responseCode == ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE) {
513                                     status = BluetoothShare.STATUS_NOT_ACCEPTABLE;
514                                 }
515                                 if (responseCode == ResponseCodes.OBEX_HTTP_FORBIDDEN
516                                         || responseCode == ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE) {
517                                     status = BluetoothShare.STATUS_FORBIDDEN;
518                                 }
519                             }
520                         } else {
521                             // responseCode is -1, which means connection error
522                             status = BluetoothShare.STATUS_CONNECTION_ERROR;
523                         }
524                     }
525 
526                     Constants.updateShareStatus(mContext1, mInfo.mId, status);
527 
528                     if (inputStream != null) {
529                         inputStream.close();
530                     }
531                     if (putOperation != null) {
532                         putOperation.close();
533                     }
534                 } catch (IOException e) {
535                     Log.e(TAG, "Error when closing stream after send");
536 
537                     // Socket has been closed due to the response timeout in the framework,
538                     // mark the transfer as failure.
539                     if (position != fileInfo.mLength) {
540                        status = BluetoothShare.STATUS_FORBIDDEN;
541                        Constants.updateShareStatus(mContext1, mInfo.mId, status);
542                     }
543                 }
544             }
545             return status;
546         }
547 
handleSendException(String exception)548         private void handleSendException(String exception) {
549             Log.e(TAG, "Error when sending file: " + exception);
550             // Update interrupted outbound content resolver entry when
551             // error during transfer.
552             Constants.updateShareStatus(mContext1, mInfo.mId,
553                 BluetoothShare.STATUS_OBEX_DATA_ERROR);
554             mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
555         }
556 
557         @Override
interrupt()558         public void interrupt() {
559             super.interrupt();
560             synchronized (this) {
561                 if (mWaitingForRemote) {
562                     if (V) Log.v(TAG, "Interrupted when waitingForRemote");
563                     try {
564                         mTransport1.close();
565                     } catch (IOException e) {
566                         Log.e(TAG, "mTransport.close error");
567                     }
568                     Message msg = Message.obtain(mCallback);
569                     msg.what = BluetoothOppObexSession.MSG_SHARE_INTERRUPTED;
570                     if (mInfo != null) {
571                         msg.obj = mInfo;
572                     }
573                     msg.sendToTarget();
574                 }
575             }
576         }
577     }
578 
applyRemoteDeviceQuirks(HeaderSet request, String address, String filename)579     public static void applyRemoteDeviceQuirks(HeaderSet request, String address, String filename) {
580         if (address == null) {
581             return;
582         }
583         if (address.startsWith("00:04:48")) {
584             // Poloroid Pogo
585             // Rejects filenames with more than one '.'. Rename to '_'.
586             // for example: 'a.b.jpg' -> 'a_b.jpg'
587             //              'abc.jpg' NOT CHANGED
588             char[] c = filename.toCharArray();
589             boolean firstDot = true;
590             boolean modified = false;
591             for (int i = c.length - 1; i >= 0; i--) {
592                 if (c[i] == '.') {
593                     if (!firstDot) {
594                         modified = true;
595                         c[i] = '_';
596                     }
597                     firstDot = false;
598                 }
599             }
600 
601             if (modified) {
602                 String newFilename = new String(c);
603                 request.setHeader(HeaderSet.NAME, newFilename);
604                 Log.i(TAG, "Sending file \"" + filename + "\" as \"" + newFilename +
605                         "\" to workaround Poloroid filename quirk");
606             }
607         }
608     }
609 
unblock()610     public void unblock() {
611         // Not used for client case
612     }
613 
614 }
615