1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.bluetooth; 18 19 import android.content.ComponentName; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.ServiceConnection; 23 import android.os.IBinder; 24 import android.os.ParcelFileDescriptor; 25 import android.os.RemoteException; 26 import android.os.ServiceManager; 27 import android.util.Log; 28 29 import java.util.ArrayList; 30 import java.util.List; 31 32 /** 33 * Public API for Bluetooth Health Profile. 34 * 35 * <p>BluetoothHealth is a proxy object for controlling the Bluetooth 36 * Service via IPC. 37 * 38 * <p> How to connect to a health device which is acting in the source role. 39 * <li> Use {@link BluetoothAdapter#getProfileProxy} to get 40 * the BluetoothHealth proxy object. </li> 41 * <li> Create an {@link BluetoothHealth} callback and call 42 * {@link #registerSinkAppConfiguration} to register an application 43 * configuration </li> 44 * <li> Pair with the remote device. This currently needs to be done manually 45 * from Bluetooth Settings </li> 46 * <li> Connect to a health device using {@link #connectChannelToSource}. Some 47 * devices will connect the channel automatically. The {@link BluetoothHealth} 48 * callback will inform the application of channel state change. </li> 49 * <li> Use the file descriptor provided with a connected channel to read and 50 * write data to the health channel. </li> 51 * <li> The received data needs to be interpreted using a health manager which 52 * implements the IEEE 11073-xxxxx specifications. 53 * <li> When done, close the health channel by calling {@link #disconnectChannel} 54 * and unregister the application configuration calling 55 * {@link #unregisterAppConfiguration} 56 * 57 */ 58 public final class BluetoothHealth implements BluetoothProfile { 59 private static final String TAG = "BluetoothHealth"; 60 private static final boolean DBG = true; 61 private static final boolean VDBG = false; 62 63 /** 64 * Health Profile Source Role - the health device. 65 */ 66 public static final int SOURCE_ROLE = 1 << 0; 67 68 /** 69 * Health Profile Sink Role the device talking to the health device. 70 */ 71 public static final int SINK_ROLE = 1 << 1; 72 73 /** 74 * Health Profile - Channel Type used - Reliable 75 */ 76 public static final int CHANNEL_TYPE_RELIABLE = 10; 77 78 /** 79 * Health Profile - Channel Type used - Streaming 80 */ 81 public static final int CHANNEL_TYPE_STREAMING = 11; 82 83 /** 84 * @hide 85 */ 86 public static final int CHANNEL_TYPE_ANY = 12; 87 88 /** @hide */ 89 public static final int HEALTH_OPERATION_SUCCESS = 6000; 90 /** @hide */ 91 public static final int HEALTH_OPERATION_ERROR = 6001; 92 /** @hide */ 93 public static final int HEALTH_OPERATION_INVALID_ARGS = 6002; 94 /** @hide */ 95 public static final int HEALTH_OPERATION_GENERIC_FAILURE = 6003; 96 /** @hide */ 97 public static final int HEALTH_OPERATION_NOT_FOUND = 6004; 98 /** @hide */ 99 public static final int HEALTH_OPERATION_NOT_ALLOWED = 6005; 100 101 final private IBluetoothStateChangeCallback mBluetoothStateChangeCallback = 102 new IBluetoothStateChangeCallback.Stub() { 103 public void onBluetoothStateChange(boolean up) { 104 if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up); 105 if (!up) { 106 if (VDBG) Log.d(TAG,"Unbinding service..."); 107 synchronized (mConnection) { 108 try { 109 mService = null; 110 mContext.unbindService(mConnection); 111 } catch (Exception re) { 112 Log.e(TAG,"",re); 113 } 114 } 115 } else { 116 synchronized (mConnection) { 117 try { 118 if (mService == null) { 119 if (VDBG) Log.d(TAG,"Binding service..."); 120 if (!mContext.bindService(new Intent(IBluetoothHealth.class.getName()), mConnection, 0)) { 121 Log.e(TAG, "Could not bind to Bluetooth Health Service"); 122 } 123 } 124 } catch (Exception re) { 125 Log.e(TAG,"",re); 126 } 127 } 128 } 129 } 130 }; 131 132 133 /** 134 * Register an application configuration that acts as a Health SINK. 135 * This is the configuration that will be used to communicate with health devices 136 * which will act as the {@link #SOURCE_ROLE}. This is an asynchronous call and so 137 * the callback is used to notify success or failure if the function returns true. 138 * 139 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 140 * 141 * @param name The friendly name associated with the application or configuration. 142 * @param dataType The dataType of the Source role of Health Profile to which 143 * the sink wants to connect to. 144 * @param callback A callback to indicate success or failure of the registration and 145 * all operations done on this application configuration. 146 * @return If true, callback will be called. 147 */ 148 public boolean registerSinkAppConfiguration(String name, int dataType, 149 BluetoothHealthCallback callback) { 150 if (!isEnabled() || name == null) return false; 151 152 if (VDBG) log("registerSinkApplication(" + name + ":" + dataType + ")"); 153 return registerAppConfiguration(name, dataType, SINK_ROLE, 154 CHANNEL_TYPE_ANY, callback); 155 } 156 157 /** 158 * Register an application configuration that acts as a Health SINK or in a Health 159 * SOURCE role.This is an asynchronous call and so 160 * the callback is used to notify success or failure if the function returns true. 161 * 162 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 163 * 164 * @param name The friendly name associated with the application or configuration. 165 * @param dataType The dataType of the Source role of Health Profile. 166 * @param channelType The channel type. Will be one of 167 * {@link #CHANNEL_TYPE_RELIABLE} or 168 * {@link #CHANNEL_TYPE_STREAMING} 169 * @param callback - A callback to indicate success or failure. 170 * @return If true, callback will be called. 171 * @hide 172 */ 173 public boolean registerAppConfiguration(String name, int dataType, int role, 174 int channelType, BluetoothHealthCallback callback) { 175 boolean result = false; 176 if (!isEnabled() || !checkAppParam(name, role, channelType, callback)) return result; 177 178 if (VDBG) log("registerApplication(" + name + ":" + dataType + ")"); 179 BluetoothHealthCallbackWrapper wrapper = new BluetoothHealthCallbackWrapper(callback); 180 BluetoothHealthAppConfiguration config = 181 new BluetoothHealthAppConfiguration(name, dataType, role, channelType); 182 183 if (mService != null) { 184 try { 185 result = mService.registerAppConfiguration(config, wrapper); 186 } catch (RemoteException e) { 187 Log.e(TAG, e.toString()); 188 } 189 } else { 190 Log.w(TAG, "Proxy not attached to service"); 191 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 192 } 193 return result; 194 } 195 196 /** 197 * Unregister an application configuration that has been registered using 198 * {@link #registerSinkAppConfiguration} 199 * 200 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 201 * 202 * @param config The health app configuration 203 * @return Success or failure. 204 */ 205 public boolean unregisterAppConfiguration(BluetoothHealthAppConfiguration config) { 206 boolean result = false; 207 if (mService != null && isEnabled() && config != null) { 208 try { 209 result = mService.unregisterAppConfiguration(config); 210 } catch (RemoteException e) { 211 Log.e(TAG, e.toString()); 212 } 213 } else { 214 Log.w(TAG, "Proxy not attached to service"); 215 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 216 } 217 218 return result; 219 } 220 221 /** 222 * Connect to a health device which has the {@link #SOURCE_ROLE}. 223 * This is an asynchronous call. If this function returns true, the callback 224 * associated with the application configuration will be called. 225 * 226 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 227 * 228 * @param device The remote Bluetooth device. 229 * @param config The application configuration which has been registered using 230 * {@link #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) } 231 * @return If true, the callback associated with the application config will be called. 232 */ 233 public boolean connectChannelToSource(BluetoothDevice device, 234 BluetoothHealthAppConfiguration config) { 235 if (mService != null && isEnabled() && isValidDevice(device) && 236 config != null) { 237 try { 238 return mService.connectChannelToSource(device, config); 239 } catch (RemoteException e) { 240 Log.e(TAG, e.toString()); 241 } 242 } else { 243 Log.w(TAG, "Proxy not attached to service"); 244 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 245 } 246 return false; 247 } 248 249 /** 250 * Connect to a health device which has the {@link #SINK_ROLE}. 251 * This is an asynchronous call. If this function returns true, the callback 252 * associated with the application configuration will be called. 253 * 254 *<p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 255 * 256 * @param device The remote Bluetooth device. 257 * @param config The application configuration which has been registered using 258 * {@link #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) } 259 * @return If true, the callback associated with the application config will be called. 260 * @hide 261 */ 262 public boolean connectChannelToSink(BluetoothDevice device, 263 BluetoothHealthAppConfiguration config, int channelType) { 264 if (mService != null && isEnabled() && isValidDevice(device) && 265 config != null) { 266 try { 267 return mService.connectChannelToSink(device, config, channelType); 268 } catch (RemoteException e) { 269 Log.e(TAG, e.toString()); 270 } 271 } else { 272 Log.w(TAG, "Proxy not attached to service"); 273 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 274 } 275 return false; 276 } 277 278 /** 279 * Disconnect a connected health channel. 280 * This is an asynchronous call. If this function returns true, the callback 281 * associated with the application configuration will be called. 282 * 283 *<p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 284 * 285 * @param device The remote Bluetooth device. 286 * @param config The application configuration which has been registered using 287 * {@link #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) } 288 * @param channelId The channel id associated with the channel 289 * @return If true, the callback associated with the application config will be called. 290 */ 291 public boolean disconnectChannel(BluetoothDevice device, 292 BluetoothHealthAppConfiguration config, int channelId) { 293 if (mService != null && isEnabled() && isValidDevice(device) && 294 config != null) { 295 try { 296 return mService.disconnectChannel(device, config, channelId); 297 } catch (RemoteException e) { 298 Log.e(TAG, e.toString()); 299 } 300 } else { 301 Log.w(TAG, "Proxy not attached to service"); 302 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 303 } 304 return false; 305 } 306 307 /** 308 * Get the file descriptor of the main channel associated with the remote device 309 * and application configuration. 310 * 311 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 312 * 313 * <p> Its the responsibility of the caller to close the ParcelFileDescriptor 314 * when done. 315 * 316 * @param device The remote Bluetooth health device 317 * @param config The application configuration 318 * @return null on failure, ParcelFileDescriptor on success. 319 */ 320 public ParcelFileDescriptor getMainChannelFd(BluetoothDevice device, 321 BluetoothHealthAppConfiguration config) { 322 if (mService != null && isEnabled() && isValidDevice(device) && 323 config != null) { 324 try { 325 return mService.getMainChannelFd(device, config); 326 } catch (RemoteException e) { 327 Log.e(TAG, e.toString()); 328 } 329 } else { 330 Log.w(TAG, "Proxy not attached to service"); 331 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 332 } 333 return null; 334 } 335 336 /** 337 * Get the current connection state of the profile. 338 * 339 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 340 * 341 * This is not specific to any application configuration but represents the connection 342 * state of the local Bluetooth adapter with the remote device. This can be used 343 * by applications like status bar which would just like to know the state of the 344 * local adapter. 345 * 346 * @param device Remote bluetooth device. 347 * @return State of the profile connection. One of 348 * {@link #STATE_CONNECTED}, {@link #STATE_CONNECTING}, 349 * {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING} 350 */ 351 @Override 352 public int getConnectionState(BluetoothDevice device) { 353 if (mService != null && isEnabled() && isValidDevice(device)) { 354 try { 355 return mService.getHealthDeviceConnectionState(device); 356 } catch (RemoteException e) { 357 Log.e(TAG, e.toString()); 358 } 359 } else { 360 Log.w(TAG, "Proxy not attached to service"); 361 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 362 } 363 return STATE_DISCONNECTED; 364 } 365 366 /** 367 * Get connected devices for the health profile. 368 * 369 * <p> Return the set of devices which are in state {@link #STATE_CONNECTED} 370 * 371 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 372 * 373 * This is not specific to any application configuration but represents the connection 374 * state of the local Bluetooth adapter for this profile. This can be used 375 * by applications like status bar which would just like to know the state of the 376 * local adapter. 377 * @return List of devices. The list will be empty on error. 378 */ 379 @Override 380 public List<BluetoothDevice> getConnectedDevices() { 381 if (mService != null && isEnabled()) { 382 try { 383 return mService.getConnectedHealthDevices(); 384 } catch (RemoteException e) { 385 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 386 return new ArrayList<BluetoothDevice>(); 387 } 388 } 389 if (mService == null) Log.w(TAG, "Proxy not attached to service"); 390 return new ArrayList<BluetoothDevice>(); 391 } 392 393 /** 394 * Get a list of devices that match any of the given connection 395 * states. 396 * 397 * <p> If none of the devices match any of the given states, 398 * an empty list will be returned. 399 * 400 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 401 * This is not specific to any application configuration but represents the connection 402 * state of the local Bluetooth adapter for this profile. This can be used 403 * by applications like status bar which would just like to know the state of the 404 * local adapter. 405 * 406 * @param states Array of states. States can be one of 407 * {@link #STATE_CONNECTED}, {@link #STATE_CONNECTING}, 408 * {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING}, 409 * @return List of devices. The list will be empty on error. 410 */ 411 @Override 412 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 413 if (mService != null && isEnabled()) { 414 try { 415 return mService.getHealthDevicesMatchingConnectionStates(states); 416 } catch (RemoteException e) { 417 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 418 return new ArrayList<BluetoothDevice>(); 419 } 420 } 421 if (mService == null) Log.w(TAG, "Proxy not attached to service"); 422 return new ArrayList<BluetoothDevice>(); 423 } 424 425 private static class BluetoothHealthCallbackWrapper extends IBluetoothHealthCallback.Stub { 426 private BluetoothHealthCallback mCallback; 427 428 public BluetoothHealthCallbackWrapper(BluetoothHealthCallback callback) { 429 mCallback = callback; 430 } 431 432 @Override 433 public void onHealthAppConfigurationStatusChange(BluetoothHealthAppConfiguration config, 434 int status) { 435 mCallback.onHealthAppConfigurationStatusChange(config, status); 436 } 437 438 @Override 439 public void onHealthChannelStateChange(BluetoothHealthAppConfiguration config, 440 BluetoothDevice device, int prevState, int newState, 441 ParcelFileDescriptor fd, int channelId) { 442 mCallback.onHealthChannelStateChange(config, device, prevState, newState, fd, 443 channelId); 444 } 445 } 446 447 /** Health Channel Connection State - Disconnected */ 448 public static final int STATE_CHANNEL_DISCONNECTED = 0; 449 /** Health Channel Connection State - Connecting */ 450 public static final int STATE_CHANNEL_CONNECTING = 1; 451 /** Health Channel Connection State - Connected */ 452 public static final int STATE_CHANNEL_CONNECTED = 2; 453 /** Health Channel Connection State - Disconnecting */ 454 public static final int STATE_CHANNEL_DISCONNECTING = 3; 455 456 /** Health App Configuration registration success */ 457 public static final int APP_CONFIG_REGISTRATION_SUCCESS = 0; 458 /** Health App Configuration registration failure */ 459 public static final int APP_CONFIG_REGISTRATION_FAILURE = 1; 460 /** Health App Configuration un-registration success */ 461 public static final int APP_CONFIG_UNREGISTRATION_SUCCESS = 2; 462 /** Health App Configuration un-registration failure */ 463 public static final int APP_CONFIG_UNREGISTRATION_FAILURE = 3; 464 465 private Context mContext; 466 private ServiceListener mServiceListener; 467 private IBluetoothHealth mService; 468 BluetoothAdapter mAdapter; 469 470 /** 471 * Create a BluetoothHealth proxy object. 472 */ 473 /*package*/ BluetoothHealth(Context context, ServiceListener l) { 474 mContext = context; 475 mServiceListener = l; 476 mAdapter = BluetoothAdapter.getDefaultAdapter(); 477 IBluetoothManager mgr = mAdapter.getBluetoothManager(); 478 if (mgr != null) { 479 try { 480 mgr.registerStateChangeCallback(mBluetoothStateChangeCallback); 481 } catch (RemoteException e) { 482 Log.e(TAG,"",e); 483 } 484 } 485 486 if (!context.bindService(new Intent(IBluetoothHealth.class.getName()), mConnection, 0)) { 487 Log.e(TAG, "Could not bind to Bluetooth Health Service"); 488 } 489 } 490 491 /*package*/ void close() { 492 if (VDBG) log("close()"); 493 IBluetoothManager mgr = mAdapter.getBluetoothManager(); 494 if (mgr != null) { 495 try { 496 mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback); 497 } catch (Exception e) { 498 Log.e(TAG,"",e); 499 } 500 } 501 502 synchronized (mConnection) { 503 if (mService != null) { 504 try { 505 mService = null; 506 mContext.unbindService(mConnection); 507 } catch (Exception re) { 508 Log.e(TAG,"",re); 509 } 510 } 511 } 512 mServiceListener = null; 513 } 514 515 private ServiceConnection mConnection = new ServiceConnection() { 516 public void onServiceConnected(ComponentName className, IBinder service) { 517 if (DBG) Log.d(TAG, "Proxy object connected"); 518 mService = IBluetoothHealth.Stub.asInterface(service); 519 520 if (mServiceListener != null) { 521 mServiceListener.onServiceConnected(BluetoothProfile.HEALTH, BluetoothHealth.this); 522 } 523 } 524 public void onServiceDisconnected(ComponentName className) { 525 if (DBG) Log.d(TAG, "Proxy object disconnected"); 526 mService = null; 527 if (mServiceListener != null) { 528 mServiceListener.onServiceDisconnected(BluetoothProfile.HEALTH); 529 } 530 } 531 }; 532 533 private boolean isEnabled() { 534 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 535 536 if (adapter != null && adapter.getState() == BluetoothAdapter.STATE_ON) return true; 537 log("Bluetooth is Not enabled"); 538 return false; 539 } 540 541 private boolean isValidDevice(BluetoothDevice device) { 542 if (device == null) return false; 543 544 if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true; 545 return false; 546 } 547 548 private boolean checkAppParam(String name, int role, int channelType, 549 BluetoothHealthCallback callback) { 550 if (name == null || (role != SOURCE_ROLE && role != SINK_ROLE) || 551 (channelType != CHANNEL_TYPE_RELIABLE && 552 channelType != CHANNEL_TYPE_STREAMING && 553 channelType != CHANNEL_TYPE_ANY) || callback == null) { 554 return false; 555 } 556 if (role == SOURCE_ROLE && channelType == CHANNEL_TYPE_ANY) return false; 557 return true; 558 } 559 560 private static void log(String msg) { 561 Log.d(TAG, msg); 562 } 563 } 564