/* * Copyright (C) 2014 Samsung System LSI * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bluetooth.map; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.bluetooth.SdpMnsRecord; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.ParcelUuid; import android.util.Log; import android.util.SparseBooleanArray; import com.android.bluetooth.BluetoothObexTransport; import java.io.IOException; import java.io.OutputStream; import javax.obex.ClientOperation; import javax.obex.ClientSession; import javax.obex.HeaderSet; import javax.obex.ObexTransport; import javax.obex.ResponseCodes; /** * The Message Notification Service class runs its own message handler thread, * to avoid executing long operations on the MAP service Thread. * This handler context is passed to the content observers, * hence all call-backs (and thereby transmission of data) is executed * from this thread. */ public class BluetoothMnsObexClient { private static final String TAG = "BluetoothMnsObexClient"; private static final boolean D = BluetoothMapService.DEBUG; private static final boolean V = BluetoothMapService.VERBOSE; private ObexTransport mTransport; public Handler mHandler = null; private volatile boolean mWaitingForRemote; private static final String TYPE_EVENT = "x-bt/MAP-event-report"; private ClientSession mClientSession; private boolean mConnected = false; BluetoothDevice mRemoteDevice; private SparseBooleanArray mRegisteredMasIds = new SparseBooleanArray(1); private HeaderSet mHsConnect = null; private Handler mCallback = null; private SdpMnsRecord mMnsRecord; // Used by the MAS to forward notification registrations public static final int MSG_MNS_NOTIFICATION_REGISTRATION = 1; public static final int MSG_MNS_SEND_EVENT = 2; public static final int MSG_MNS_SDP_SEARCH_REGISTRATION = 3; //Copy SdpManager.SDP_INTENT_DELAY - The timeout to wait for reply from native. private static final int MNS_SDP_SEARCH_DELAY = 6000; public MnsSdpSearchInfo mMnsLstRegRqst = null; private static final int MNS_NOTIFICATION_DELAY = 10; public static final ParcelUuid BLUETOOTH_UUID_OBEX_MNS = ParcelUuid.fromString("00001133-0000-1000-8000-00805F9B34FB"); public BluetoothMnsObexClient(BluetoothDevice remoteDevice, SdpMnsRecord mnsRecord, Handler callback) { if (remoteDevice == null) { throw new NullPointerException("Obex transport is null"); } mRemoteDevice = remoteDevice; HandlerThread thread = new HandlerThread("BluetoothMnsObexClient"); thread.start(); /* This will block until the looper have started, hence it will be safe to use it, when the constructor completes */ Looper looper = thread.getLooper(); mHandler = new MnsObexClientHandler(looper); mCallback = callback; mMnsRecord = mnsRecord; } public Handler getMessageHandler() { return mHandler; } class MnsSdpSearchInfo { private boolean mIsSearchInProgress; public int lastMasId; public int lastNotificationStatus; MnsSdpSearchInfo(boolean isSearchON, int masId, int notification) { mIsSearchInProgress = isSearchON; lastMasId = masId; lastNotificationStatus = notification; } public boolean isSearchInProgress() { return mIsSearchInProgress; } public void setIsSearchInProgress(boolean isSearchON) { mIsSearchInProgress = isSearchON; } } private final class MnsObexClientHandler extends Handler { private MnsObexClientHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_MNS_NOTIFICATION_REGISTRATION: if (V) { Log.v(TAG, "Reg masId: " + msg.arg1 + " notfStatus: " + msg.arg2); } if (isValidMnsRecord()) { handleRegistration(msg.arg1 /*masId*/, msg.arg2 /*status*/); } else { //Should not happen if (D) { Log.d(TAG, "MNS SDP info not available yet - Cannot Connect."); } } break; case MSG_MNS_SEND_EVENT: sendEventHandler((byte[]) msg.obj/*byte[]*/, msg.arg1 /*masId*/); break; case MSG_MNS_SDP_SEARCH_REGISTRATION: //Initiate SDP Search notifyMnsSdpSearch(); //Save the mns search info mMnsLstRegRqst = new MnsSdpSearchInfo(true, msg.arg1, msg.arg2); //Handle notification registration. Message msgReg = mHandler.obtainMessage(MSG_MNS_NOTIFICATION_REGISTRATION, msg.arg1, msg.arg2); if (V) { Log.v(TAG, "SearchReg masId: " + msg.arg1 + " notfStatus: " + msg.arg2); } mHandler.sendMessageDelayed(msgReg, MNS_SDP_SEARCH_DELAY); break; default: break; } } } public boolean isConnected() { return mConnected; } /** * Disconnect the connection to MNS server. * Call this when the MAS client requests a de-registration on events. */ public synchronized void disconnect() { try { if (mClientSession != null) { mClientSession.disconnect(null); if (D) { Log.d(TAG, "OBEX session disconnected"); } } } catch (IOException e) { Log.w(TAG, "OBEX session disconnect error " + e.getMessage()); } try { if (mClientSession != null) { if (D) { Log.d(TAG, "OBEX session close mClientSession"); } mClientSession.close(); mClientSession = null; if (D) { Log.d(TAG, "OBEX session closed"); } } } catch (IOException e) { Log.w(TAG, "OBEX session close error:" + e.getMessage()); } if (mTransport != null) { try { if (D) { Log.d(TAG, "Close Obex Transport"); } mTransport.close(); mTransport = null; mConnected = false; if (D) { Log.d(TAG, "Obex Transport Closed"); } } catch (IOException e) { Log.e(TAG, "mTransport.close error: " + e.getMessage()); } } } /** * Shutdown the MNS. */ public synchronized void shutdown() { /* should shutdown handler thread first to make sure * handleRegistration won't be called when disconnect */ if (mHandler != null) { // Shut down the thread mHandler.removeCallbacksAndMessages(null); Looper looper = mHandler.getLooper(); if (looper != null) { looper.quit(); } } /* Disconnect if connected */ disconnect(); mRegisteredMasIds.clear(); } /** * We store a list of registered MasIds only to control connect/disconnect * @param masId * @param notificationStatus */ public synchronized void handleRegistration(int masId, int notificationStatus) { if (D) { Log.d(TAG, "handleRegistration( " + masId + ", " + notificationStatus + ")"); } boolean sendObserverRegistration = true; if (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_NO) { mRegisteredMasIds.delete(masId); if (mMnsLstRegRqst != null && mMnsLstRegRqst.lastMasId == masId) { //Clear last saved MNSSdpSearchInfo , if Disconnect requested for same MasId. mMnsLstRegRqst = null; } } else if (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) { /* Connect if we do not have a connection, and start the content observers providing * this thread as Handler. */ if (!isConnected()) { if (D) { Log.d(TAG, "handleRegistration: connect"); } connect(); } sendObserverRegistration = isConnected(); mRegisteredMasIds.put(masId, true); // We don't use the value for anything // Clear last saved MNSSdpSearchInfo after connect is processed. mMnsLstRegRqst = null; } if (mRegisteredMasIds.size() == 0) { // No more registrations - disconnect if (D) { Log.d(TAG, "handleRegistration: disconnect"); } disconnect(); } //Register ContentObserver After connect/disconnect MNS channel. if (V) { Log.v(TAG, "Send registerObserver: " + sendObserverRegistration); } if (mCallback != null && sendObserverRegistration) { Message msg = Message.obtain(mCallback); msg.what = BluetoothMapService.MSG_OBSERVER_REGISTRATION; msg.arg1 = masId; msg.arg2 = notificationStatus; msg.sendToTarget(); } } public boolean isValidMnsRecord() { return (mMnsRecord != null); } public void setMnsRecord(SdpMnsRecord mnsRecord) { if (V) { Log.v(TAG, "setMNSRecord"); } if (isValidMnsRecord()) { Log.w(TAG, "MNS Record already available. Still update."); } mMnsRecord = mnsRecord; if (mMnsLstRegRqst != null) { //SDP Search completed. mMnsLstRegRqst.setIsSearchInProgress(false); if (mHandler.hasMessages(MSG_MNS_NOTIFICATION_REGISTRATION)) { mHandler.removeMessages(MSG_MNS_NOTIFICATION_REGISTRATION); //Search Result obtained within MNS_SDP_SEARCH_DELAY timeout if (!isValidMnsRecord()) { // SDP info still not available for last trial. // Clear saved info. mMnsLstRegRqst = null; } else { if (V) { Log.v(TAG, "Handle registration for last saved request"); } Message msgReg = mHandler.obtainMessage(MSG_MNS_NOTIFICATION_REGISTRATION); msgReg.arg1 = mMnsLstRegRqst.lastMasId; msgReg.arg2 = mMnsLstRegRqst.lastNotificationStatus; if (V) { Log.v(TAG, "SearchReg masId: " + msgReg.arg1 + " notfStatus: " + msgReg.arg2); } //Handle notification registration. mHandler.sendMessageDelayed(msgReg, MNS_NOTIFICATION_DELAY); } } } else { if (V) { Log.v(TAG, "No last saved MNSSDPInfo to handle"); } } } public void connect() { mConnected = true; BluetoothSocket btSocket = null; try { // TODO: Do SDP record search again? if (isValidMnsRecord() && mMnsRecord.getL2capPsm() > 0) { // Do L2CAP connect btSocket = mRemoteDevice.createL2capSocket(mMnsRecord.getL2capPsm()); } else if (isValidMnsRecord() && mMnsRecord.getRfcommChannelNumber() > 0) { // Do Rfcomm connect btSocket = mRemoteDevice.createRfcommSocket(mMnsRecord.getRfcommChannelNumber()); } else { // This should not happen... Log.e(TAG, "Invalid SDP content - attempt a connect to UUID..."); // TODO: Why insecure? - is it because the link is already encrypted? btSocket = mRemoteDevice.createInsecureRfcommSocketToServiceRecord( BLUETOOTH_UUID_OBEX_MNS.getUuid()); } btSocket.connect(); } catch (IOException e) { Log.e(TAG, "BtSocket Connect error " + e.getMessage(), e); // TODO: do we need to report error somewhere? mConnected = false; return; } mTransport = new BluetoothObexTransport(btSocket); try { mClientSession = new ClientSession(mTransport); } catch (IOException e1) { Log.e(TAG, "OBEX session create error " + e1.getMessage()); mConnected = false; } if (mConnected && mClientSession != null) { boolean connected = false; HeaderSet hs = new HeaderSet(); // bb582b41-420c-11db-b0de-0800200c9a66 byte[] mnsTarget = { (byte) 0xbb, (byte) 0x58, (byte) 0x2b, (byte) 0x41, (byte) 0x42, (byte) 0x0c, (byte) 0x11, (byte) 0xdb, (byte) 0xb0, (byte) 0xde, (byte) 0x08, (byte) 0x00, (byte) 0x20, (byte) 0x0c, (byte) 0x9a, (byte) 0x66 }; hs.setHeader(HeaderSet.TARGET, mnsTarget); synchronized (this) { mWaitingForRemote = true; } try { mHsConnect = mClientSession.connect(hs); if (D) { Log.d(TAG, "OBEX session created"); } connected = true; } catch (IOException e) { Log.e(TAG, "OBEX session connect error " + e.getMessage()); } mConnected = connected; } synchronized (this) { mWaitingForRemote = false; } } /** * Call this method to queue an event report to be send to the MNS server. * @param eventBytes the encoded event data. * @param masInstanceId the MasId of the instance sending the event. */ public void sendEvent(byte[] eventBytes, int masInstanceId) { // We need to check for null, to handle shutdown. if (mHandler != null) { Message msg = mHandler.obtainMessage(MSG_MNS_SEND_EVENT, masInstanceId, 0, eventBytes); if (msg != null) { msg.sendToTarget(); } } notifyUpdateWakeLock(); } private void notifyMnsSdpSearch() { if (mCallback != null) { Message msg = Message.obtain(mCallback); msg.what = BluetoothMapService.MSG_MNS_SDP_SEARCH; msg.sendToTarget(); } } private int sendEventHandler(byte[] eventBytes, int masInstanceId) { boolean error = false; int responseCode = -1; HeaderSet request; int maxChunkSize, bytesToWrite, bytesWritten = 0; ClientSession clientSession = mClientSession; if ((!mConnected) || (clientSession == null)) { Log.w(TAG, "sendEvent after disconnect:" + mConnected); return responseCode; } request = new HeaderSet(); BluetoothMapAppParams appParams = new BluetoothMapAppParams(); appParams.setMasInstanceId(masInstanceId); ClientOperation putOperation = null; OutputStream outputStream = null; try { request.setHeader(HeaderSet.TYPE, TYPE_EVENT); request.setHeader(HeaderSet.APPLICATION_PARAMETER, appParams.encodeParams()); if (mHsConnect.mConnectionID != null) { request.mConnectionID = new byte[4]; System.arraycopy(mHsConnect.mConnectionID, 0, request.mConnectionID, 0, 4); } else { Log.w(TAG, "sendEvent: no connection ID"); } synchronized (this) { mWaitingForRemote = true; } // Send the header first and then the body try { if (V) { Log.v(TAG, "Send headerset Event "); } putOperation = (ClientOperation) clientSession.put(request); // TODO - Should this be kept or Removed } catch (IOException e) { Log.e(TAG, "Error when put HeaderSet " + e.getMessage()); error = true; } synchronized (this) { mWaitingForRemote = false; } if (!error) { try { if (V) { Log.v(TAG, "Send headerset Event "); } outputStream = putOperation.openOutputStream(); } catch (IOException e) { Log.e(TAG, "Error when opening OutputStream " + e.getMessage()); error = true; } } if (!error) { maxChunkSize = putOperation.getMaxPacketSize(); while (bytesWritten < eventBytes.length) { bytesToWrite = Math.min(maxChunkSize, eventBytes.length - bytesWritten); outputStream.write(eventBytes, bytesWritten, bytesToWrite); bytesWritten += bytesToWrite; } if (bytesWritten == eventBytes.length) { Log.i(TAG, "SendEvent finished send length" + eventBytes.length); } else { error = true; putOperation.abort(); Log.i(TAG, "SendEvent interrupted"); } } } catch (IOException e) { handleSendException(e.toString()); error = true; } catch (IndexOutOfBoundsException e) { handleSendException(e.toString()); error = true; } finally { try { if (outputStream != null) { outputStream.close(); } } catch (IOException e) { Log.e(TAG, "Error when closing stream after send " + e.getMessage()); } try { if ((!error) && (putOperation != null)) { responseCode = putOperation.getResponseCode(); if (responseCode != -1) { if (V) { Log.v(TAG, "Put response code " + responseCode); } if (responseCode != ResponseCodes.OBEX_HTTP_OK) { Log.i(TAG, "Response error code is " + responseCode); } } } if (putOperation != null) { putOperation.close(); } } catch (IOException e) { Log.e(TAG, "Error when closing stream after send " + e.getMessage()); } } return responseCode; } private void handleSendException(String exception) { Log.e(TAG, "Error when sending event: " + exception); } private void notifyUpdateWakeLock() { if (mCallback != null) { Message msg = Message.obtain(mCallback); msg.what = BluetoothMapService.MSG_ACQUIRE_WAKE_LOCK; msg.sendToTarget(); } } }