BluetoothOppObexClientSession.java revision cb136395d7977aaa85ee0840a976c77ca811647f
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 = "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
78    public BluetoothOppObexClientSession(Context context, ObexTransport transport) {
79        if (transport == null) {
80            throw new NullPointerException("transport is null");
81        }
82        mContext = context;
83        mTransport = transport;
84    }
85
86    public void start(Handler handler, int numShares) {
87        if (D) Log.d(TAG, "Start!");
88        mCallback = handler;
89        mThread = new ClientThread(mContext, mTransport, numShares);
90        mThread.start();
91    }
92
93    public void stop() {
94        if (D) Log.d(TAG, "Stop!");
95        if (mThread != null) {
96            mInterrupted = true;
97            try {
98                mThread.interrupt();
99                if (V) Log.v(TAG, "waiting for thread to terminate");
100                mThread.join();
101                mThread = null;
102            } catch (InterruptedException e) {
103                if (V) Log.v(TAG, "Interrupted waiting for thread to join");
104            }
105        }
106        mCallback = null;
107    }
108
109    public void addShare(BluetoothOppShareInfo share) {
110        mThread.addShare(share);
111    }
112
113    private 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
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
156        public void addShare(BluetoothOppShareInfo info) {
157            mInfo = info;
158            mFileInfo = processShareInfo();
159            waitingForShare = false;
160        }
161
162        @Override
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
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
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
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         */
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
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
419                        if (position != fileInfo.mLength) {
420                            mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
421                            synchronized (this) {
422                                mWaitingForRemote = false;
423                            }
424                        } else {
425                            // if file length is smaller than buffer size, only one packet
426                            // so block point is here
427                            outputStream.close();
428                            mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
429                            synchronized (this) {
430                                mWaitingForRemote = false;
431                            }
432                        }
433                        /* check remote accept or reject */
434                        responseCode = putOperation.getResponseCode();
435
436                        if (responseCode == ResponseCodes.OBEX_HTTP_CONTINUE
437                                || responseCode == ResponseCodes.OBEX_HTTP_OK) {
438                            if (V) Log.v(TAG, "Remote accept");
439                            okToProceed = true;
440                            updateValues = new ContentValues();
441                            updateValues.put(BluetoothShare.CURRENT_BYTES, position);
442                            mContext1.getContentResolver().update(contentUri, updateValues, null,
443                                    null);
444                        } else {
445                            Log.i(TAG, "Remote reject, Response code is " + responseCode);
446                        }
447                    }
448
449                    while (!mInterrupted && okToProceed && (position != fileInfo.mLength)) {
450                        {
451                            if (V) timestamp = System.currentTimeMillis();
452
453                            readLength = a.read(buffer, 0, outputBufferSize);
454                            outputStream.write(buffer, 0, readLength);
455
456                            /* check remote abort */
457                            responseCode = putOperation.getResponseCode();
458                            if (V) Log.v(TAG, "Response code is " + responseCode);
459                            if (responseCode != ResponseCodes.OBEX_HTTP_CONTINUE
460                                    && responseCode != ResponseCodes.OBEX_HTTP_OK) {
461                                /* abort happens */
462                                okToProceed = false;
463                            } else {
464                                position += readLength;
465                                if (V) {
466                                    Log.v(TAG, "Sending file position = " + position
467                                            + " readLength " + readLength + " bytes took "
468                                            + (System.currentTimeMillis() - timestamp) + " ms");
469                                }
470                                // Update the Progress Bar only if there is change in percentage
471                                percent = position * 100 / fileInfo.mLength;
472                                if (percent > prevPercent) {
473                                    updateValues = new ContentValues();
474                                    updateValues.put(BluetoothShare.CURRENT_BYTES, position);
475                                    mContext1.getContentResolver().update(contentUri, updateValues,
476                                            null, null);
477                                    prevPercent = percent;
478                                }
479                            }
480                        }
481                    }
482
483                    if (responseCode == ResponseCodes.OBEX_HTTP_FORBIDDEN
484                            || responseCode == ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE) {
485                        Log.i(TAG, "Remote reject file " + fileInfo.mFileName + " length "
486                                + fileInfo.mLength);
487                        status = BluetoothShare.STATUS_FORBIDDEN;
488                    } else if (responseCode == ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE) {
489                        Log.i(TAG, "Remote reject file type " + fileInfo.mMimetype);
490                        status = BluetoothShare.STATUS_NOT_ACCEPTABLE;
491                    } else if (!mInterrupted && position == fileInfo.mLength) {
492                        Log.i(TAG, "SendFile finished send out file " + fileInfo.mFileName
493                                + " length " + fileInfo.mLength);
494                        outputStream.close();
495                    } else {
496                        error = true;
497                        status = BluetoothShare.STATUS_CANCELED;
498                        putOperation.abort();
499                        /* interrupted */
500                        Log.i(TAG, "SendFile interrupted when send out file " + fileInfo.mFileName
501                                + " at " + position + " of " + fileInfo.mLength);
502                    }
503                }
504            } catch (IOException e) {
505                handleSendException(e.toString());
506            } catch (NullPointerException e) {
507                handleSendException(e.toString());
508            } catch (IndexOutOfBoundsException e) {
509                handleSendException(e.toString());
510            } finally {
511                try {
512                    // Close InputStream and remove SendFileInfo from map
513                    BluetoothOppUtility.closeSendFileInfo(mInfo.mUri);
514                    if (!error) {
515                        responseCode = putOperation.getResponseCode();
516                        if (responseCode != -1) {
517                            if (V) Log.v(TAG, "Get response code " + responseCode);
518                            if (responseCode != ResponseCodes.OBEX_HTTP_OK) {
519                                Log.i(TAG, "Response error code is " + responseCode);
520                                status = BluetoothShare.STATUS_UNHANDLED_OBEX_CODE;
521                                if (responseCode == ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE) {
522                                    status = BluetoothShare.STATUS_NOT_ACCEPTABLE;
523                                }
524                                if (responseCode == ResponseCodes.OBEX_HTTP_FORBIDDEN
525                                        || responseCode == ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE) {
526                                    status = BluetoothShare.STATUS_FORBIDDEN;
527                                }
528                            }
529                        } else {
530                            // responseCode is -1, which means connection error
531                            status = BluetoothShare.STATUS_CONNECTION_ERROR;
532                        }
533                    }
534
535                    Constants.updateShareStatus(mContext1, mInfo.mId, status);
536
537                    if (inputStream != null) {
538                        inputStream.close();
539                    }
540                    if (putOperation != null) {
541                        putOperation.close();
542                    }
543                } catch (IOException e) {
544                    Log.e(TAG, "Error when closing stream after send");
545
546                    // Socket has been closed due to the response timeout in the framework,
547                    // mark the transfer as failure.
548                    if (position != fileInfo.mLength) {
549                       status = BluetoothShare.STATUS_FORBIDDEN;
550                       Constants.updateShareStatus(mContext1, mInfo.mId, status);
551                    }
552                }
553            }
554            return status;
555        }
556
557        private void handleSendException(String exception) {
558            Log.e(TAG, "Error when sending file: " + exception);
559            // Update interrupted outbound content resolver entry when
560            // error during transfer.
561            Constants.updateShareStatus(mContext1, mInfo.mId,
562                BluetoothShare.STATUS_OBEX_DATA_ERROR);
563            mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
564        }
565
566        @Override
567        public void interrupt() {
568            super.interrupt();
569            synchronized (this) {
570                if (mWaitingForRemote) {
571                    if (V) Log.v(TAG, "Interrupted when waitingForRemote");
572                    try {
573                        mTransport1.close();
574                    } catch (IOException e) {
575                        Log.e(TAG, "mTransport.close error");
576                    }
577                    Message msg = Message.obtain(mCallback);
578                    msg.what = BluetoothOppObexSession.MSG_SHARE_INTERRUPTED;
579                    if (mInfo != null) {
580                        msg.obj = mInfo;
581                    }
582                    msg.sendToTarget();
583                }
584            }
585        }
586    }
587
588    public static void applyRemoteDeviceQuirks(HeaderSet request, String address, String filename) {
589        if (address == null) {
590            return;
591        }
592        if (address.startsWith("00:04:48")) {
593            // Poloroid Pogo
594            // Rejects filenames with more than one '.'. Rename to '_'.
595            // for example: 'a.b.jpg' -> 'a_b.jpg'
596            //              'abc.jpg' NOT CHANGED
597            char[] c = filename.toCharArray();
598            boolean firstDot = true;
599            boolean modified = false;
600            for (int i = c.length - 1; i >= 0; i--) {
601                if (c[i] == '.') {
602                    if (!firstDot) {
603                        modified = true;
604                        c[i] = '_';
605                    }
606                    firstDot = false;
607                }
608            }
609
610            if (modified) {
611                String newFilename = new String(c);
612                request.setHeader(HeaderSet.NAME, newFilename);
613                Log.i(TAG, "Sending file \"" + filename + "\" as \"" + newFilename +
614                        "\" to workaround Poloroid filename quirk");
615            }
616        }
617    }
618
619    public void unblock() {
620        // Not used for client case
621    }
622
623}
624