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