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