/*
* Copyright (C) 2011 The Android Open Source Project
*
* 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 android.bluetooth;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
/**
* Public API for Bluetooth Health Profile.
*
* <p>BluetoothHealth is a proxy object for controlling the Bluetooth
* Service via IPC.
*
* <p> How to connect to a health device which is acting in the source role.
* <li> Use {@link BluetoothAdapter#getProfileProxy} to get
* the BluetoothHealth proxy object. </li>
* <li> Create an {@link BluetoothHealth} callback and call
* {@link #registerSinkAppConfiguration} to register an application
* configuration </li>
* <li> Pair with the remote device. This currently needs to be done manually
* from Bluetooth Settings </li>
* <li> Connect to a health device using {@link #connectChannelToSource}. Some
* devices will connect the channel automatically. The {@link BluetoothHealth}
* callback will inform the application of channel state change. </li>
* <li> Use the file descriptor provided with a connected channel to read and
* write data to the health channel. </li>
* <li> The received data needs to be interpreted using a health manager which
* implements the IEEE 11073-xxxxx specifications.
* <li> When done, close the health channel by calling {@link #disconnectChannel}
* and unregister the application configuration calling
* {@link #unregisterAppConfiguration}
*
*/
public final class BluetoothHealth implements BluetoothProfile {
private static final String TAG = "BluetoothHealth";
private static final boolean DBG = true;
private static final boolean VDBG = false;
/**
* Health Profile Source Role - the health device.
*/
public static final int SOURCE_ROLE = 1 << 0;
/**
* Health Profile Sink Role the device talking to the health device.
*/
public static final int SINK_ROLE = 1 << 1;
/**
* Health Profile - Channel Type used - Reliable
*/
public static final int CHANNEL_TYPE_RELIABLE = 10;
/**
* Health Profile - Channel Type used - Streaming
*/
public static final int CHANNEL_TYPE_STREAMING = 11;
/**
* @hide
*/
public static final int CHANNEL_TYPE_ANY = 12;
/** @hide */
public static final int HEALTH_OPERATION_SUCCESS = 6000;
/** @hide */
public static final int HEALTH_OPERATION_ERROR = 6001;
/** @hide */
public static final int HEALTH_OPERATION_INVALID_ARGS = 6002;
/** @hide */
public static final int HEALTH_OPERATION_GENERIC_FAILURE = 6003;
/** @hide */
public static final int HEALTH_OPERATION_NOT_FOUND = 6004;
/** @hide */
public static final int HEALTH_OPERATION_NOT_ALLOWED = 6005;
final private IBluetoothStateChangeCallback mBluetoothStateChangeCallback =
new IBluetoothStateChangeCallback.Stub() {
public void onBluetoothStateChange(boolean up) {
if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up);
if (!up) {
if (VDBG) Log.d(TAG,"Unbinding service...");
synchronized (mConnection) {
try {
mService = null;
mContext.unbindService(mConnection);
} catch (Exception re) {
Log.e(TAG,"",re);
}
}
} else {
synchronized (mConnection) {
try {
if (mService == null) {
if (VDBG) Log.d(TAG,"Binding service...");
doBind();
}
} catch (Exception re) {
Log.e(TAG,"",re);
}
}
}
}
};
/**
* Register an application configuration that acts as a Health SINK.
* This is the configuration that will be used to communicate with health devices
* which will act as the {@link #SOURCE_ROLE}. This is an asynchronous call and so
* the callback is used to notify success or failure if the function returns true.
*
* <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
*
* @param name The friendly name associated with the application or configuration.
* @param dataType The dataType of the Source role of Health Profile to which
* the sink wants to connect to.
* @param callback A callback to indicate success or failure of the registration and
* all operations done on this application configuration.
* @return If true, callback will be called.
*/
public boolean registerSinkAppConfiguration(String name, int dataType,
BluetoothHealthCallback callback) {
if (!isEnabled() || name == null) return false;
if (VDBG) log("registerSinkApplication(" + name + ":" + dataType + ")");
return registerAppConfiguration(name, dataType, SINK_ROLE,
CHANNEL_TYPE_ANY, callback);
}
/**
* Register an application configuration that acts as a Health SINK or in a Health
* SOURCE role.This is an asynchronous call and so
* the callback is used to notify success or failure if the function returns true.
*
* <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
*
* @param name The friendly name associated with the application or configuration.
* @param dataType The dataType of the Source role of Health Profile.
* @param channelType The channel type. Will be one of
* {@link #CHANNEL_TYPE_RELIABLE} or
* {@link #CHANNEL_TYPE_STREAMING}
* @param callback - A callback to indicate success or failure.
* @return If true, callback will be called.
* @hide
*/
public boolean registerAppConfiguration(String name, int dataType, int role,
int channelType, BluetoothHealthCallback callback) {
boolean result = false;
if (!isEnabled() || !checkAppParam(name, role, channelType, callback)) return result;
if (VDBG) log("registerApplication(" + name + ":" + dataType + ")");
BluetoothHealthCallbackWrapper wrapper = new BluetoothHealthCallbackWrapper(callback);
BluetoothHealthAppConfiguration config =
new BluetoothHealthAppConfiguration(name, dataType, role, channelType);
if (mService != null) {
try {
result = mService.registerAppConfiguration(config, wrapper);
} catch (RemoteException e) {
Log.e(TAG, e.toString());
}
} else {
Log.w(TAG, "Proxy not attached to service");
if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
}
return result;
}
/**
* Unregister an application configuration that has been registered using
* {@link #registerSinkAppConfiguration}
*
* <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
*
* @param config The health app configuration
* @return Success or failure.
*/
public boolean unregisterAppConfiguration(BluetoothHealthAppConfiguration config) {
boolean result = false;
if (mService != null && isEnabled() && config != null) {
try {
result = mService.unregisterAppConfiguration(config);
} catch (RemoteException e) {
Log.e(TAG, e.toString());
}
} else {
Log.w(TAG, "Proxy not attached to service");
if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
}
return result;
}
/**
* Connect to a health device which has the {@link #SOURCE_ROLE}.
* This is an asynchronous call. If this function returns true, the callback
* associated with the application configuration will be called.
*
* <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
*
* @param device The remote Bluetooth device.
* @param config The application configuration which has been registered using
* {@link #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) }
* @return If true, the callback associated with the application config will be called.
*/
public boolean connectChannelToSource(BluetoothDevice device,
BluetoothHealthAppConfiguration config) {
if (mService != null && isEnabled() && isValidDevice(device) &&
config != null) {
try {
return mService.connectChannelToSource(device, config);
} catch (RemoteException e) {
Log.e(TAG, e.toString());
}
} else {
Log.w(TAG, "Proxy not attached to service");
if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
}
return false;
}
/**
* Connect to a health device which has the {@link #SINK_ROLE}.
* This is an asynchronous call. If this function returns true, the callback
* associated with the application configuration will be called.
*
*<p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
*
* @param device The remote Bluetooth device.
* @param config The application configuration which has been registered using
* {@link #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) }
* @return If true, the callback associated with the application config will be called.
* @hide
*/
public boolean connectChannelToSink(BluetoothDevice device,
BluetoothHealthAppConfiguration config, int channelType) {
if (mService != null && isEnabled() && isValidDevice(device) &&
config != null) {
try {
return mService.connectChannelToSink(device, config, channelType);
} catch (RemoteException e) {
Log.e(TAG, e.toString());
}
} else {
Log.w(TAG, "Proxy not attached to service");
if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
}
return false;
}
/**
* Disconnect a connected health channel.
* This is an asynchronous call. If this function returns true, the callback
* associated with the application configuration will be called.
*
*<p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
*
* @param device The remote Bluetooth device.
* @param config The application configuration which has been registered using
* {@link #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) }
* @param channelId The channel id associated with the channel
* @return If true, the callback associated with the application config will be called.
*/
public boolean disconnectChannel(BluetoothDevice device,
BluetoothHealthAppConfiguration config, int channelId) {
if (mService != null && isEnabled() && isValidDevice(device) &&
config != null) {
try {
return mService.disconnectChannel(device, config, channelId);
} catch (RemoteException e) {
Log.e(TAG, e.toString());
}
} else {
Log.w(TAG, "Proxy not attached to service");
if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
}
return false;
}
/**
* Get the file descriptor of the main channel associated with the remote device
* and application configuration.
*
* <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
*
* <p> Its the responsibility of the caller to close the ParcelFileDescriptor
* when done.
*
* @param device The remote Bluetooth health device
* @param config The application configuration
* @return null on failure, ParcelFileDescriptor on success.
*/
public ParcelFileDescriptor getMainChannelFd(BluetoothDevice device,
BluetoothHealthAppConfiguration config) {
if (mService != null && isEnabled() && isValidDevice(device) &&
config != null) {
try {
return mService.getMainChannelFd(device, config);
} catch (RemoteException e) {
Log.e(TAG, e.toString());
}
} else {
Log.w(TAG, "Proxy not attached to service");
if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
}
return null;
}
/**
* Get the current connection state of the profile.
*
* <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
*
* This is not specific to any application configuration but represents the connection
* state of the local Bluetooth adapter with the remote device. This can be used
* by applications like status bar which would just like to know the state of the
* local adapter.
*
* @param device Remote bluetooth device.
* @return State of the profile connection. One of
* {@link #STATE_CONNECTED}, {@link #STATE_CONNECTING},
* {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING}
*/
@Override
public int getConnectionState(BluetoothDevice device) {
if (mService != null && isEnabled() && isValidDevice(device)) {
try {
return mService.getHealthDeviceConnectionState(device);
} catch (RemoteException e) {
Log.e(TAG, e.toString());
}
} else {
Log.w(TAG, "Proxy not attached to service");
if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
}
return STATE_DISCONNECTED;
}
/**
* Get connected devices for the health profile.
*
* <p> Return the set of devices which are in state {@link #STATE_CONNECTED}
*
* <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
*
* This is not specific to any application configuration but represents the connection
* state of the local Bluetooth adapter for this profile. This can be used
* by applications like status bar which would just like to know the state of the
* local adapter.
* @return List of devices. The list will be empty on error.
*/
@Override
public List<BluetoothDevice> getConnectedDevices() {
if (mService != null && isEnabled()) {
try {
return mService.getConnectedHealthDevices();
} catch (RemoteException e) {
Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
return new ArrayList<BluetoothDevice>();
}
}
if (mService == null) Log.w(TAG, "Proxy not attached to service");
return new ArrayList<BluetoothDevice>();
}
/**
* Get a list of devices that match any of the given connection
* states.
*
* <p> If none of the devices match any of the given states,
* an empty list will be returned.
*
* <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
* This is not specific to any application configuration but represents the connection
* state of the local Bluetooth adapter for this profile. This can be used
* by applications like status bar which would just like to know the state of the
* local adapter.
*
* @param states Array of states. States can be one of
* {@link #STATE_CONNECTED}, {@link #STATE_CONNECTING},
* {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING},
* @return List of devices. The list will be empty on error.
*/
@Override
public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
if (mService != null && isEnabled()) {
try {
return mService.getHealthDevicesMatchingConnectionStates(states);
} catch (RemoteException e) {
Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
return new ArrayList<BluetoothDevice>();
}
}
if (mService == null) Log.w(TAG, "Proxy not attached to service");
return new ArrayList<BluetoothDevice>();
}
private static class BluetoothHealthCallbackWrapper extends IBluetoothHealthCallback.Stub {
private BluetoothHealthCallback mCallback;
public BluetoothHealthCallbackWrapper(BluetoothHealthCallback callback) {
mCallback = callback;
}
@Override
public void onHealthAppConfigurationStatusChange(BluetoothHealthAppConfiguration config,
int status) {
mCallback.onHealthAppConfigurationStatusChange(config, status);
}
@Override
public void onHealthChannelStateChange(BluetoothHealthAppConfiguration config,
BluetoothDevice device, int prevState, int newState,
ParcelFileDescriptor fd, int channelId) {
mCallback.onHealthChannelStateChange(config, device, prevState, newState, fd,
channelId);
}
}
/** Health Channel Connection State - Disconnected */
public static final int STATE_CHANNEL_DISCONNECTED = 0;
/** Health Channel Connection State - Connecting */
public static final int STATE_CHANNEL_CONNECTING = 1;
/** Health Channel Connection State - Connected */
public static final int STATE_CHANNEL_CONNECTED = 2;
/** Health Channel Connection State - Disconnecting */
public static final int STATE_CHANNEL_DISCONNECTING = 3;
/** Health App Configuration registration success */
public static final int APP_CONFIG_REGISTRATION_SUCCESS = 0;
/** Health App Configuration registration failure */
public static final int APP_CONFIG_REGISTRATION_FAILURE = 1;
/** Health App Configuration un-registration success */
public static final int APP_CONFIG_UNREGISTRATION_SUCCESS = 2;
/** Health App Configuration un-registration failure */
public static final int APP_CONFIG_UNREGISTRATION_FAILURE = 3;
private Context mContext;
private ServiceListener mServiceListener;
private IBluetoothHealth mService;
BluetoothAdapter mAdapter;
/**
* Create a BluetoothHealth proxy object.
*/
/*package*/ BluetoothHealth(Context context, ServiceListener l) {
mContext = context;
mServiceListener = l;
mAdapter = BluetoothAdapter.getDefaultAdapter();
IBluetoothManager mgr = mAdapter.getBluetoothManager();
if (mgr != null) {
try {
mgr.registerStateChangeCallback(mBluetoothStateChangeCallback);
} catch (RemoteException e) {
Log.e(TAG,"",e);
}
}
doBind();
}
boolean doBind() {
Intent intent = new Intent(IBluetoothHealth.class.getName());
ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0);
intent.setComponent(comp);
if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0,
android.os.Process.myUserHandle())) {
Log.e(TAG, "Could not bind to Bluetooth Health Service with " + intent);
return false;
}
return true;
}
/*package*/ void close() {
if (VDBG) log("close()");
IBluetoothManager mgr = mAdapter.getBluetoothManager();
if (mgr != null) {
try {
mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback);
} catch (Exception e) {
Log.e(TAG,"",e);
}
}
synchronized (mConnection) {
if (mService != null) {
try {
mService = null;
mContext.unbindService(mConnection);
} catch (Exception re) {
Log.e(TAG,"",re);
}
}
}
mServiceListener = null;
}
private final ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
if (DBG) Log.d(TAG, "Proxy object connected");
mService = IBluetoothHealth.Stub.asInterface(service);
if (mServiceListener != null) {
mServiceListener.onServiceConnected(BluetoothProfile.HEALTH, BluetoothHealth.this);
}
}
public void onServiceDisconnected(ComponentName className) {
if (DBG) Log.d(TAG, "Proxy object disconnected");
mService = null;
if (mServiceListener != null) {
mServiceListener.onServiceDisconnected(BluetoothProfile.HEALTH);
}
}
};
private boolean isEnabled() {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter != null && adapter.getState() == BluetoothAdapter.STATE_ON) return true;
log("Bluetooth is Not enabled");
return false;
}
private boolean isValidDevice(BluetoothDevice device) {
if (device == null) return false;
if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
return false;
}
private boolean checkAppParam(String name, int role, int channelType,
BluetoothHealthCallback callback) {
if (name == null || (role != SOURCE_ROLE && role != SINK_ROLE) ||
(channelType != CHANNEL_TYPE_RELIABLE &&
channelType != CHANNEL_TYPE_STREAMING &&
channelType != CHANNEL_TYPE_ANY) || callback == null) {
return false;
}
if (role == SOURCE_ROLE && channelType == CHANNEL_TYPE_ANY) return false;
return true;
}
private static void log(String msg) {
Log.d(TAG, msg);
}
}