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