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