Home | History | Annotate | Download | only in map
      1 /*
      2 * Copyright (C) 2014 Samsung System LSI
      3 * Licensed under the Apache License, Version 2.0 (the "License");
      4 * you may not use this file except in compliance with the License.
      5 * You may obtain a copy of the License at
      6 *
      7 *      http://www.apache.org/licenses/LICENSE-2.0
      8 *
      9 * Unless required by applicable law or agreed to in writing, software
     10 * distributed under the License is distributed on an "AS IS" BASIS,
     11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 * See the License for the specific language governing permissions and
     13 * limitations under the License.
     14 */
     15 package com.android.bluetooth.map;
     16 
     17 import android.bluetooth.BluetoothDevice;
     18 import android.bluetooth.BluetoothSocket;
     19 import android.bluetooth.SdpMnsRecord;
     20 import android.os.Handler;
     21 import android.os.HandlerThread;
     22 import android.os.Looper;
     23 import android.os.Message;
     24 import android.os.ParcelUuid;
     25 import android.util.Log;
     26 import android.util.SparseBooleanArray;
     27 
     28 import com.android.bluetooth.BluetoothObexTransport;
     29 
     30 import java.io.IOException;
     31 import java.io.OutputStream;
     32 
     33 import javax.obex.ClientOperation;
     34 import javax.obex.ClientSession;
     35 import javax.obex.HeaderSet;
     36 import javax.obex.ObexTransport;
     37 import javax.obex.ResponseCodes;
     38 
     39 /**
     40  * The Message Notification Service class runs its own message handler thread,
     41  * to avoid executing long operations on the MAP service Thread.
     42  * This handler context is passed to the content observers,
     43  * hence all call-backs (and thereby transmission of data) is executed
     44  * from this thread.
     45  */
     46 public class BluetoothMnsObexClient {
     47 
     48     private static final String TAG = "BluetoothMnsObexClient";
     49     private static final boolean D = BluetoothMapService.DEBUG;
     50     private static final boolean V = BluetoothMapService.VERBOSE;
     51 
     52     private ObexTransport mTransport;
     53     public Handler mHandler = null;
     54     private volatile boolean mWaitingForRemote;
     55     private static final String TYPE_EVENT = "x-bt/MAP-event-report";
     56     private ClientSession mClientSession;
     57     private boolean mConnected = false;
     58     BluetoothDevice mRemoteDevice;
     59     private SparseBooleanArray mRegisteredMasIds = new SparseBooleanArray(1);
     60 
     61     private HeaderSet mHsConnect = null;
     62     private Handler mCallback = null;
     63     private SdpMnsRecord mMnsRecord;
     64     // Used by the MAS to forward notification registrations
     65     public static final int MSG_MNS_NOTIFICATION_REGISTRATION = 1;
     66     public static final int MSG_MNS_SEND_EVENT = 2;
     67     public static final int MSG_MNS_SDP_SEARCH_REGISTRATION = 3;
     68 
     69     //Copy SdpManager.SDP_INTENT_DELAY - The timeout to wait for reply from native.
     70     private final int MNS_SDP_SEARCH_DELAY = 6000;
     71     public MnsSdpSearchInfo mMnsLstRegRqst = null;
     72     private static final int MNS_NOTIFICATION_DELAY = 10;
     73     public static final ParcelUuid BLUETOOTH_UUID_OBEX_MNS =
     74             ParcelUuid.fromString("00001133-0000-1000-8000-00805F9B34FB");
     75 
     76 
     77     public BluetoothMnsObexClient(BluetoothDevice remoteDevice,
     78             SdpMnsRecord mnsRecord, Handler callback) {
     79         if (remoteDevice == null) {
     80             throw new NullPointerException("Obex transport is null");
     81         }
     82         mRemoteDevice = remoteDevice;
     83         HandlerThread thread = new HandlerThread("BluetoothMnsObexClient");
     84         thread.start();
     85         /* This will block until the looper have started, hence it will be safe to use it,
     86            when the constructor completes */
     87         Looper looper = thread.getLooper();
     88         mHandler = new MnsObexClientHandler(looper);
     89         mCallback = callback;
     90         mMnsRecord = mnsRecord;
     91     }
     92 
     93     public Handler getMessageHandler() {
     94         return mHandler;
     95     }
     96 
     97     class MnsSdpSearchInfo {
     98         private boolean isSearchInProgress;
     99         int lastMasId;
    100         int lastNotificationStatus;
    101 
    102         MnsSdpSearchInfo (boolean isSearchON, int masId, int notification) {
    103             isSearchInProgress = isSearchON;
    104             lastMasId = masId;
    105             lastNotificationStatus = notification;
    106         }
    107 
    108         public boolean isSearchInProgress() {
    109             return isSearchInProgress;
    110         }
    111 
    112         public void setIsSearchInProgress(boolean isSearchON) {
    113             isSearchInProgress = isSearchON;
    114         }
    115     }
    116 
    117     private final class MnsObexClientHandler extends Handler {
    118         private MnsObexClientHandler(Looper looper) {
    119             super(looper);
    120         }
    121 
    122         @Override
    123         public void handleMessage(Message msg) {
    124             switch (msg.what) {
    125             case MSG_MNS_NOTIFICATION_REGISTRATION:
    126                 if (V) Log.v(TAG, "Reg  masId:  " + msg.arg1 + " notfStatus: " + msg.arg2);
    127                 if (isValidMnsRecord()) {
    128                     handleRegistration(msg.arg1 /*masId*/, msg.arg2 /*status*/);
    129                 } else {
    130                     //Should not happen
    131                     if (D) Log.d(TAG, "MNS SDP info not available yet - Cannot Connect.");
    132                 }
    133                 break;
    134             case MSG_MNS_SEND_EVENT:
    135                 sendEventHandler((byte[])msg.obj/*byte[]*/, msg.arg1 /*masId*/);
    136                 break;
    137             case MSG_MNS_SDP_SEARCH_REGISTRATION:
    138                 //Initiate SDP Search
    139                 notifyMnsSdpSearch();
    140                 //Save the mns search info
    141                 mMnsLstRegRqst = new MnsSdpSearchInfo(true, msg.arg1, msg.arg2);
    142                 //Handle notification registration.
    143                 Message msgReg =
    144                         mHandler.obtainMessage(MSG_MNS_NOTIFICATION_REGISTRATION,
    145                                 msg.arg1, msg.arg2);
    146                 if (V) Log.v(TAG, "SearchReg  masId:  " + msg.arg1 + " notfStatus: " + msg.arg2);
    147                 mHandler.sendMessageDelayed(msgReg, MNS_SDP_SEARCH_DELAY);
    148                 break;
    149             default:
    150                 break;
    151             }
    152         }
    153     }
    154 
    155     public boolean isConnected() {
    156         return mConnected;
    157     }
    158 
    159     /**
    160      * Disconnect the connection to MNS server.
    161      * Call this when the MAS client requests a de-registration on events.
    162      */
    163     public synchronized void disconnect() {
    164         try {
    165             if (mClientSession != null) {
    166                 mClientSession.disconnect(null);
    167                 if (D) Log.d(TAG, "OBEX session disconnected");
    168             }
    169         } catch (IOException e) {
    170             Log.w(TAG, "OBEX session disconnect error " + e.getMessage());
    171         }
    172         try {
    173             if (mClientSession != null) {
    174                 if (D) Log.d(TAG, "OBEX session close mClientSession");
    175                 mClientSession.close();
    176                 mClientSession = null;
    177                 if (D) Log.d(TAG, "OBEX session closed");
    178             }
    179         } catch (IOException e) {
    180             Log.w(TAG, "OBEX session close error:" + e.getMessage());
    181         }
    182         if (mTransport != null) {
    183             try {
    184                 if (D) Log.d(TAG, "Close Obex Transport");
    185                 mTransport.close();
    186                 mTransport = null;
    187                 mConnected = false;
    188                 if (D) Log.d(TAG, "Obex Transport Closed");
    189             } catch (IOException e) {
    190                 Log.e(TAG, "mTransport.close error: " + e.getMessage());
    191             }
    192         }
    193     }
    194 
    195     /**
    196      * Shutdown the MNS.
    197      */
    198     public synchronized void shutdown() {
    199         /* should shutdown handler thread first to make sure
    200          * handleRegistration won't be called when disconnect
    201          */
    202         if (mHandler != null) {
    203             // Shut down the thread
    204             mHandler.removeCallbacksAndMessages(null);
    205             Looper looper = mHandler.getLooper();
    206             if (looper != null) {
    207                 looper.quit();
    208             }
    209             mHandler = null;
    210         }
    211 
    212         /* Disconnect if connected */
    213         disconnect();
    214 
    215         mRegisteredMasIds.clear();
    216     }
    217 
    218     /**
    219      * We store a list of registered MasIds only to control connect/disconnect
    220      * @param masId
    221      * @param notificationStatus
    222      */
    223     public synchronized void handleRegistration(int masId, int notificationStatus){
    224         if(D) Log.d(TAG, "handleRegistration( " + masId + ", " + notificationStatus + ")");
    225         boolean sendObserverRegistration = true;
    226         if (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_NO) {
    227             mRegisteredMasIds.delete(masId);
    228             if (mMnsLstRegRqst != null &&  mMnsLstRegRqst.lastMasId == masId) {
    229                 //Clear last saved MNSSdpSearchInfo , if Disconnect requested for same MasId.
    230                 mMnsLstRegRqst = null;
    231             }
    232         } else if (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) {
    233             /* Connect if we do not have a connection, and start the content observers providing
    234              * this thread as Handler.
    235              */
    236             if (isConnected() == false) {
    237                 if(D) Log.d(TAG, "handleRegistration: connect");
    238                 connect();
    239             }
    240             sendObserverRegistration = isConnected();
    241             mRegisteredMasIds.put(masId, true); // We don't use the value for anything
    242 
    243             // Clear last saved MNSSdpSearchInfo after connect is processed.
    244             mMnsLstRegRqst = null;
    245         }
    246 
    247         if (mRegisteredMasIds.size() == 0) {
    248             // No more registrations - disconnect
    249             if(D) Log.d(TAG, "handleRegistration: disconnect");
    250             disconnect();
    251         }
    252 
    253         //Register ContentObserver After connect/disconnect MNS channel.
    254         if (V) Log.v(TAG, "Send  registerObserver: " + sendObserverRegistration);
    255         if (mCallback != null && sendObserverRegistration) {
    256             Message msg = Message.obtain(mCallback);
    257             msg.what = BluetoothMapService.MSG_OBSERVER_REGISTRATION;
    258             msg.arg1 = masId;
    259             msg.arg2 = notificationStatus;
    260             msg.sendToTarget();
    261         }
    262     }
    263 
    264     public boolean isValidMnsRecord() {
    265         return (mMnsRecord != null);
    266     }
    267 
    268     public void setMnsRecord(SdpMnsRecord mnsRecord) {
    269         if (V) Log.v(TAG, "setMNSRecord");
    270         if (isValidMnsRecord()) {
    271            Log.w(TAG,"MNS Record already available. Still update.");
    272         }
    273         mMnsRecord = mnsRecord;
    274         if (mMnsLstRegRqst != null) {
    275             //SDP Search completed.
    276             mMnsLstRegRqst.setIsSearchInProgress(false);
    277             if (mHandler.hasMessages(MSG_MNS_NOTIFICATION_REGISTRATION)) {
    278                 mHandler.removeMessages(MSG_MNS_NOTIFICATION_REGISTRATION);
    279                 //Search Result obtained within MNS_SDP_SEARCH_DELAY timeout
    280                 if (!isValidMnsRecord()) {
    281                     // SDP info still not available for last trial.
    282                     // Clear saved info.
    283                     mMnsLstRegRqst = null;
    284                 } else {
    285                     if (V) Log.v(TAG, "Handle registration for last saved request");
    286                     Message msgReg =
    287                             mHandler.obtainMessage(MSG_MNS_NOTIFICATION_REGISTRATION);
    288                     msgReg.arg1 = mMnsLstRegRqst.lastMasId;
    289                     msgReg.arg2 = mMnsLstRegRqst.lastNotificationStatus;
    290                     if (V) Log.v(TAG, "SearchReg  masId:  " + msgReg.arg1
    291                         + " notfStatus: " + msgReg.arg2);
    292                     //Handle notification registration.
    293                     mHandler.sendMessageDelayed(msgReg, MNS_NOTIFICATION_DELAY);
    294                 }
    295             }
    296         } else {
    297            if (V) Log.v(TAG, "No last saved MNSSDPInfo to handle");
    298         }
    299     }
    300 
    301     public void connect() {
    302 
    303         mConnected = true;
    304 
    305         BluetoothSocket btSocket = null;
    306         try {
    307             // TODO: Do SDP record search again?
    308             if (isValidMnsRecord() && mMnsRecord.getL2capPsm() > 0) {
    309                 // Do L2CAP connect
    310                 btSocket = mRemoteDevice.createL2capSocket(mMnsRecord.getL2capPsm());
    311 
    312             } else if (isValidMnsRecord() && mMnsRecord.getRfcommChannelNumber() > 0) {
    313                 // Do Rfcomm connect
    314                 btSocket = mRemoteDevice.createRfcommSocket(mMnsRecord.getRfcommChannelNumber());
    315             } else {
    316                 // This should not happen...
    317                 Log.e(TAG, "Invalid SDP content - attempt a connect to UUID...");
    318                 // TODO: Why insecure? - is it because the link is already encrypted?
    319               btSocket = mRemoteDevice.createInsecureRfcommSocketToServiceRecord(
    320                       BLUETOOTH_UUID_OBEX_MNS.getUuid());
    321             }
    322             btSocket.connect();
    323         } catch (IOException e) {
    324             Log.e(TAG, "BtSocket Connect error " + e.getMessage(), e);
    325             // TODO: do we need to report error somewhere?
    326             mConnected = false;
    327             return;
    328         }
    329 
    330         mTransport = new BluetoothObexTransport(btSocket);
    331 
    332         try {
    333             mClientSession = new ClientSession(mTransport);
    334         } catch (IOException e1) {
    335             Log.e(TAG, "OBEX session create error " + e1.getMessage());
    336             mConnected = false;
    337         }
    338         if (mConnected && mClientSession != null) {
    339             boolean connected = false;
    340             HeaderSet hs = new HeaderSet();
    341             // bb582b41-420c-11db-b0de-0800200c9a66
    342             byte[] mnsTarget = { (byte) 0xbb, (byte) 0x58, (byte) 0x2b, (byte) 0x41,
    343                                  (byte) 0x42, (byte) 0x0c, (byte) 0x11, (byte) 0xdb,
    344                                  (byte) 0xb0, (byte) 0xde, (byte) 0x08, (byte) 0x00,
    345                                  (byte) 0x20, (byte) 0x0c, (byte) 0x9a, (byte) 0x66 };
    346             hs.setHeader(HeaderSet.TARGET, mnsTarget);
    347 
    348             synchronized (this) {
    349                 mWaitingForRemote = true;
    350             }
    351             try {
    352                 mHsConnect = mClientSession.connect(hs);
    353                 if (D) Log.d(TAG, "OBEX session created");
    354                 connected = true;
    355             } catch (IOException e) {
    356                 Log.e(TAG, "OBEX session connect error " + e.getMessage());
    357             }
    358             mConnected = connected;
    359         }
    360         synchronized (this) {
    361                 mWaitingForRemote = false;
    362         }
    363     }
    364 
    365     /**
    366      * Call this method to queue an event report to be send to the MNS server.
    367      * @param eventBytes the encoded event data.
    368      * @param masInstanceId the MasId of the instance sending the event.
    369      */
    370     public void sendEvent(byte[] eventBytes, int masInstanceId) {
    371         // We need to check for null, to handle shutdown.
    372         if(mHandler != null) {
    373             Message msg = mHandler.obtainMessage(MSG_MNS_SEND_EVENT, masInstanceId, 0, eventBytes);
    374             if(msg != null) {
    375                 msg.sendToTarget();
    376             }
    377         }
    378         notifyUpdateWakeLock();
    379     }
    380 
    381     private void notifyMnsSdpSearch() {
    382         if (mCallback != null) {
    383             Message msg = Message.obtain(mCallback);
    384             msg.what = BluetoothMapService.MSG_MNS_SDP_SEARCH;
    385             msg.sendToTarget();
    386         }
    387     }
    388 
    389     private int sendEventHandler(byte[] eventBytes, int masInstanceId) {
    390 
    391         boolean error = false;
    392         int responseCode = -1;
    393         HeaderSet request;
    394         int maxChunkSize, bytesToWrite, bytesWritten = 0;
    395         ClientSession clientSession = mClientSession;
    396 
    397         if ((!mConnected) || (clientSession == null)) {
    398             Log.w(TAG, "sendEvent after disconnect:" + mConnected);
    399             return responseCode;
    400         }
    401 
    402         request = new HeaderSet();
    403         BluetoothMapAppParams appParams = new BluetoothMapAppParams();
    404         appParams.setMasInstanceId(masInstanceId);
    405 
    406         ClientOperation putOperation = null;
    407         OutputStream outputStream = null;
    408 
    409         try {
    410             request.setHeader(HeaderSet.TYPE, TYPE_EVENT);
    411             request.setHeader(HeaderSet.APPLICATION_PARAMETER, appParams.EncodeParams());
    412 
    413             if (mHsConnect.mConnectionID != null) {
    414                 request.mConnectionID = new byte[4];
    415                 System.arraycopy(mHsConnect.mConnectionID, 0, request.mConnectionID, 0, 4);
    416             } else {
    417                 Log.w(TAG, "sendEvent: no connection ID");
    418             }
    419 
    420             synchronized (this) {
    421                 mWaitingForRemote = true;
    422             }
    423             // Send the header first and then the body
    424             try {
    425                 if (V) Log.v(TAG, "Send headerset Event ");
    426                 putOperation = (ClientOperation)clientSession.put(request);
    427                 // TODO - Should this be kept or Removed
    428 
    429             } catch (IOException e) {
    430                 Log.e(TAG, "Error when put HeaderSet " + e.getMessage());
    431                 error = true;
    432             }
    433             synchronized (this) {
    434                 mWaitingForRemote = false;
    435             }
    436             if (!error) {
    437                 try {
    438                     if (V) Log.v(TAG, "Send headerset Event ");
    439                     outputStream = putOperation.openOutputStream();
    440                 } catch (IOException e) {
    441                     Log.e(TAG, "Error when opening OutputStream " + e.getMessage());
    442                     error = true;
    443                 }
    444             }
    445 
    446             if (!error) {
    447 
    448                 maxChunkSize = putOperation.getMaxPacketSize();
    449 
    450                 while (bytesWritten < eventBytes.length) {
    451                     bytesToWrite = Math.min(maxChunkSize, eventBytes.length - bytesWritten);
    452                     outputStream.write(eventBytes, bytesWritten, bytesToWrite);
    453                     bytesWritten += bytesToWrite;
    454                 }
    455 
    456                 if (bytesWritten == eventBytes.length) {
    457                     Log.i(TAG, "SendEvent finished send length" + eventBytes.length);
    458                 } else {
    459                     error = true;
    460                     putOperation.abort();
    461                     Log.i(TAG, "SendEvent interrupted");
    462                 }
    463             }
    464         } catch (IOException e) {
    465             handleSendException(e.toString());
    466             error = true;
    467         } catch (IndexOutOfBoundsException e) {
    468             handleSendException(e.toString());
    469             error = true;
    470         } finally {
    471             try {
    472                 if (outputStream != null) {
    473                     outputStream.close();
    474                 }
    475             } catch (IOException e) {
    476                 Log.e(TAG, "Error when closing stream after send " + e.getMessage());
    477             }
    478             try {
    479                 if ((!error) && (putOperation != null)) {
    480                     responseCode = putOperation.getResponseCode();
    481                     if (responseCode != -1) {
    482                         if (V) Log.v(TAG, "Put response code " + responseCode);
    483                         if (responseCode != ResponseCodes.OBEX_HTTP_OK) {
    484                             Log.i(TAG, "Response error code is " + responseCode);
    485                         }
    486                     }
    487                 }
    488                 if (putOperation != null) {
    489                     putOperation.close();
    490                 }
    491             } catch (IOException e) {
    492                 Log.e(TAG, "Error when closing stream after send " + e.getMessage());
    493             }
    494         }
    495 
    496         return responseCode;
    497     }
    498 
    499     private void handleSendException(String exception) {
    500         Log.e(TAG, "Error when sending event: " + exception);
    501     }
    502 
    503     private void notifyUpdateWakeLock() {
    504         if(mCallback != null) {
    505             Message msg = Message.obtain(mCallback);
    506             msg.what = BluetoothMapService.MSG_ACQUIRE_WAKE_LOCK;
    507             msg.sendToTarget();
    508         }
    509     }
    510 }
    511