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