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