BluetoothOppObexServerSession.java revision df7415da0e510ab8e4b73831a5ade38306982fe1
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.FileNotFoundException;
38import java.io.FileOutputStream;
39import java.io.IOException;
40import java.io.InputStream;
41
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.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 = "BtOpp Server";
65
66    private ObexTransport mTransport;
67
68    private Context mContext;
69
70    private Handler mCallback = null;
71
72    /* status when server is blocking for user/auto confirmation */
73    private boolean mServerBlocking = true;
74
75    /* the current transfer info */
76    private BluetoothOppShareInfo mInfo;
77
78    /* info id when we insert the record */
79    private int mLocalShareInfoId;
80
81    private int mAccepted = BluetoothShare.USER_CONFIRMATION_PENDING;
82
83    private boolean mInterrupted = false;
84
85    private ServerSession mSession;
86
87    private long mTimestamp;
88
89    private BluetoothOppReceiveFileInfo mFileInfo;
90
91    public BluetoothOppObexServerSession(Context context, ObexTransport transport) {
92        mContext = context;
93        mTransport = transport;
94    }
95
96    public void unblock() {
97        mServerBlocking = false;
98    }
99
100    /**
101     * Called when connection is accepted from remote, to retrieve the first
102     * Header then wait for user confirmation
103     */
104    public void preStart() {
105        try {
106            if (Constants.LOGV) {
107                Log.v(TAG, "Create ServerSession with transport " + mTransport.toString());
108            }
109            mSession = new ServerSession(mTransport, this, null);
110        } catch (IOException e) {
111            Log.e(TAG, "Create server session error" + e);
112        }
113    }
114
115    /**
116     * Called from BluetoothOppTransfer to start the "Transfer"
117     */
118    public void start(Handler handler) {
119        if (Constants.LOGV) {
120            Log.v(TAG, "Start!");
121        }
122        mCallback = handler;
123
124    }
125
126    /**
127     * Called from BluetoothOppTransfer to cancel the "Transfer" Otherwise,
128     * server should end by itself.
129     */
130    public void stop() {
131        /*
132         * TODO now we implement in a tough way, just close the socket.
133         * maybe need nice way
134         */
135        if (Constants.LOGV) {
136            Log.v(TAG, "Stop!");
137        }
138        mInterrupted = true;
139        if (mSession != null) {
140            try {
141                mSession.close();
142                mTransport.close();
143            } catch (IOException e) {
144                Log.e(TAG, "close mTransport error" + e);
145            }
146        }
147    }
148
149    public void addShare(BluetoothOppShareInfo info) {
150        if (Constants.LOGV) {
151            Log.v(TAG, "addShare for id " + info.mId);
152        }
153        mInfo = info;
154        mFileInfo = processShareInfo();
155    }
156
157    @Override
158    public int onPut(Operation op) {
159        if (Constants.LOGV) {
160            Log.v(TAG, "onPut " + op.toString());
161        }
162        HeaderSet request;
163        String name, mimeType;
164        Long length;
165
166        int obexResponse = ResponseCodes.OBEX_HTTP_OK;
167
168        /**
169         * For multiple objects, reject further objects after user deny the
170         * first one
171         */
172        if (mAccepted == BluetoothShare.USER_CONFIRMATION_DENIED) {
173            return ResponseCodes.OBEX_HTTP_FORBIDDEN;
174        }
175
176        try {
177            boolean pre_reject = false;
178            request = op.getReceivedHeader();
179            if (Constants.LOGVV) {
180                logHeader(request);
181            }
182            name = (String)request.getHeader(HeaderSet.NAME);
183            length = (Long)request.getHeader(HeaderSet.LENGTH);
184            mimeType = (String)request.getHeader(HeaderSet.TYPE);
185
186            if (length == 0) {
187                if (Constants.LOGV) {
188                    Log.w(TAG, "length is 0, reject the transfer");
189                }
190                pre_reject = true;
191                obexResponse = ResponseCodes.OBEX_HTTP_LENGTH_REQUIRED;
192            }
193
194            if (name == null || name.equals("")) {
195                if (Constants.LOGV) {
196                    Log.w(TAG, "name is null or empty, reject the transfer");
197                }
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 (Constants.LOGV) {
208                        Log.w(TAG, "There is no file extension, reject the transfer");
209                    }
210                    pre_reject = true;
211                    obexResponse = ResponseCodes.OBEX_HTTP_BAD_REQUEST;
212                } else {
213                    extension = name.substring(dotIndex + 1);
214                    MimeTypeMap map = MimeTypeMap.getSingleton();
215                    type = map.getMimeTypeFromExtension(extension);
216                    if (Constants.LOGVV) {
217                        Log.v(TAG, "Mimetype guessed from extension " + extension + " is "
218                                + mimeType);
219                    }
220                    if (type != null) {
221                        mimeType = type;
222
223                    } else {
224                        if (mimeType == null) {
225                            if (Constants.LOGV) {
226                                Log.w(TAG, "Can't get mimetype, reject the transfer");
227                            }
228                            pre_reject = true;
229                            obexResponse = ResponseCodes.OBEX_HTTP_BAD_REQUEST;
230                        }
231                    }
232                    if (mimeType != null) {
233                        mimeType = mimeType.toLowerCase();
234                    }
235                }
236            }
237
238            if (!pre_reject
239                    && (mimeType == null || Constants.mimeTypeMatches(mimeType,
240                            Constants.UNACCEPTABLE_SHARE_INBOUND_TYPES))) {
241                if (Constants.LOGV) {
242                    Log.w(TAG, "mimeType is null or in unacceptable list, reject the transfer");
243                }
244                pre_reject = true;
245                obexResponse = ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE;
246            }
247
248            if (pre_reject && obexResponse != ResponseCodes.OBEX_HTTP_OK) {
249                return obexResponse;
250            }
251
252        } catch (IOException e) {
253            Log.e(TAG, "get getReceivedHeaders error " + e);
254            return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
255        }
256
257        ContentValues values = new ContentValues();
258
259        values.put(BluetoothShare.FILENAME_HINT, name);
260        values.put(BluetoothShare.TOTAL_BYTES, length.intValue());
261        values.put(BluetoothShare.MIMETYPE, mimeType);
262
263        if (mTransport instanceof BluetoothOppRfcommTransport) {
264            String a = ((BluetoothOppRfcommTransport)mTransport).getRemoteAddress();
265            values.put(BluetoothShare.DESTINATION, a);
266        } else {
267            values.put(BluetoothShare.DESTINATION, "FF:FF:FF:00:00:00");
268        }
269
270        values.put(BluetoothShare.DIRECTION, BluetoothShare.DIRECTION_INBOUND);
271        values.put(BluetoothShare.TIMESTAMP, mTimestamp);
272
273        boolean needConfirm = true;
274        /** It's not first put if !serverBlocking, so we auto accept it */
275        if (!mServerBlocking) {
276            values.put(BluetoothShare.USER_CONFIRMATION,
277                    BluetoothShare.USER_CONFIRMATION_AUTO_CONFIRMED);
278            needConfirm = false;
279        }
280
281        Uri contentUri = mContext.getContentResolver().insert(BluetoothShare.CONTENT_URI, values);
282        mLocalShareInfoId = Integer.parseInt(contentUri.getPathSegments().get(1));
283
284        if (needConfirm) {
285            Intent in = new Intent(BluetoothShare.INCOMING_FILE_CONFIRMATION_REQUEST_ACTION);
286            in.setClassName(Constants.THIS_PACKAGE_NAME, BluetoothOppReceiver.class.getName());
287            mContext.sendBroadcast(in);
288        }
289
290        if (Constants.LOGVV) {
291            Log.v(TAG, "insert contentUri: " + contentUri);
292            Log.v(TAG, "mLocalShareInfoId = " + mLocalShareInfoId);
293        }
294
295        // TODO add server wait timeout
296        mServerBlocking = true;
297        boolean msgSent = false;
298        synchronized (this) {
299            try {
300
301                while (mServerBlocking) {
302                    wait(1000);
303                    if (mCallback != null && !msgSent) {
304                        mCallback.sendMessageDelayed(mCallback
305                                .obtainMessage(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT),
306                                BluetoothOppObexSession.SESSION_TIMEOUT);
307                        msgSent = true;
308                        if (Constants.LOGVV) {
309                            Log.v(TAG, "MSG_CONNECT_TIMEOUT sent");
310                        }
311                    }
312                }
313            } catch (InterruptedException e) {
314                if (Constants.LOGVV) {
315                    Log.v(TAG, "Interrupted in onPut blocking");
316                }
317            }
318        }
319        if (Constants.LOGV) {
320            Log.v(TAG, "Server unblocked ");
321        }
322        if (mCallback != null && msgSent) {
323            mCallback.removeMessages(BluetoothOppObexSession.MSG_CONNECT_TIMEOUT);
324        }
325
326        /* we should have mInfo now */
327
328        /*
329         * TODO check if this mInfo match the one that we insert before server
330         * blocking? just to make sure no error happens
331         */
332        if (mInfo.mId != mLocalShareInfoId) {
333            Log.e(TAG, "Unexpected error!");
334        }
335        mAccepted = mInfo.mConfirm;
336
337        if (Constants.LOGVV) {
338            Log.v(TAG, "after confirm: userAccepted=" + mAccepted);
339        }
340        int status = BluetoothShare.STATUS_SUCCESS;
341
342        if (mAccepted == BluetoothShare.USER_CONFIRMATION_CONFIRMED
343                || mAccepted == BluetoothShare.USER_CONFIRMATION_AUTO_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                Message msg = Message.obtain(mCallback, BluetoothOppObexSession.MSG_SESSION_ERROR);
379                msg.obj = mInfo;
380                msg.sendToTarget();
381            }
382        } else if (mAccepted == BluetoothShare.USER_CONFIRMATION_DENIED
383                || mAccepted == BluetoothShare.USER_CONFIRMATION_TIMEOUT) {
384            /* user actively deny the inbound transfer */
385            /*
386             * Note There is a question: what's next if user deny the first obj?
387             * Option 1 :continue prompt for next objects
388             * Option 2 :reject next objects and finish the session
389             * Now we take option 2:
390             */
391            if (Constants.LOGVV) {
392                Log.v(TAG, "request forbidden, indicate interrupted");
393            }
394            status = BluetoothShare.STATUS_FORBIDDEN;
395            Constants.updateShareStatus(mContext, mInfo.mId, status);
396            obexResponse = ResponseCodes.OBEX_HTTP_FORBIDDEN;
397            Message msg = Message.obtain(mCallback);
398            /* TODO check which message should be sent */
399            msg.what = BluetoothOppObexSession.MSG_SHARE_INTERRUPTED;
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            File f = new File(fileInfo.mFileName);
434            try {
435                bos = new BufferedOutputStream(new FileOutputStream(f), 0x10000);
436            } catch (FileNotFoundException e1) {
437                Log.e(TAG, "Error when open file " + f.toString());
438                status = BluetoothShare.STATUS_FILE_ERROR;
439                error = true;
440            }
441        }
442
443        if (!error) {
444            int outputBufferSize = op.getMaxPacketSize();
445            byte[] b = new byte[outputBufferSize];
446            int readLength = 0;
447            long timestamp;
448            try {
449                while ((!mInterrupted) && (position != fileInfo.mLength)) {
450
451                    if (Constants.LOGVV) {
452                        timestamp = System.currentTimeMillis();
453                    }
454
455                    readLength = is.read(b);
456
457                    if (readLength == -1) {
458                        if (Constants.LOGV) {
459                            Log.v(TAG, "Receive file reached stream end at position" + position);
460                        }
461                        break;
462                    }
463
464                    bos.write(b, 0, readLength);
465                    position += readLength;
466
467                    if (Constants.USE_EMULATOR_DEBUG) {
468                        synchronized (this) {
469                            try {
470                                wait(300);
471                            } catch (InterruptedException e) {
472                                status = BluetoothShare.STATUS_CANCELED;
473                                mInterrupted = true;
474                                if (Constants.LOGVV) {
475                                    Log.v(TAG, "ReceiveFile interrupted when receive file "
476                                            + fileInfo.mFileName + " at " + position + " of "
477                                            + position);
478                                }
479                                Constants.updateShareStatus(mContext, mInfo.mId, status);
480                            }
481                        }
482                    }
483
484                    if (Constants.LOGVV) {
485                        Log.v(TAG, "Receive file position = " + position + " readLength "
486                                + readLength + " bytes took "
487                                + (System.currentTimeMillis() - timestamp) + " ms");
488                    }
489
490                    ContentValues updateValues = new ContentValues();
491                    updateValues.put(BluetoothShare.CURRENT_BYTES, position);
492                    mContext.getContentResolver().update(contentUri, updateValues, null, null);
493                }
494            } catch (IOException e1) {
495                Log.e(TAG, "Error when receiving file");
496                status = BluetoothShare.STATUS_OBEX_DATA_ERROR;
497                error = true;
498            }
499        }
500
501        if (mInterrupted) {
502            if (Constants.LOGV) {
503                Log.v(TAG, "receiving file interrupted by user.");
504            }
505            status = BluetoothShare.STATUS_CANCELED;
506        } else {
507            if (position == fileInfo.mLength) {
508                if (Constants.LOGV) {
509                    Log.v(TAG, "Receiving file completed for " + fileInfo.mFileName);
510                }
511                status = BluetoothShare.STATUS_SUCCESS;
512            } else {
513                if (Constants.LOGV) {
514                    Log.v(TAG, "Reading file failed at " + position + " of " + fileInfo.mLength);
515                }
516                if (status == -1) {
517                    status = BluetoothShare.STATUS_UNKNOWN_ERROR;
518                }
519            }
520        }
521
522        Constants.updateShareStatus(mContext, mInfo.mId, status);
523        if (bos != null) {
524            try {
525                bos.close();
526            } catch (IOException e) {
527                Log.e(TAG, "Error when closing stream after send");
528            }
529        }
530        return status;
531    }
532
533    private BluetoothOppReceiveFileInfo processShareInfo() {
534        if (Constants.LOGV) {
535            Log.v(TAG, "processShareInfo() " + mInfo.mId);
536        }
537        BluetoothOppReceiveFileInfo fileInfo = BluetoothOppReceiveFileInfo.generateFileInfo(
538                mContext, mInfo.mId);
539        if (Constants.LOGVV) {
540            Log.v(TAG, "Generate BluetoothOppReceiveFileInfo:");
541            Log.v(TAG, "filename  :" + fileInfo.mFileName);
542            Log.v(TAG, "length    :" + fileInfo.mLength);
543            Log.v(TAG, "status    :" + fileInfo.mStatus);
544        }
545        return fileInfo;
546    }
547
548    @Override
549    public int onConnect(HeaderSet request, HeaderSet reply) {
550
551        if (Constants.LOGV) {
552            Log.v(TAG, "onConnect");
553        }
554        if (Constants.LOGVV) {
555            logHeader(request);
556        }
557
558        mTimestamp = System.currentTimeMillis();
559        return ResponseCodes.OBEX_HTTP_OK;
560    }
561
562    @Override
563    public void onDisconnect(HeaderSet req, HeaderSet resp) {
564
565        if (Constants.LOGV) {
566            Log.v(TAG, "onDisconnect");
567        }
568        resp.responseCode = ResponseCodes.OBEX_HTTP_OK;
569        /* onDisconnect could happen even before start() where mCallback is set */
570        if (mCallback != null) {
571            Message msg = Message.obtain(mCallback);
572            msg.what = BluetoothOppObexSession.MSG_SESSION_COMPLETE;
573            msg.obj = mInfo;
574            msg.sendToTarget();
575        }
576    }
577
578    @Override
579    public void onClose() {
580    }
581
582    private void logHeader(HeaderSet hs) {
583        Log.v(TAG, "Dumping HeaderSet " + hs.toString());
584        try {
585
586            Log.v(TAG, "COUNT : " + hs.getHeader(HeaderSet.COUNT));
587            Log.v(TAG, "NAME : " + hs.getHeader(HeaderSet.NAME));
588            Log.v(TAG, "TYPE : " + hs.getHeader(HeaderSet.TYPE));
589            Log.v(TAG, "LENGTH : " + hs.getHeader(HeaderSet.LENGTH));
590            Log.v(TAG, "TIME_ISO_8601 : " + hs.getHeader(HeaderSet.TIME_ISO_8601));
591            Log.v(TAG, "TIME_4_BYTE : " + hs.getHeader(HeaderSet.TIME_4_BYTE));
592            Log.v(TAG, "DESCRIPTION : " + hs.getHeader(HeaderSet.DESCRIPTION));
593            Log.v(TAG, "TARGET : " + hs.getHeader(HeaderSet.TARGET));
594            Log.v(TAG, "HTTP : " + hs.getHeader(HeaderSet.HTTP));
595            Log.v(TAG, "WHO : " + hs.getHeader(HeaderSet.WHO));
596            Log.v(TAG, "OBJECT_CLASS : " + hs.getHeader(HeaderSet.OBJECT_CLASS));
597            Log.v(TAG, "APPLICATION_PARAMETER : " + hs.getHeader(HeaderSet.APPLICATION_PARAMETER));
598        } catch (IOException e) {
599            Log.e(TAG, "dump HeaderSet error " + e);
600        }
601
602    }
603}
604