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