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