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 static 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, SdpMnsRecord mnsRecord,
     78             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 mIsSearchInProgress;
     99         public int lastMasId;
    100         public int lastNotificationStatus;
    101 
    102         MnsSdpSearchInfo(boolean isSearchON, int masId, int notification) {
    103             mIsSearchInProgress = isSearchON;
    104             lastMasId = masId;
    105             lastNotificationStatus = notification;
    106         }
    107 
    108         public boolean isSearchInProgress() {
    109             return mIsSearchInProgress;
    110         }
    111 
    112         public void setIsSearchInProgress(boolean isSearchON) {
    113             mIsSearchInProgress = 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) {
    127                         Log.v(TAG, "Reg  masId:  " + msg.arg1 + " notfStatus: " + msg.arg2);
    128                     }
    129                     if (isValidMnsRecord()) {
    130                         handleRegistration(msg.arg1 /*masId*/, msg.arg2 /*status*/);
    131                     } else {
    132                         //Should not happen
    133                         if (D) {
    134                             Log.d(TAG, "MNS SDP info not available yet - Cannot Connect.");
    135                         }
    136                     }
    137                     break;
    138                 case MSG_MNS_SEND_EVENT:
    139                     sendEventHandler((byte[]) msg.obj/*byte[]*/, msg.arg1 /*masId*/);
    140                     break;
    141                 case MSG_MNS_SDP_SEARCH_REGISTRATION:
    142                     //Initiate SDP Search
    143                     notifyMnsSdpSearch();
    144                     //Save the mns search info
    145                     mMnsLstRegRqst = new MnsSdpSearchInfo(true, msg.arg1, msg.arg2);
    146                     //Handle notification registration.
    147                     Message msgReg =
    148                             mHandler.obtainMessage(MSG_MNS_NOTIFICATION_REGISTRATION, msg.arg1,
    149                                     msg.arg2);
    150                     if (V) {
    151                         Log.v(TAG, "SearchReg  masId:  " + msg.arg1 + " notfStatus: " + msg.arg2);
    152                     }
    153                     mHandler.sendMessageDelayed(msgReg, MNS_SDP_SEARCH_DELAY);
    154                     break;
    155                 default:
    156                     break;
    157             }
    158         }
    159     }
    160 
    161     public boolean isConnected() {
    162         return mConnected;
    163     }
    164 
    165     /**
    166      * Disconnect the connection to MNS server.
    167      * Call this when the MAS client requests a de-registration on events.
    168      */
    169     public synchronized void disconnect() {
    170         try {
    171             if (mClientSession != null) {
    172                 mClientSession.disconnect(null);
    173                 if (D) {
    174                     Log.d(TAG, "OBEX session disconnected");
    175                 }
    176             }
    177         } catch (IOException e) {
    178             Log.w(TAG, "OBEX session disconnect error " + e.getMessage());
    179         }
    180         try {
    181             if (mClientSession != null) {
    182                 if (D) {
    183                     Log.d(TAG, "OBEX session close mClientSession");
    184                 }
    185                 mClientSession.close();
    186                 mClientSession = null;
    187                 if (D) {
    188                     Log.d(TAG, "OBEX session closed");
    189                 }
    190             }
    191         } catch (IOException e) {
    192             Log.w(TAG, "OBEX session close error:" + e.getMessage());
    193         }
    194         if (mTransport != null) {
    195             try {
    196                 if (D) {
    197                     Log.d(TAG, "Close Obex Transport");
    198                 }
    199                 mTransport.close();
    200                 mTransport = null;
    201                 mConnected = false;
    202                 if (D) {
    203                     Log.d(TAG, "Obex Transport Closed");
    204                 }
    205             } catch (IOException e) {
    206                 Log.e(TAG, "mTransport.close error: " + e.getMessage());
    207             }
    208         }
    209     }
    210 
    211     /**
    212      * Shutdown the MNS.
    213      */
    214     public synchronized void shutdown() {
    215         /* should shutdown handler thread first to make sure
    216          * handleRegistration won't be called when disconnect
    217          */
    218         if (mHandler != null) {
    219             // Shut down the thread
    220             mHandler.removeCallbacksAndMessages(null);
    221             Looper looper = mHandler.getLooper();
    222             if (looper != null) {
    223                 looper.quit();
    224             }
    225             mHandler = null;
    226         }
    227 
    228         /* Disconnect if connected */
    229         disconnect();
    230 
    231         mRegisteredMasIds.clear();
    232     }
    233 
    234     /**
    235      * We store a list of registered MasIds only to control connect/disconnect
    236      * @param masId
    237      * @param notificationStatus
    238      */
    239     public synchronized void handleRegistration(int masId, int notificationStatus) {
    240         if (D) {
    241             Log.d(TAG, "handleRegistration( " + masId + ", " + notificationStatus + ")");
    242         }
    243         boolean sendObserverRegistration = true;
    244         if (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_NO) {
    245             mRegisteredMasIds.delete(masId);
    246             if (mMnsLstRegRqst != null && mMnsLstRegRqst.lastMasId == masId) {
    247                 //Clear last saved MNSSdpSearchInfo , if Disconnect requested for same MasId.
    248                 mMnsLstRegRqst = null;
    249             }
    250         } else if (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) {
    251             /* Connect if we do not have a connection, and start the content observers providing
    252              * this thread as Handler.
    253              */
    254             if (!isConnected()) {
    255                 if (D) {
    256                     Log.d(TAG, "handleRegistration: connect");
    257                 }
    258                 connect();
    259             }
    260             sendObserverRegistration = isConnected();
    261             mRegisteredMasIds.put(masId, true); // We don't use the value for anything
    262 
    263             // Clear last saved MNSSdpSearchInfo after connect is processed.
    264             mMnsLstRegRqst = null;
    265         }
    266 
    267         if (mRegisteredMasIds.size() == 0) {
    268             // No more registrations - disconnect
    269             if (D) {
    270                 Log.d(TAG, "handleRegistration: disconnect");
    271             }
    272             disconnect();
    273         }
    274 
    275         //Register ContentObserver After connect/disconnect MNS channel.
    276         if (V) {
    277             Log.v(TAG, "Send  registerObserver: " + sendObserverRegistration);
    278         }
    279         if (mCallback != null && sendObserverRegistration) {
    280             Message msg = Message.obtain(mCallback);
    281             msg.what = BluetoothMapService.MSG_OBSERVER_REGISTRATION;
    282             msg.arg1 = masId;
    283             msg.arg2 = notificationStatus;
    284             msg.sendToTarget();
    285         }
    286     }
    287 
    288     public boolean isValidMnsRecord() {
    289         return (mMnsRecord != null);
    290     }
    291 
    292     public void setMnsRecord(SdpMnsRecord mnsRecord) {
    293         if (V) {
    294             Log.v(TAG, "setMNSRecord");
    295         }
    296         if (isValidMnsRecord()) {
    297             Log.w(TAG, "MNS Record already available. Still update.");
    298         }
    299         mMnsRecord = mnsRecord;
    300         if (mMnsLstRegRqst != null) {
    301             //SDP Search completed.
    302             mMnsLstRegRqst.setIsSearchInProgress(false);
    303             if (mHandler.hasMessages(MSG_MNS_NOTIFICATION_REGISTRATION)) {
    304                 mHandler.removeMessages(MSG_MNS_NOTIFICATION_REGISTRATION);
    305                 //Search Result obtained within MNS_SDP_SEARCH_DELAY timeout
    306                 if (!isValidMnsRecord()) {
    307                     // SDP info still not available for last trial.
    308                     // Clear saved info.
    309                     mMnsLstRegRqst = null;
    310                 } else {
    311                     if (V) {
    312                         Log.v(TAG, "Handle registration for last saved request");
    313                     }
    314                     Message msgReg = mHandler.obtainMessage(MSG_MNS_NOTIFICATION_REGISTRATION);
    315                     msgReg.arg1 = mMnsLstRegRqst.lastMasId;
    316                     msgReg.arg2 = mMnsLstRegRqst.lastNotificationStatus;
    317                     if (V) {
    318                         Log.v(TAG, "SearchReg  masId:  " + msgReg.arg1 + " notfStatus: "
    319                                 + msgReg.arg2);
    320                     }
    321                     //Handle notification registration.
    322                     mHandler.sendMessageDelayed(msgReg, MNS_NOTIFICATION_DELAY);
    323                 }
    324             }
    325         } else {
    326             if (V) {
    327                 Log.v(TAG, "No last saved MNSSDPInfo to handle");
    328             }
    329         }
    330     }
    331 
    332     public void connect() {
    333 
    334         mConnected = true;
    335 
    336         BluetoothSocket btSocket = null;
    337         try {
    338             // TODO: Do SDP record search again?
    339             if (isValidMnsRecord() && mMnsRecord.getL2capPsm() > 0) {
    340                 // Do L2CAP connect
    341                 btSocket = mRemoteDevice.createL2capSocket(mMnsRecord.getL2capPsm());
    342 
    343             } else if (isValidMnsRecord() && mMnsRecord.getRfcommChannelNumber() > 0) {
    344                 // Do Rfcomm connect
    345                 btSocket = mRemoteDevice.createRfcommSocket(mMnsRecord.getRfcommChannelNumber());
    346             } else {
    347                 // This should not happen...
    348                 Log.e(TAG, "Invalid SDP content - attempt a connect to UUID...");
    349                 // TODO: Why insecure? - is it because the link is already encrypted?
    350                 btSocket = mRemoteDevice.createInsecureRfcommSocketToServiceRecord(
    351                         BLUETOOTH_UUID_OBEX_MNS.getUuid());
    352             }
    353             btSocket.connect();
    354         } catch (IOException e) {
    355             Log.e(TAG, "BtSocket Connect error " + e.getMessage(), e);
    356             // TODO: do we need to report error somewhere?
    357             mConnected = false;
    358             return;
    359         }
    360 
    361         mTransport = new BluetoothObexTransport(btSocket);
    362 
    363         try {
    364             mClientSession = new ClientSession(mTransport);
    365         } catch (IOException e1) {
    366             Log.e(TAG, "OBEX session create error " + e1.getMessage());
    367             mConnected = false;
    368         }
    369         if (mConnected && mClientSession != null) {
    370             boolean connected = false;
    371             HeaderSet hs = new HeaderSet();
    372             // bb582b41-420c-11db-b0de-0800200c9a66
    373             byte[] mnsTarget = {
    374                     (byte) 0xbb,
    375                     (byte) 0x58,
    376                     (byte) 0x2b,
    377                     (byte) 0x41,
    378                     (byte) 0x42,
    379                     (byte) 0x0c,
    380                     (byte) 0x11,
    381                     (byte) 0xdb,
    382                     (byte) 0xb0,
    383                     (byte) 0xde,
    384                     (byte) 0x08,
    385                     (byte) 0x00,
    386                     (byte) 0x20,
    387                     (byte) 0x0c,
    388                     (byte) 0x9a,
    389                     (byte) 0x66
    390             };
    391             hs.setHeader(HeaderSet.TARGET, mnsTarget);
    392 
    393             synchronized (this) {
    394                 mWaitingForRemote = true;
    395             }
    396             try {
    397                 mHsConnect = mClientSession.connect(hs);
    398                 if (D) {
    399                     Log.d(TAG, "OBEX session created");
    400                 }
    401                 connected = true;
    402             } catch (IOException e) {
    403                 Log.e(TAG, "OBEX session connect error " + e.getMessage());
    404             }
    405             mConnected = connected;
    406         }
    407         synchronized (this) {
    408             mWaitingForRemote = false;
    409         }
    410     }
    411 
    412     /**
    413      * Call this method to queue an event report to be send to the MNS server.
    414      * @param eventBytes the encoded event data.
    415      * @param masInstanceId the MasId of the instance sending the event.
    416      */
    417     public void sendEvent(byte[] eventBytes, int masInstanceId) {
    418         // We need to check for null, to handle shutdown.
    419         if (mHandler != null) {
    420             Message msg = mHandler.obtainMessage(MSG_MNS_SEND_EVENT, masInstanceId, 0, eventBytes);
    421             if (msg != null) {
    422                 msg.sendToTarget();
    423             }
    424         }
    425         notifyUpdateWakeLock();
    426     }
    427 
    428     private void notifyMnsSdpSearch() {
    429         if (mCallback != null) {
    430             Message msg = Message.obtain(mCallback);
    431             msg.what = BluetoothMapService.MSG_MNS_SDP_SEARCH;
    432             msg.sendToTarget();
    433         }
    434     }
    435 
    436     private int sendEventHandler(byte[] eventBytes, int masInstanceId) {
    437 
    438         boolean error = false;
    439         int responseCode = -1;
    440         HeaderSet request;
    441         int maxChunkSize, bytesToWrite, bytesWritten = 0;
    442         ClientSession clientSession = mClientSession;
    443 
    444         if ((!mConnected) || (clientSession == null)) {
    445             Log.w(TAG, "sendEvent after disconnect:" + mConnected);
    446             return responseCode;
    447         }
    448 
    449         request = new HeaderSet();
    450         BluetoothMapAppParams appParams = new BluetoothMapAppParams();
    451         appParams.setMasInstanceId(masInstanceId);
    452 
    453         ClientOperation putOperation = null;
    454         OutputStream outputStream = null;
    455 
    456         try {
    457             request.setHeader(HeaderSet.TYPE, TYPE_EVENT);
    458             request.setHeader(HeaderSet.APPLICATION_PARAMETER, appParams.encodeParams());
    459 
    460             if (mHsConnect.mConnectionID != null) {
    461                 request.mConnectionID = new byte[4];
    462                 System.arraycopy(mHsConnect.mConnectionID, 0, request.mConnectionID, 0, 4);
    463             } else {
    464                 Log.w(TAG, "sendEvent: no connection ID");
    465             }
    466 
    467             synchronized (this) {
    468                 mWaitingForRemote = true;
    469             }
    470             // Send the header first and then the body
    471             try {
    472                 if (V) {
    473                     Log.v(TAG, "Send headerset Event ");
    474                 }
    475                 putOperation = (ClientOperation) clientSession.put(request);
    476                 // TODO - Should this be kept or Removed
    477 
    478             } catch (IOException e) {
    479                 Log.e(TAG, "Error when put HeaderSet " + e.getMessage());
    480                 error = true;
    481             }
    482             synchronized (this) {
    483                 mWaitingForRemote = false;
    484             }
    485             if (!error) {
    486                 try {
    487                     if (V) {
    488                         Log.v(TAG, "Send headerset Event ");
    489                     }
    490                     outputStream = putOperation.openOutputStream();
    491                 } catch (IOException e) {
    492                     Log.e(TAG, "Error when opening OutputStream " + e.getMessage());
    493                     error = true;
    494                 }
    495             }
    496 
    497             if (!error) {
    498 
    499                 maxChunkSize = putOperation.getMaxPacketSize();
    500 
    501                 while (bytesWritten < eventBytes.length) {
    502                     bytesToWrite = Math.min(maxChunkSize, eventBytes.length - bytesWritten);
    503                     outputStream.write(eventBytes, bytesWritten, bytesToWrite);
    504                     bytesWritten += bytesToWrite;
    505                 }
    506 
    507                 if (bytesWritten == eventBytes.length) {
    508                     Log.i(TAG, "SendEvent finished send length" + eventBytes.length);
    509                 } else {
    510                     error = true;
    511                     putOperation.abort();
    512                     Log.i(TAG, "SendEvent interrupted");
    513                 }
    514             }
    515         } catch (IOException e) {
    516             handleSendException(e.toString());
    517             error = true;
    518         } catch (IndexOutOfBoundsException e) {
    519             handleSendException(e.toString());
    520             error = true;
    521         } finally {
    522             try {
    523                 if (outputStream != null) {
    524                     outputStream.close();
    525                 }
    526             } catch (IOException e) {
    527                 Log.e(TAG, "Error when closing stream after send " + e.getMessage());
    528             }
    529             try {
    530                 if ((!error) && (putOperation != null)) {
    531                     responseCode = putOperation.getResponseCode();
    532                     if (responseCode != -1) {
    533                         if (V) {
    534                             Log.v(TAG, "Put response code " + responseCode);
    535                         }
    536                         if (responseCode != ResponseCodes.OBEX_HTTP_OK) {
    537                             Log.i(TAG, "Response error code is " + responseCode);
    538                         }
    539                     }
    540                 }
    541                 if (putOperation != null) {
    542                     putOperation.close();
    543                 }
    544             } catch (IOException e) {
    545                 Log.e(TAG, "Error when closing stream after send " + e.getMessage());
    546             }
    547         }
    548 
    549         return responseCode;
    550     }
    551 
    552     private void handleSendException(String exception) {
    553         Log.e(TAG, "Error when sending event: " + exception);
    554     }
    555 
    556     private void notifyUpdateWakeLock() {
    557         if (mCallback != null) {
    558             Message msg = Message.obtain(mCallback);
    559             msg.what = BluetoothMapService.MSG_ACQUIRE_WAKE_LOCK;
    560             msg.sendToTarget();
    561         }
    562     }
    563 }
    564