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