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