• 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.app.NotificationManager;
42 import android.content.ContentValues;
43 import android.content.Context;
44 import android.net.Uri;
45 import android.os.Handler;
46 import android.os.Message;
47 import android.os.PowerManager;
48 import android.os.PowerManager.WakeLock;
49 import android.os.Process;
50 import android.util.Log;
51 
52 import java.io.BufferedInputStream;
53 import java.io.IOException;
54 import java.io.InputStream;
55 import java.io.OutputStream;
56 import java.lang.Thread;
57 
58 /**
59  * This class runs as an OBEX client
60  */
61 public class BluetoothOppObexClientSession implements BluetoothOppObexSession {
62 
63     private static final String TAG = "BtOppObexClient";
64     private static final boolean D = Constants.DEBUG;
65     private static final boolean V = Constants.VERBOSE;
66 
67     private ClientThread mThread;
68 
69     private ObexTransport mTransport;
70 
71     private Context mContext;
72 
73     private volatile boolean mInterrupted;
74 
75     private volatile boolean mWaitingForRemote;
76 
77     private Handler mCallback;
78 
BluetoothOppObexClientSession(Context context, ObexTransport transport)79     public BluetoothOppObexClientSession(Context context, ObexTransport transport) {
80         if (transport == null) {
81             throw new NullPointerException("transport is null");
82         }
83         mContext = context;
84         mTransport = transport;
85     }
86 
start(Handler handler, int numShares)87     public void start(Handler handler, int numShares) {
88         if (D) Log.d(TAG, "Start!");
89         mCallback = handler;
90         mThread = new ClientThread(mContext, mTransport, numShares);
91         mThread.start();
92     }
93 
stop()94     public void stop() {
95         if (D) Log.d(TAG, "Stop!");
96         if (mThread != null) {
97             mInterrupted = true;
98             try {
99                 mThread.interrupt();
100                 if (V) Log.v(TAG, "waiting for thread to terminate");
101                 mThread.join();
102                 mThread = null;
103             } catch (InterruptedException e) {
104                 if (V) Log.v(TAG, "Interrupted waiting for thread to join");
105             }
106         }
107         NotificationManager nm =
108                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
109         nm.cancel(BluetoothOppNotification.NOTIFICATION_ID_PROGRESS);
110 
111         mCallback = null;
112     }
113 
addShare(BluetoothOppShareInfo share)114     public void addShare(BluetoothOppShareInfo share) {
115         mThread.addShare(share);
116     }
117 
readFully(InputStream is, byte[] buffer, int size)118     private static int readFully(InputStream is, byte[] buffer, int size) throws IOException {
119         int done = 0;
120         while (done < size) {
121             int got = is.read(buffer, done, size - done);
122             if (got <= 0) break;
123             done += got;
124         }
125         return done;
126     }
127 
128     private class ClientThread extends Thread {
129 
130         private static final int sSleepTime = 500;
131 
132         private Context mContext1;
133 
134         private BluetoothOppShareInfo mInfo;
135 
136         private volatile boolean waitingForShare;
137 
138         private ObexTransport mTransport1;
139 
140         private ClientSession mCs;
141 
142         private WakeLock wakeLock;
143 
144         private BluetoothOppSendFileInfo mFileInfo = null;
145 
146         private boolean mConnected = false;
147 
148         private int mNumShares;
149 
ClientThread(Context context, ObexTransport transport, int initialNumShares)150         public ClientThread(Context context, ObexTransport transport, int initialNumShares) {
151             super("BtOpp ClientThread");
152             mContext1 = context;
153             mTransport1 = transport;
154             waitingForShare = true;
155             mWaitingForRemote = false;
156             mNumShares = initialNumShares;
157             PowerManager pm = (PowerManager)mContext1.getSystemService(Context.POWER_SERVICE);
158             wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
159         }
160 
addShare(BluetoothOppShareInfo info)161         public void addShare(BluetoothOppShareInfo info) {
162             mInfo = info;
163             mFileInfo = processShareInfo();
164             waitingForShare = false;
165         }
166 
167         @Override
run()168         public void run() {
169             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
170 
171             if (V) Log.v(TAG, "acquire partial WakeLock");
172             wakeLock.acquire();
173 
174             try {
175                 Thread.sleep(100);
176             } catch (InterruptedException e1) {
177                 if (V) Log.v(TAG, "Client thread was interrupted (1), exiting");
178                 mInterrupted = true;
179             }
180             if (!mInterrupted) {
181                 connect(mNumShares);
182             }
183 
184             while (!mInterrupted) {
185                 if (!waitingForShare) {
186                     doSend();
187                 } else {
188                     try {
189                         if (D) Log.d(TAG, "Client thread waiting for next share, sleep for "
190                                     + sSleepTime);
191                         Thread.sleep(sSleepTime);
192                     } catch (InterruptedException e) {
193 
194                     }
195                 }
196             }
197             disconnect();
198 
199             if (wakeLock.isHeld()) {
200                 if (V) Log.v(TAG, "release partial WakeLock");
201                 wakeLock.release();
202             }
203             Message msg = Message.obtain(mCallback);
204             msg.what = BluetoothOppObexSession.MSG_SESSION_COMPLETE;
205             msg.obj = mInfo;
206             msg.sendToTarget();
207 
208         }
209 
disconnect()210         private void disconnect() {
211             try {
212                 if (mCs != null) {
213                     mCs.disconnect(null);
214                 }
215                 mCs = null;
216                 if (D) Log.d(TAG, "OBEX session disconnected");
217             } catch (IOException e) {
218                 Log.w(TAG, "OBEX session disconnect error" + e);
219             }
220             try {
221                 if (mCs != null) {
222                     if (D) Log.d(TAG, "OBEX session close mCs");
223                     mCs.close();
224                     if (D) Log.d(TAG, "OBEX session closed");
225                     }
226             } catch (IOException e) {
227                 Log.w(TAG, "OBEX session close error" + e);
228             }
229             if (mTransport1 != null) {
230                 try {
231                     mTransport1.close();
232                 } catch (IOException e) {
233                     Log.e(TAG, "mTransport.close error");
234                 }
235 
236             }
237         }
238 
connect(int numShares)239         private void connect(int numShares) {
240             if (D) Log.d(TAG, "Create ClientSession with transport " + mTransport1.toString());
241             try {
242                 mCs = new ClientSession(mTransport1);
243                 mConnected = true;
244             } catch (IOException e1) {
245                 Log.e(TAG, "OBEX session create error");
246             }
247             if (mConnected) {
248                 mConnected = false;
249                 HeaderSet hs = new HeaderSet();
250                 hs.setHeader(HeaderSet.COUNT, (long) numShares);
251                 synchronized (this) {
252                     mWaitingForRemote = true;
253                 }
254                 try {
255                     mCs.connect(hs);
256                     if (D) Log.d(TAG, "OBEX session created");
257                     mConnected = true;
258                 } catch (IOException e) {
259                     Log.e(TAG, "OBEX session connect error");
260                 }
261             }
262             synchronized (this) {
263                 mWaitingForRemote = false;
264             }
265         }
266 
doSend()267         private void doSend() {
268 
269             int status = BluetoothShare.STATUS_SUCCESS;
270 
271             /* connection is established too fast to get first mInfo */
272             while (mFileInfo == null) {
273                 try {
274                     Thread.sleep(50);
275                 } catch (InterruptedException e) {
276                     status = BluetoothShare.STATUS_CANCELED;
277                 }
278             }
279             if (!mConnected) {
280                 // Obex connection error
281                 status = BluetoothShare.STATUS_CONNECTION_ERROR;
282             }
283             if (status == BluetoothShare.STATUS_SUCCESS) {
284                 /* do real send */
285                 if (mFileInfo.mFileName != null) {
286                     status = sendFile(mFileInfo);
287                 } else {
288                     /* this is invalid request */
289                     status = mFileInfo.mStatus;
290                 }
291                 waitingForShare = true;
292             } else {
293                 Constants.updateShareStatus(mContext1, mInfo.mId, status);
294             }
295 
296             Message msg = Message.obtain(mCallback);
297             msg.what = (status == BluetoothShare.STATUS_SUCCESS) ?
298                         BluetoothOppObexSession.MSG_SHARE_COMPLETE :
299                         BluetoothOppObexSession.MSG_SESSION_ERROR;
300             mInfo.mStatus = status;
301             msg.obj = mInfo;
302             msg.sendToTarget();
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                         if (position == fileInfo.mLength) {
422                             // if file length is smaller than buffer size, only one packet
423                             // so block point is here
424                             outputStream.close();
425                             outputStream = null;
426                         }
427 
428                         mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
429                         synchronized (this) {
430                             mWaitingForRemote = false;
431                         }
432 
433                         if (responseCode == ResponseCodes.OBEX_HTTP_CONTINUE
434                                 || responseCode == ResponseCodes.OBEX_HTTP_OK) {
435                             if (V) Log.v(TAG, "Remote accept");
436                             okToProceed = true;
437                             updateValues = new ContentValues();
438                             updateValues.put(BluetoothShare.CURRENT_BYTES, position);
439                             mContext1.getContentResolver().update(contentUri, updateValues, null,
440                                     null);
441                         } else {
442                             Log.i(TAG, "Remote reject, Response code is " + responseCode);
443                         }
444                     }
445 
446                     while (!mInterrupted && okToProceed && (position < fileInfo.mLength)) {
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                             // Update the Progress Bar only if there is change in percentage
467                             percent = position * 100 / fileInfo.mLength;
468                             if (percent > prevPercent) {
469                                 updateValues = new ContentValues();
470                                 updateValues.put(BluetoothShare.CURRENT_BYTES, position);
471                                 mContext1.getContentResolver().update(contentUri, updateValues,
472                                         null, null);
473                                 prevPercent = percent;
474                             }
475                         }
476                     }
477 
478                     if (responseCode == ResponseCodes.OBEX_HTTP_FORBIDDEN
479                             || responseCode == ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE) {
480                         Log.i(TAG, "Remote reject file " + fileInfo.mFileName + " length "
481                                 + fileInfo.mLength);
482                         status = BluetoothShare.STATUS_FORBIDDEN;
483                     } else if (responseCode == ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE) {
484                         Log.i(TAG, "Remote reject file type " + fileInfo.mMimetype);
485                         status = BluetoothShare.STATUS_NOT_ACCEPTABLE;
486                     } else if (!mInterrupted && position == fileInfo.mLength) {
487                         Log.i(TAG, "SendFile finished send out file " + fileInfo.mFileName
488                                 + " length " + fileInfo.mLength);
489                     } else {
490                         error = true;
491                         status = BluetoothShare.STATUS_CANCELED;
492                         putOperation.abort();
493                         /* interrupted */
494                         Log.i(TAG, "SendFile interrupted when send out file " + fileInfo.mFileName
495                                 + " at " + position + " of " + fileInfo.mLength);
496                     }
497                 }
498             } catch (IOException e) {
499                 handleSendException(e.toString());
500             } catch (NullPointerException e) {
501                 handleSendException(e.toString());
502             } catch (IndexOutOfBoundsException e) {
503                 handleSendException(e.toString());
504             } finally {
505                 try {
506                     if (outputStream != null) {
507                         outputStream.close();
508                     }
509 
510                     // Close InputStream and remove SendFileInfo from map
511                     BluetoothOppUtility.closeSendFileInfo(mInfo.mUri);
512                     if (!error) {
513                         responseCode = putOperation.getResponseCode();
514                         if (responseCode != -1) {
515                             if (V) Log.v(TAG, "Get response code " + responseCode);
516                             if (responseCode != ResponseCodes.OBEX_HTTP_OK) {
517                                 Log.i(TAG, "Response error code is " + responseCode);
518                                 status = BluetoothShare.STATUS_UNHANDLED_OBEX_CODE;
519                                 if (responseCode == ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE) {
520                                     status = BluetoothShare.STATUS_NOT_ACCEPTABLE;
521                                 }
522                                 if (responseCode == ResponseCodes.OBEX_HTTP_FORBIDDEN
523                                         || responseCode == ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE) {
524                                     status = BluetoothShare.STATUS_FORBIDDEN;
525                                 }
526                             }
527                         } else {
528                             // responseCode is -1, which means connection error
529                             status = BluetoothShare.STATUS_CONNECTION_ERROR;
530                         }
531                     }
532 
533                     Constants.updateShareStatus(mContext1, mInfo.mId, status);
534 
535                     if (inputStream != null) {
536                         inputStream.close();
537                     }
538                     if (putOperation != null) {
539                         putOperation.close();
540                     }
541                 } catch (IOException e) {
542                     Log.e(TAG, "Error when closing stream after send");
543 
544                     // Socket has been closed due to the response timeout in the framework,
545                     // mark the transfer as failure.
546                     if (position != fileInfo.mLength) {
547                        status = BluetoothShare.STATUS_FORBIDDEN;
548                        Constants.updateShareStatus(mContext1, mInfo.mId, status);
549                     }
550                 }
551             }
552             return status;
553         }
554 
handleSendException(String exception)555         private void handleSendException(String exception) {
556             Log.e(TAG, "Error when sending file: " + exception);
557             // Update interrupted outbound content resolver entry when
558             // error during transfer.
559             Constants.updateShareStatus(mContext1, mInfo.mId,
560                 BluetoothShare.STATUS_OBEX_DATA_ERROR);
561             mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
562         }
563 
564         @Override
interrupt()565         public void interrupt() {
566             super.interrupt();
567             synchronized (this) {
568                 if (mWaitingForRemote) {
569                     if (V) Log.v(TAG, "Interrupted when waitingForRemote");
570                     try {
571                         mTransport1.close();
572                     } catch (IOException e) {
573                         Log.e(TAG, "mTransport.close error");
574                     }
575                     Message msg = Message.obtain(mCallback);
576                     msg.what = BluetoothOppObexSession.MSG_SHARE_INTERRUPTED;
577                     if (mInfo != null) {
578                         msg.obj = mInfo;
579                     }
580                     msg.sendToTarget();
581                 }
582             }
583         }
584     }
585 
applyRemoteDeviceQuirks(HeaderSet request, String address, String filename)586     public static void applyRemoteDeviceQuirks(HeaderSet request, String address, String filename) {
587         if (address == null) {
588             return;
589         }
590         if (address.startsWith("00:04:48")) {
591             // Poloroid Pogo
592             // Rejects filenames with more than one '.'. Rename to '_'.
593             // for example: 'a.b.jpg' -> 'a_b.jpg'
594             //              'abc.jpg' NOT CHANGED
595             char[] c = filename.toCharArray();
596             boolean firstDot = true;
597             boolean modified = false;
598             for (int i = c.length - 1; i >= 0; i--) {
599                 if (c[i] == '.') {
600                     if (!firstDot) {
601                         modified = true;
602                         c[i] = '_';
603                     }
604                     firstDot = false;
605                 }
606             }
607 
608             if (modified) {
609                 String newFilename = new String(c);
610                 request.setHeader(HeaderSet.NAME, newFilename);
611                 Log.i(TAG, "Sending file \"" + filename + "\" as \"" + newFilename +
612                         "\" to workaround Poloroid filename quirk");
613             }
614         }
615     }
616 
unblock()617     public void unblock() {
618         // Not used for client case
619     }
620 
621 }
622