BluetoothOppObexServerSession.java revision 32e47df203390052c1ef771d78b22fc4aa5f9e72
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 java.io.BufferedOutputStream;
36import java.io.File;
37import java.io.IOException;
38import java.io.InputStream;
39
40import android.content.ContentValues;
41import android.content.Context;
42import android.content.Intent;
43import android.net.Uri;
44import android.os.Handler;
45import android.os.Message;
46import android.os.PowerManager;
47import android.os.PowerManager.WakeLock;
48import android.util.Log;
49import android.webkit.MimeTypeMap;
50
51import javax.obex.HeaderSet;
52import javax.obex.ObexTransport;
53import javax.obex.Operation;
54import javax.obex.ResponseCodes;
55import javax.obex.ServerRequestHandler;
56import javax.obex.ServerSession;
57
58/**
59 * This class runs as an OBEX server
60 */
61public class BluetoothOppObexServerSession extends ServerRequestHandler implements
62        BluetoothOppObexSession {
63
64    private static final String TAG = "BtOppObexServer";
65    private static final boolean D = Constants.DEBUG;
66    private static final boolean V = Constants.VERBOSE;
67
68    private ObexTransport mTransport;
69
70    private Context mContext;
71
72    private Handler mCallback = null;
73
74    /* status when server is blocking for user/auto confirmation */
75    private boolean mServerBlocking = true;
76
77    /* the current transfer info */
78    private BluetoothOppShareInfo mInfo;
79
80    /* info id when we insert the record */
81    private int mLocalShareInfoId;
82
83    private int mAccepted = BluetoothShare.USER_CONFIRMATION_PENDING;
84
85    private boolean mInterrupted = false;
86
87    private ServerSession mSession;
88
89    private long mTimestamp;
90
91    private BluetoothOppReceiveFileInfo mFileInfo;
92
93    private WakeLock mWakeLock;
94
95    private WakeLock mPartialWakeLock;
96
97    boolean mTimeoutMsgSent = false;
98
99    public BluetoothOppObexServerSession(Context context, ObexTransport transport) {
100        mContext = context;
101        mTransport = transport;
102        PowerManager pm = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE);
103        mWakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP
104                | PowerManager.ON_AFTER_RELEASE, TAG);
105        mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
106    }
107
108    public void unblock() {
109        mServerBlocking = false;
110    }
111
112    /**
113     * Called when connection is accepted from remote, to retrieve the first
114     * Header then wait for user confirmation
115     */
116    public void preStart() {
117        if (D) Log.d(TAG, "acquire full WakeLock");
118        mWakeLock.acquire();
119        try {
120            if (D) Log.d(TAG, "Create ServerSession with transport " + mTransport.toString());
121            mSession = new ServerSession(mTransport, this, null);
122        } catch (IOException e) {
123            Log.e(TAG, "Create server session error" + e);
124        }
125    }
126
127    /**
128     * Called from BluetoothOppTransfer to start the "Transfer"
129     */
130    public void start(Handler handler) {
131        if (D) Log.d(TAG, "Start!");
132        mCallback = handler;
133
134    }
135
136    /**
137     * Called from BluetoothOppTransfer to cancel the "Transfer" Otherwise,
138     * server should end by itself.
139     */
140    public void stop() {
141        /*
142         * TODO now we implement in a tough way, just close the socket.
143         * maybe need nice way
144         */
145        if (D) Log.d(TAG, "Stop!");
146        mInterrupted = true;
147        if (mSession != null) {
148            try {
149                mSession.close();
150                mTransport.close();
151            } catch (IOException e) {
152                Log.e(TAG, "close mTransport error" + e);
153            }
154        }
155        mCallback = null;
156        mSession = null;
157    }
158
159    public void addShare(BluetoothOppShareInfo info) {
160        if (D) Log.d(TAG, "addShare for id " + info.mId);
161        mInfo = info;
162        mFileInfo = processShareInfo();
163    }
164
165    @Override
166    public int onPut(Operation op) {
167        if (D) Log.d(TAG, "onPut " + op.toString());
168        HeaderSet request;
169        String name, mimeType;
170        Long length;
171
172        int obexResponse = ResponseCodes.OBEX_HTTP_OK;
173
174        /**
175         * For multiple objects, reject further objects after user deny the
176         * first one
177         */
178        if (mAccepted == BluetoothShare.USER_CONFIRMATION_DENIED) {
179            return ResponseCodes.OBEX_HTTP_FORBIDDEN;
180        }
181
182        try {
183            boolean pre_reject = false;
184            request = op.getReceivedHeader();
185            if (V) Constants.logHeader(request);
186            name = (String)request.getHeader(HeaderSet.NAME);
187            length = (Long)request.getHeader(HeaderSet.LENGTH);
188            mimeType = (String)request.getHeader(HeaderSet.TYPE);
189
190            if (length == 0) {
191                if (D) Log.w(TAG, "length is 0, reject the transfer");
192                pre_reject = true;
193                obexResponse = ResponseCodes.OBEX_HTTP_LENGTH_REQUIRED;
194            }
195
196            if (name == null || name.equals("")) {
197                if (D) Log.w(TAG, "name is null or empty, reject the transfer");
198                pre_reject = true;
199                obexResponse = ResponseCodes.OBEX_HTTP_BAD_REQUEST;
200            }
201
202            if (!pre_reject) {
203                /* first we look for Mimetype in Android map */
204                String extension, type;
205                int dotIndex = name.indexOf('.');
206                if (dotIndex < 0) {
207                    if (D) Log.w(TAG, "There is no file extension, reject the transfer");
208                    pre_reject = true;
209                    obexResponse = ResponseCodes.OBEX_HTTP_BAD_REQUEST;
210                } else {
211                    extension = name.substring(dotIndex + 1).toLowerCase();
212                    MimeTypeMap map = MimeTypeMap.getSingleton();
213                    type = map.getMimeTypeFromExtension(extension);
214                    if (V) Log.v(TAG, "Mimetype guessed from extension " + extension + " is " + type);
215                    if (type != null) {
216                        mimeType = type;
217
218                    } else {
219                        if (mimeType == null) {
220                            if (D) Log.w(TAG, "Can't get mimetype, reject the transfer");
221                            pre_reject = true;
222                            obexResponse = ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE;
223                        }
224                    }
225                    if (mimeType != null) {
226                        mimeType = mimeType.toLowerCase();
227                    }
228                }
229            }
230
231            if (!pre_reject
232                    && (mimeType == null || Constants.mimeTypeMatches(mimeType,
233                            Constants.UNACCEPTABLE_SHARE_INBOUND_TYPES))) {
234                if (D) Log.w(TAG, "mimeType is null or in unacceptable list, reject the transfer");
235                pre_reject = true;
236                obexResponse = ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE;
237            }
238
239            if (pre_reject && obexResponse != ResponseCodes.OBEX_HTTP_OK) {
240                // some bad implemented client won't send disconnect
241                return obexResponse;
242            }
243
244        } catch (IOException e) {
245            Log.e(TAG, "get getReceivedHeaders error " + e);
246            return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
247        }
248
249        ContentValues values = new ContentValues();
250
251        values.put(BluetoothShare.FILENAME_HINT, name);
252        values.put(BluetoothShare.TOTAL_BYTES, length.intValue());
253        values.put(BluetoothShare.MIMETYPE, mimeType);
254
255        if (mTransport instanceof BluetoothOppRfcommTransport) {
256            String a = ((BluetoothOppRfcommTransport)mTransport).getRemoteAddress();
257            values.put(BluetoothShare.DESTINATION, a);
258        } else {
259            values.put(BluetoothShare.DESTINATION, "FF:FF:FF:00:00:00");
260        }
261
262        values.put(BluetoothShare.DIRECTION, BluetoothShare.DIRECTION_INBOUND);
263        values.put(BluetoothShare.TIMESTAMP, mTimestamp);
264
265        boolean needConfirm = true;
266        /** It's not first put if !serverBlocking, so we auto accept it */
267        if (!mServerBlocking) {
268            values.put(BluetoothShare.USER_CONFIRMATION,
269                    BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED);
270            needConfirm = false;
271        }
272
273        Uri contentUri = mContext.getContentResolver().insert(BluetoothShare.CONTENT_URI, values);
274        mLocalShareInfoId = Integer.parseInt(contentUri.getPathSegments().get(1));
275
276        if (needConfirm) {
277            Intent in = new Intent(BluetoothShare.INCOMING_FILE_CONFIRMATION_REQUEST_ACTION);
278            in.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
279            mContext.sendBroadcast(in);
280        }
281
282        if (V) Log.v(TAG, "insert contentUri: " + contentUri);
283            Log.v(TAG, "mLocalShareInfoId = " + mLocalShareInfoId);
284
285        if (V) Log.v(TAG, "acquire partial WakeLock");
286        if (mWakeLock.isHeld()) {
287            mPartialWakeLock.acquire();
288            mWakeLock.release();
289        }
290
291        mServerBlocking = true;
292
293        synchronized (this) {
294            try {
295
296                while (mServerBlocking) {
297                    wait(1000);
298                    if (mCallback != null && !mTimeoutMsgSent) {
299                        mCallback.sendMessageDelayed(mCallback
300                                .obtainMessage(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT),
301                                BluetoothOppObexSession.SESSION_TIMEOUT);
302                        mTimeoutMsgSent = true;
303                        if (V) Log.v(TAG, "MSG_CONNECT_TIMEOUT sent");
304                    }
305                }
306            } catch (InterruptedException e) {
307                if (V) Log.v(TAG, "Interrupted in onPut blocking");
308            }
309        }
310        if (D) Log.d(TAG, "Server unblocked ");
311        if (mCallback != null && mTimeoutMsgSent) {
312            mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
313        }
314
315        /* we should have mInfo now */
316
317        /*
318         * TODO check if this mInfo match the one that we insert before server
319         * blocking? just to make sure no error happens
320         */
321        if (mInfo.mId != mLocalShareInfoId) {
322            Log.e(TAG, "Unexpected error!");
323        }
324        mAccepted = mInfo.mConfirm;
325
326        if (V) Log.v(TAG, "after confirm: userAccepted=" + mAccepted);
327        int status = BluetoothShare.STATUS_SUCCESS;
328
329        if (mAccepted == BluetoothShare.USER_CONFIRMATION_CONFIRMED
330                || mAccepted == BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED) {
331            /* Confirm or auto-confirm */
332
333            if (mFileInfo.mFileName == null) {
334                status = mFileInfo.mStatus;
335                /* TODO need to check if this line is correct */
336                mInfo.mStatus = mFileInfo.mStatus;
337                Constants.updateShareStatus(mContext, mInfo.mId, status);
338                obexResponse = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
339
340            }
341
342            if (mFileInfo.mFileName != null) {
343
344                ContentValues updateValues = new ContentValues();
345                contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId);
346                updateValues.put(BluetoothShare._DATA, mFileInfo.mFileName);
347                updateValues.put(BluetoothShare.STATUS, BluetoothShare.STATUS_RUNNING);
348                mContext.getContentResolver().update(contentUri, updateValues, null, null);
349
350                status = receiveFile(mFileInfo, op);
351                /*
352                 * TODO map status to obex response code
353                 */
354                if (status != BluetoothShare.STATUS_SUCCESS) {
355                    obexResponse = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
356                }
357                Constants.updateShareStatus(mContext, mInfo.mId, status);
358            }
359
360            if (status == BluetoothShare.STATUS_SUCCESS) {
361                Message msg = Message.obtain(mCallback, BluetoothOppObexSession.MSG_SHARE_COMPLETE);
362                msg.obj = mInfo;
363                msg.sendToTarget();
364            } else {
365                if (mCallback != null) {
366                    Message msg = Message.obtain(mCallback,
367                            BluetoothOppObexSession.MSG_SESSION_ERROR);
368                    mInfo.mStatus = status;
369                    msg.obj = mInfo;
370                    msg.sendToTarget();
371                }
372            }
373        } else if (mAccepted == BluetoothShare.USER_CONFIRMATION_DENIED
374                || mAccepted == BluetoothShare.USER_CONFIRMATION_TIMEOUT) {
375            /* user actively deny the inbound transfer */
376            /*
377             * Note There is a question: what's next if user deny the first obj?
378             * Option 1 :continue prompt for next objects
379             * Option 2 :reject next objects and finish the session
380             * Now we take option 2:
381             */
382
383            Log.i(TAG, "Rejected incoming request");
384            if (mFileInfo.mFileName != null) {
385                try {
386                    mFileInfo.mOutputStream.close();
387                } catch (IOException e) {
388                    Log.e(TAG, "error close file stream");
389                }
390                new File(mFileInfo.mFileName).delete();
391            }
392            // set status as local cancel
393            status = BluetoothShare.STATUS_CANCELED;
394            Constants.updateShareStatus(mContext, mInfo.mId, status);
395            obexResponse = ResponseCodes.OBEX_HTTP_FORBIDDEN;
396
397            Message msg = Message.obtain(mCallback);
398            msg.what = BluetoothOppObexSession.MSG_SHARE_INTERRUPTED;
399            mInfo.mStatus = status;
400            msg.obj = mInfo;
401            msg.sendToTarget();
402        }
403        return obexResponse;
404    }
405
406    private int receiveFile(BluetoothOppReceiveFileInfo fileInfo, Operation op) {
407        /*
408         * implement receive file
409         */
410        int status = -1;
411        BufferedOutputStream bos = null;
412
413        InputStream is = null;
414        boolean error = false;
415        try {
416            is = op.openInputStream();
417        } catch (IOException e1) {
418            Log.e(TAG, "Error when openInputStream");
419            status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
420            error = true;
421        }
422
423        Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + mInfo.mId);
424
425        if (!error) {
426            ContentValues updateValues = new ContentValues();
427            updateValues.put(BluetoothShare._DATA, fileInfo.mFileName);
428            mContext.getContentResolver().update(contentUri, updateValues, null, null);
429        }
430
431        int position = 0;
432        if (!error) {
433            bos = new BufferedOutputStream(fileInfo.mOutputStream, 0x10000);
434        }
435
436        if (!error) {
437            int outputBufferSize = op.getMaxPacketSize();
438            byte[] b = new byte[outputBufferSize];
439            int readLength = 0;
440            long timestamp = 0;
441            try {
442                while ((!mInterrupted) && (position != fileInfo.mLength)) {
443
444                    if (V) timestamp = System.currentTimeMillis();
445
446                    readLength = is.read(b);
447
448                    if (readLength == -1) {
449                        if (D) Log.d(TAG, "Receive file reached stream end at position" + position);
450                        break;
451                    }
452
453                    bos.write(b, 0, readLength);
454                    position += readLength;
455
456                    if (V) {
457                        Log.v(TAG, "Receive file position = " + position + " readLength "
458                                + readLength + " bytes took "
459                                + (System.currentTimeMillis() - timestamp) + " ms");
460                    }
461
462                    ContentValues updateValues = new ContentValues();
463                    updateValues.put(BluetoothShare.CURRENT_BYTES, position);
464                    mContext.getContentResolver().update(contentUri, updateValues, null, null);
465                }
466            } catch (IOException e1) {
467                Log.e(TAG, "Error when receiving file");
468                status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
469                error = true;
470            }
471        }
472
473        if (mInterrupted) {
474            if (D) Log.d(TAG, "receiving file interrupted by user.");
475            status = BluetoothShare.STATUS_CANCELED;
476        } else {
477            if (position == fileInfo.mLength) {
478                if (D) Log.d(TAG, "Receiving file completed for " + fileInfo.mFileName);
479                status = BluetoothShare.STATUS_SUCCESS;
480            } else {
481                if (D) Log.d(TAG, "Reading file failed at " + position + " of " + fileInfo.mLength);
482                if (status == -1) {
483                    status = BluetoothShare.STATUS_UNKNOWN_ERROR;
484                }
485            }
486        }
487
488        if (bos != null) {
489            try {
490                bos.close();
491            } catch (IOException e) {
492                Log.e(TAG, "Error when closing stream after send");
493            }
494        }
495        return status;
496    }
497
498    private BluetoothOppReceiveFileInfo processShareInfo() {
499        if (D) Log.d(TAG, "processShareInfo() " + mInfo.mId);
500        BluetoothOppReceiveFileInfo fileInfo = BluetoothOppReceiveFileInfo.generateFileInfo(
501                mContext, mInfo.mId);
502        if (V) {
503            Log.v(TAG, "Generate BluetoothOppReceiveFileInfo:");
504            Log.v(TAG, "filename  :" + fileInfo.mFileName);
505            Log.v(TAG, "length    :" + fileInfo.mLength);
506            Log.v(TAG, "status    :" + fileInfo.mStatus);
507        }
508        return fileInfo;
509    }
510
511    @Override
512    public int onConnect(HeaderSet request, HeaderSet reply) {
513
514        if (D) Log.d(TAG, "onConnect");
515        if (V) Constants.logHeader(request);
516
517        mTimestamp = System.currentTimeMillis();
518        return ResponseCodes.OBEX_HTTP_OK;
519    }
520
521    @Override
522    public void onDisconnect(HeaderSet req, HeaderSet resp) {
523
524        if (D) Log.d(TAG, "onDisconnect");
525        resp.responseCode = ResponseCodes.OBEX_HTTP_OK;
526
527        /* onDisconnect could happen even before start() where mCallback is set */
528        if (mCallback != null) {
529            Message msg = Message.obtain(mCallback);
530            msg.what = BluetoothOppObexSession.MSG_SESSION_COMPLETE;
531            msg.obj = mInfo;
532            msg.sendToTarget();
533        }
534    }
535
536    @Override
537    public void onClose() {
538        if (V) Log.v(TAG, "release WakeLock");
539        if (mWakeLock.isHeld()) {
540            mWakeLock.release();
541        }
542        if (mPartialWakeLock.isHeld()) {
543            mPartialWakeLock.release();
544        }
545    }
546}
547