blob: 25586417977c995168200e06c834f8f068dd9113 [file] [log] [blame]
/*
* 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 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 isSearchInProgress;
int lastMasId;
int lastNotificationStatus;
MnsSdpSearchInfo (boolean isSearchON, int masId, int notification) {
isSearchInProgress = isSearchON;
lastMasId = masId;
lastNotificationStatus = notification;
}
public boolean isSearchInProgress() {
return isSearchInProgress;
}
public void setIsSearchInProgress(boolean isSearchON) {
isSearchInProgress = 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();
}
mHandler = null;
}
/* 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() == false) {
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();
}
}
}