/* * 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. * *

BluetoothHealth is a proxy object for controlling the Bluetooth * Service via IPC. * *

How to connect to a health device which is acting in the source role. *

  • Use {@link BluetoothAdapter#getProfileProxy} to get * the BluetoothHealth proxy object.
  • *
  • Create an {@link BluetoothHealth} callback and call * {@link #registerSinkAppConfiguration} to register an application * configuration
  • *
  • Pair with the remote device. This currently needs to be done manually * from Bluetooth Settings
  • *
  • 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.
  • *
  • Use the file descriptor provided with a connected channel to read and * write data to the health channel.
  • *
  • The received data needs to be interpreted using a health manager which * implements the IEEE 11073-xxxxx specifications. *
  • 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. * *

    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. * *

    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} * *

    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. * *

    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. * *

    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. * *

    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. * *

    Requires {@link android.Manifest.permission#BLUETOOTH} permission. * *

    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. * *

    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. * *

    Return the set of devices which are in state {@link #STATE_CONNECTED} * *

    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 getConnectedDevices() { if (mService != null && isEnabled()) { try { return mService.getConnectedHealthDevices(); } catch (RemoteException e) { Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); return new ArrayList(); } } if (mService == null) Log.w(TAG, "Proxy not attached to service"); return new ArrayList(); } /** * Get a list of devices that match any of the given connection * states. * *

    If none of the devices match any of the given states, * an empty list will be returned. * *

    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 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(); } } if (mService == null) Log.w(TAG, "Proxy not attached to service"); return new ArrayList(); } 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); } }