BluetoothOppObexClientSession.java revision e026ad5004a084c95b86a061924c692546a10395
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                        mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
420                        synchronized (this) {
421                            mWaitingForRemote = false;
422                        }
423                        /* check remote accept or reject */
424                        responseCode = putOperation.getResponseCode();
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
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
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
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
610    public void unblock() {
611        // Not used for client case
612    }
613
614}
615