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