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.Binder; 24 import android.os.IBinder; 25 import android.os.ParcelFileDescriptor; 26 import android.os.RemoteException; 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 public final class BluetoothHealth implements BluetoothProfile { 58 private static final String TAG = "BluetoothHealth"; 59 private static final boolean DBG = true; 60 private static final boolean VDBG = false; 61 62 /** 63 * Health Profile Source Role - the health device. 64 */ 65 public static final int SOURCE_ROLE = 1 << 0; 66 67 /** 68 * Health Profile Sink Role the device talking to the health device. 69 */ 70 public static final int SINK_ROLE = 1 << 1; 71 72 /** 73 * Health Profile - Channel Type used - Reliable 74 */ 75 public static final int CHANNEL_TYPE_RELIABLE = 10; 76 77 /** 78 * Health Profile - Channel Type used - Streaming 79 */ 80 public static final int CHANNEL_TYPE_STREAMING = 11; 81 82 /** 83 * @hide 84 */ 85 public static final int CHANNEL_TYPE_ANY = 12; 86 87 /** @hide */ 88 public static final int HEALTH_OPERATION_SUCCESS = 6000; 89 /** @hide */ 90 public static final int HEALTH_OPERATION_ERROR = 6001; 91 /** @hide */ 92 public static final int HEALTH_OPERATION_INVALID_ARGS = 6002; 93 /** @hide */ 94 public static final int HEALTH_OPERATION_GENERIC_FAILURE = 6003; 95 /** @hide */ 96 public static final int HEALTH_OPERATION_NOT_FOUND = 6004; 97 /** @hide */ 98 public static final int HEALTH_OPERATION_NOT_ALLOWED = 6005; 99 100 private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback = 101 new IBluetoothStateChangeCallback.Stub() { 102 public void onBluetoothStateChange(boolean up) { 103 if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up); 104 if (!up) { 105 if (VDBG) Log.d(TAG, "Unbinding service..."); 106 synchronized (mConnection) { 107 try { 108 mService = null; 109 mContext.unbindService(mConnection); 110 } catch (Exception re) { 111 Log.e(TAG, "", re); 112 } 113 } 114 } else { 115 synchronized (mConnection) { 116 try { 117 if (mService == null) { 118 if (VDBG) Log.d(TAG, "Binding service..."); 119 doBind(); 120 } 121 } catch (Exception re) { 122 Log.e(TAG, "", re); 123 } 124 } 125 } 126 } 127 }; 128 129 130 /** 131 * Register an application configuration that acts as a Health SINK. 132 * This is the configuration that will be used to communicate with health devices 133 * which will act as the {@link #SOURCE_ROLE}. This is an asynchronous call and so 134 * the callback is used to notify success or failure if the function returns true. 135 * 136 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 137 * 138 * @param name The friendly name associated with the application or configuration. 139 * @param dataType The dataType of the Source role of Health Profile to which the sink wants to 140 * connect to. 141 * @param callback A callback to indicate success or failure of the registration and all 142 * operations done on this application configuration. 143 * @return If true, callback will be called. 144 */ 145 public boolean registerSinkAppConfiguration(String name, int dataType, 146 BluetoothHealthCallback callback) { 147 if (!isEnabled() || name == null) return false; 148 149 if (VDBG) log("registerSinkApplication(" + name + ":" + dataType + ")"); 150 return registerAppConfiguration(name, dataType, SINK_ROLE, 151 CHANNEL_TYPE_ANY, callback); 152 } 153 154 /** 155 * Register an application configuration that acts as a Health SINK or in a Health 156 * SOURCE role.This is an asynchronous call and so 157 * the callback is used to notify success or failure if the function returns true. 158 * 159 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 160 * 161 * @param name The friendly name associated with the application or configuration. 162 * @param dataType The dataType of the Source role of Health Profile. 163 * @param channelType The channel type. Will be one of {@link #CHANNEL_TYPE_RELIABLE} or {@link 164 * #CHANNEL_TYPE_STREAMING} 165 * @param callback - A callback to indicate success or failure. 166 * @return If true, callback will be called. 167 * @hide 168 */ 169 public boolean registerAppConfiguration(String name, int dataType, int role, 170 int channelType, BluetoothHealthCallback callback) { 171 boolean result = false; 172 if (!isEnabled() || !checkAppParam(name, role, channelType, callback)) return result; 173 174 if (VDBG) log("registerApplication(" + name + ":" + dataType + ")"); 175 BluetoothHealthCallbackWrapper wrapper = new BluetoothHealthCallbackWrapper(callback); 176 BluetoothHealthAppConfiguration config = 177 new BluetoothHealthAppConfiguration(name, dataType, role, channelType); 178 179 final IBluetoothHealth service = mService; 180 if (service != null) { 181 try { 182 result = service.registerAppConfiguration(config, wrapper); 183 } catch (RemoteException e) { 184 Log.e(TAG, e.toString()); 185 } 186 } else { 187 Log.w(TAG, "Proxy not attached to service"); 188 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 189 } 190 return result; 191 } 192 193 /** 194 * Unregister an application configuration that has been registered using 195 * {@link #registerSinkAppConfiguration} 196 * 197 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 198 * 199 * @param config The health app configuration 200 * @return Success or failure. 201 */ 202 public boolean unregisterAppConfiguration(BluetoothHealthAppConfiguration config) { 203 boolean result = false; 204 final IBluetoothHealth service = mService; 205 if (service != null && isEnabled() && config != null) { 206 try { 207 result = service.unregisterAppConfiguration(config); 208 } catch (RemoteException e) { 209 Log.e(TAG, e.toString()); 210 } 211 } else { 212 Log.w(TAG, "Proxy not attached to service"); 213 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 214 } 215 216 return result; 217 } 218 219 /** 220 * Connect to a health device which has the {@link #SOURCE_ROLE}. 221 * This is an asynchronous call. If this function returns true, the callback 222 * associated with the application configuration will be called. 223 * 224 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 225 * 226 * @param device The remote Bluetooth device. 227 * @param config The application configuration which has been registered using {@link 228 * #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) } 229 * @return If true, the callback associated with the application config will be called. 230 */ 231 public boolean connectChannelToSource(BluetoothDevice device, 232 BluetoothHealthAppConfiguration config) { 233 final IBluetoothHealth service = mService; 234 if (service != null && isEnabled() && isValidDevice(device) && config != null) { 235 try { 236 return service.connectChannelToSource(device, config); 237 } catch (RemoteException e) { 238 Log.e(TAG, e.toString()); 239 } 240 } else { 241 Log.w(TAG, "Proxy not attached to service"); 242 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 243 } 244 return false; 245 } 246 247 /** 248 * Connect to a health device which has the {@link #SINK_ROLE}. 249 * This is an asynchronous call. If this function returns true, the callback 250 * associated with the application configuration will be called. 251 * 252 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 253 * 254 * @param device The remote Bluetooth device. 255 * @param config The application configuration which has been registered using {@link 256 * #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) } 257 * @return If true, the callback associated with the application config will be called. 258 * @hide 259 */ 260 public boolean connectChannelToSink(BluetoothDevice device, 261 BluetoothHealthAppConfiguration config, int channelType) { 262 final IBluetoothHealth service = mService; 263 if (service != null && isEnabled() && isValidDevice(device) && config != null) { 264 try { 265 return service.connectChannelToSink(device, config, channelType); 266 } catch (RemoteException e) { 267 Log.e(TAG, e.toString()); 268 } 269 } else { 270 Log.w(TAG, "Proxy not attached to service"); 271 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 272 } 273 return false; 274 } 275 276 /** 277 * Disconnect a connected health channel. 278 * This is an asynchronous call. If this function returns true, the callback 279 * associated with the application configuration will be called. 280 * 281 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 282 * 283 * @param device The remote Bluetooth device. 284 * @param config The application configuration which has been registered using {@link 285 * #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) } 286 * @param channelId The channel id associated with the channel 287 * @return If true, the callback associated with the application config will be called. 288 */ 289 public boolean disconnectChannel(BluetoothDevice device, 290 BluetoothHealthAppConfiguration config, int channelId) { 291 final IBluetoothHealth service = mService; 292 if (service != null && isEnabled() && isValidDevice(device) && config != null) { 293 try { 294 return service.disconnectChannel(device, config, channelId); 295 } catch (RemoteException e) { 296 Log.e(TAG, e.toString()); 297 } 298 } else { 299 Log.w(TAG, "Proxy not attached to service"); 300 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 301 } 302 return false; 303 } 304 305 /** 306 * Get the file descriptor of the main channel associated with the remote device 307 * and application configuration. 308 * 309 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 310 * 311 * <p> Its the responsibility of the caller to close the ParcelFileDescriptor 312 * when done. 313 * 314 * @param device The remote Bluetooth health device 315 * @param config The application configuration 316 * @return null on failure, ParcelFileDescriptor on success. 317 */ 318 public ParcelFileDescriptor getMainChannelFd(BluetoothDevice device, 319 BluetoothHealthAppConfiguration config) { 320 final IBluetoothHealth service = mService; 321 if (service != null && isEnabled() && isValidDevice(device) && config != null) { 322 try { 323 return service.getMainChannelFd(device, config); 324 } catch (RemoteException e) { 325 Log.e(TAG, e.toString()); 326 } 327 } else { 328 Log.w(TAG, "Proxy not attached to service"); 329 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 330 } 331 return null; 332 } 333 334 /** 335 * Get the current connection state of the profile. 336 * 337 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 338 * 339 * This is not specific to any application configuration but represents the connection 340 * state of the local Bluetooth adapter with the remote device. This can be used 341 * by applications like status bar which would just like to know the state of the 342 * local adapter. 343 * 344 * @param device Remote bluetooth device. 345 * @return State of the profile connection. One of {@link #STATE_CONNECTED}, {@link 346 * #STATE_CONNECTING}, {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING} 347 */ 348 @Override 349 public int getConnectionState(BluetoothDevice device) { 350 final IBluetoothHealth service = mService; 351 if (service != null && isEnabled() && isValidDevice(device)) { 352 try { 353 return service.getHealthDeviceConnectionState(device); 354 } catch (RemoteException e) { 355 Log.e(TAG, e.toString()); 356 } 357 } else { 358 Log.w(TAG, "Proxy not attached to service"); 359 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 360 } 361 return STATE_DISCONNECTED; 362 } 363 364 /** 365 * Get connected devices for the health profile. 366 * 367 * <p> Return the set of devices which are in state {@link #STATE_CONNECTED} 368 * 369 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 370 * 371 * This is not specific to any application configuration but represents the connection 372 * state of the local Bluetooth adapter for this profile. This can be used 373 * by applications like status bar which would just like to know the state of the 374 * local adapter. 375 * 376 * @return List of devices. The list will be empty on error. 377 */ 378 @Override 379 public List<BluetoothDevice> getConnectedDevices() { 380 final IBluetoothHealth service = mService; 381 if (service != null && isEnabled()) { 382 try { 383 return service.getConnectedHealthDevices(); 384 } catch (RemoteException e) { 385 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 386 return new ArrayList<BluetoothDevice>(); 387 } 388 } 389 if (service == 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 {@link #STATE_CONNECTED}, {@link 407 * #STATE_CONNECTING}, {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING}, 408 * @return List of devices. The list will be empty on error. 409 */ 410 @Override 411 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 412 final IBluetoothHealth service = mService; 413 if (service != null && isEnabled()) { 414 try { 415 return service.getHealthDevicesMatchingConnectionStates(states); 416 } catch (RemoteException e) { 417 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 418 return new ArrayList<BluetoothDevice>(); 419 } 420 } 421 if (service == 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 volatile 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 doBind(); 487 } 488 489 boolean doBind() { 490 Intent intent = new Intent(IBluetoothHealth.class.getName()); 491 ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); 492 intent.setComponent(comp); 493 if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0, 494 mContext.getUser())) { 495 Log.e(TAG, "Could not bind to Bluetooth Health Service with " + intent); 496 return false; 497 } 498 return true; 499 } 500 501 /*package*/ void close() { 502 if (VDBG) log("close()"); 503 IBluetoothManager mgr = mAdapter.getBluetoothManager(); 504 if (mgr != null) { 505 try { 506 mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback); 507 } catch (Exception e) { 508 Log.e(TAG, "", e); 509 } 510 } 511 512 synchronized (mConnection) { 513 if (mService != null) { 514 try { 515 mService = null; 516 mContext.unbindService(mConnection); 517 } catch (Exception re) { 518 Log.e(TAG, "", re); 519 } 520 } 521 } 522 mServiceListener = null; 523 } 524 525 private final ServiceConnection mConnection = new ServiceConnection() { 526 public void onServiceConnected(ComponentName className, IBinder service) { 527 if (DBG) Log.d(TAG, "Proxy object connected"); 528 mService = IBluetoothHealth.Stub.asInterface(Binder.allowBlocking(service)); 529 530 if (mServiceListener != null) { 531 mServiceListener.onServiceConnected(BluetoothProfile.HEALTH, BluetoothHealth.this); 532 } 533 } 534 535 public void onServiceDisconnected(ComponentName className) { 536 if (DBG) Log.d(TAG, "Proxy object disconnected"); 537 mService = null; 538 if (mServiceListener != null) { 539 mServiceListener.onServiceDisconnected(BluetoothProfile.HEALTH); 540 } 541 } 542 }; 543 544 private boolean isEnabled() { 545 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 546 547 if (adapter != null && adapter.getState() == BluetoothAdapter.STATE_ON) return true; 548 log("Bluetooth is Not enabled"); 549 return false; 550 } 551 552 private static boolean isValidDevice(BluetoothDevice device) { 553 return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress()); 554 } 555 556 private boolean checkAppParam(String name, int role, int channelType, 557 BluetoothHealthCallback callback) { 558 if (name == null || (role != SOURCE_ROLE && role != SINK_ROLE) 559 || (channelType != CHANNEL_TYPE_RELIABLE && channelType != CHANNEL_TYPE_STREAMING 560 && channelType != CHANNEL_TYPE_ANY) 561 || callback == null) { 562 return false; 563 } 564 if (role == SOURCE_ROLE && channelType == CHANNEL_TYPE_ANY) return false; 565 return true; 566 } 567 568 private static void log(String msg) { 569 Log.d(TAG, msg); 570 } 571 } 572