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.util.Log; 27 28 import java.util.ArrayList; 29 import java.util.List; 30 31 /** 32 * Public API for Bluetooth Health Profile. 33 * 34 * <p>BluetoothHealth is a proxy object for controlling the Bluetooth 35 * Service via IPC. 36 * 37 * <p> How to connect to a health device which is acting in the source role. 38 * <li> Use {@link BluetoothAdapter#getProfileProxy} to get 39 * the BluetoothHealth proxy object. </li> 40 * <li> Create an {@link BluetoothHealth} callback and call 41 * {@link #registerSinkAppConfiguration} to register an application 42 * configuration </li> 43 * <li> Pair with the remote device. This currently needs to be done manually 44 * from Bluetooth Settings </li> 45 * <li> Connect to a health device using {@link #connectChannelToSource}. Some 46 * devices will connect the channel automatically. The {@link BluetoothHealth} 47 * callback will inform the application of channel state change. </li> 48 * <li> Use the file descriptor provided with a connected channel to read and 49 * write data to the health channel. </li> 50 * <li> The received data needs to be interpreted using a health manager which 51 * implements the IEEE 11073-xxxxx specifications. 52 * <li> When done, close the health channel by calling {@link #disconnectChannel} 53 * and unregister the application configuration calling 54 * {@link #unregisterAppConfiguration} 55 * 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 final private 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 140 * the sink wants to connect to. 141 * @param callback A callback to indicate success or failure of the registration and 142 * all operations done on this application configuration. 143 * @return If true, callback will be called. 144 */ registerSinkAppConfiguration(String name, int dataType, BluetoothHealthCallback callback)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 164 * {@link #CHANNEL_TYPE_RELIABLE} or 165 * {@link #CHANNEL_TYPE_STREAMING} 166 * @param callback - A callback to indicate success or failure. 167 * @return If true, callback will be called. 168 * @hide 169 */ registerAppConfiguration(String name, int dataType, int role, int channelType, BluetoothHealthCallback callback)170 public boolean registerAppConfiguration(String name, int dataType, int role, 171 int channelType, BluetoothHealthCallback callback) { 172 boolean result = false; 173 if (!isEnabled() || !checkAppParam(name, role, channelType, callback)) return result; 174 175 if (VDBG) log("registerApplication(" + name + ":" + dataType + ")"); 176 BluetoothHealthCallbackWrapper wrapper = new BluetoothHealthCallbackWrapper(callback); 177 BluetoothHealthAppConfiguration config = 178 new BluetoothHealthAppConfiguration(name, dataType, role, channelType); 179 180 if (mService != null) { 181 try { 182 result = mService.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 */ unregisterAppConfiguration(BluetoothHealthAppConfiguration config)202 public boolean unregisterAppConfiguration(BluetoothHealthAppConfiguration config) { 203 boolean result = false; 204 if (mService != null && isEnabled() && config != null) { 205 try { 206 result = mService.unregisterAppConfiguration(config); 207 } catch (RemoteException e) { 208 Log.e(TAG, e.toString()); 209 } 210 } else { 211 Log.w(TAG, "Proxy not attached to service"); 212 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 213 } 214 215 return result; 216 } 217 218 /** 219 * Connect to a health device which has the {@link #SOURCE_ROLE}. 220 * This is an asynchronous call. If this function returns true, the callback 221 * associated with the application configuration will be called. 222 * 223 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 224 * 225 * @param device The remote Bluetooth device. 226 * @param config The application configuration which has been registered using 227 * {@link #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) } 228 * @return If true, the callback associated with the application config will be called. 229 */ connectChannelToSource(BluetoothDevice device, BluetoothHealthAppConfiguration config)230 public boolean connectChannelToSource(BluetoothDevice device, 231 BluetoothHealthAppConfiguration config) { 232 if (mService != null && isEnabled() && isValidDevice(device) && 233 config != null) { 234 try { 235 return mService.connectChannelToSource(device, config); 236 } catch (RemoteException e) { 237 Log.e(TAG, e.toString()); 238 } 239 } else { 240 Log.w(TAG, "Proxy not attached to service"); 241 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 242 } 243 return false; 244 } 245 246 /** 247 * Connect to a health device which has the {@link #SINK_ROLE}. 248 * This is an asynchronous call. If this function returns true, the callback 249 * associated with the application configuration will be called. 250 * 251 *<p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 252 * 253 * @param device The remote Bluetooth device. 254 * @param config The application configuration which has been registered using 255 * {@link #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) } 256 * @return If true, the callback associated with the application config will be called. 257 * @hide 258 */ connectChannelToSink(BluetoothDevice device, BluetoothHealthAppConfiguration config, int channelType)259 public boolean connectChannelToSink(BluetoothDevice device, 260 BluetoothHealthAppConfiguration config, int channelType) { 261 if (mService != null && isEnabled() && isValidDevice(device) && 262 config != null) { 263 try { 264 return mService.connectChannelToSink(device, config, channelType); 265 } catch (RemoteException e) { 266 Log.e(TAG, e.toString()); 267 } 268 } else { 269 Log.w(TAG, "Proxy not attached to service"); 270 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 271 } 272 return false; 273 } 274 275 /** 276 * Disconnect a connected health channel. 277 * This is an asynchronous call. If this function returns true, the callback 278 * associated with the application configuration will be called. 279 * 280 *<p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 281 * 282 * @param device The remote Bluetooth device. 283 * @param config The application configuration which has been registered using 284 * {@link #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) } 285 * @param channelId The channel id associated with the channel 286 * @return If true, the callback associated with the application config will be called. 287 */ disconnectChannel(BluetoothDevice device, BluetoothHealthAppConfiguration config, int channelId)288 public boolean disconnectChannel(BluetoothDevice device, 289 BluetoothHealthAppConfiguration config, int channelId) { 290 if (mService != null && isEnabled() && isValidDevice(device) && 291 config != null) { 292 try { 293 return mService.disconnectChannel(device, config, channelId); 294 } catch (RemoteException e) { 295 Log.e(TAG, e.toString()); 296 } 297 } else { 298 Log.w(TAG, "Proxy not attached to service"); 299 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 300 } 301 return false; 302 } 303 304 /** 305 * Get the file descriptor of the main channel associated with the remote device 306 * and application configuration. 307 * 308 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 309 * 310 * <p> Its the responsibility of the caller to close the ParcelFileDescriptor 311 * when done. 312 * 313 * @param device The remote Bluetooth health device 314 * @param config The application configuration 315 * @return null on failure, ParcelFileDescriptor on success. 316 */ getMainChannelFd(BluetoothDevice device, BluetoothHealthAppConfiguration config)317 public ParcelFileDescriptor getMainChannelFd(BluetoothDevice device, 318 BluetoothHealthAppConfiguration config) { 319 if (mService != null && isEnabled() && isValidDevice(device) && 320 config != null) { 321 try { 322 return mService.getMainChannelFd(device, config); 323 } catch (RemoteException e) { 324 Log.e(TAG, e.toString()); 325 } 326 } else { 327 Log.w(TAG, "Proxy not attached to service"); 328 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 329 } 330 return null; 331 } 332 333 /** 334 * Get the current connection state of the profile. 335 * 336 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 337 * 338 * This is not specific to any application configuration but represents the connection 339 * state of the local Bluetooth adapter with the remote device. This can be used 340 * by applications like status bar which would just like to know the state of the 341 * local adapter. 342 * 343 * @param device Remote bluetooth device. 344 * @return State of the profile connection. One of 345 * {@link #STATE_CONNECTED}, {@link #STATE_CONNECTING}, 346 * {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING} 347 */ 348 @Override getConnectionState(BluetoothDevice device)349 public int getConnectionState(BluetoothDevice device) { 350 if (mService != null && isEnabled() && isValidDevice(device)) { 351 try { 352 return mService.getHealthDeviceConnectionState(device); 353 } catch (RemoteException e) { 354 Log.e(TAG, e.toString()); 355 } 356 } else { 357 Log.w(TAG, "Proxy not attached to service"); 358 if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); 359 } 360 return STATE_DISCONNECTED; 361 } 362 363 /** 364 * Get connected devices for the health profile. 365 * 366 * <p> Return the set of devices which are in state {@link #STATE_CONNECTED} 367 * 368 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 369 * 370 * This is not specific to any application configuration but represents the connection 371 * state of the local Bluetooth adapter for this profile. This can be used 372 * by applications like status bar which would just like to know the state of the 373 * local adapter. 374 * @return List of devices. The list will be empty on error. 375 */ 376 @Override getConnectedDevices()377 public List<BluetoothDevice> getConnectedDevices() { 378 if (mService != null && isEnabled()) { 379 try { 380 return mService.getConnectedHealthDevices(); 381 } catch (RemoteException e) { 382 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 383 return new ArrayList<BluetoothDevice>(); 384 } 385 } 386 if (mService == null) Log.w(TAG, "Proxy not attached to service"); 387 return new ArrayList<BluetoothDevice>(); 388 } 389 390 /** 391 * Get a list of devices that match any of the given connection 392 * states. 393 * 394 * <p> If none of the devices match any of the given states, 395 * an empty list will be returned. 396 * 397 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 398 * This is not specific to any application configuration but represents the connection 399 * state of the local Bluetooth adapter for this profile. This can be used 400 * by applications like status bar which would just like to know the state of the 401 * local adapter. 402 * 403 * @param states Array of states. States can be one of 404 * {@link #STATE_CONNECTED}, {@link #STATE_CONNECTING}, 405 * {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING}, 406 * @return List of devices. The list will be empty on error. 407 */ 408 @Override getDevicesMatchingConnectionStates(int[] states)409 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 410 if (mService != null && isEnabled()) { 411 try { 412 return mService.getHealthDevicesMatchingConnectionStates(states); 413 } catch (RemoteException e) { 414 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 415 return new ArrayList<BluetoothDevice>(); 416 } 417 } 418 if (mService == null) Log.w(TAG, "Proxy not attached to service"); 419 return new ArrayList<BluetoothDevice>(); 420 } 421 422 private static class BluetoothHealthCallbackWrapper extends IBluetoothHealthCallback.Stub { 423 private BluetoothHealthCallback mCallback; 424 BluetoothHealthCallbackWrapper(BluetoothHealthCallback callback)425 public BluetoothHealthCallbackWrapper(BluetoothHealthCallback callback) { 426 mCallback = callback; 427 } 428 429 @Override onHealthAppConfigurationStatusChange(BluetoothHealthAppConfiguration config, int status)430 public void onHealthAppConfigurationStatusChange(BluetoothHealthAppConfiguration config, 431 int status) { 432 mCallback.onHealthAppConfigurationStatusChange(config, status); 433 } 434 435 @Override onHealthChannelStateChange(BluetoothHealthAppConfiguration config, BluetoothDevice device, int prevState, int newState, ParcelFileDescriptor fd, int channelId)436 public void onHealthChannelStateChange(BluetoothHealthAppConfiguration config, 437 BluetoothDevice device, int prevState, int newState, 438 ParcelFileDescriptor fd, int channelId) { 439 mCallback.onHealthChannelStateChange(config, device, prevState, newState, fd, 440 channelId); 441 } 442 } 443 444 /** Health Channel Connection State - Disconnected */ 445 public static final int STATE_CHANNEL_DISCONNECTED = 0; 446 /** Health Channel Connection State - Connecting */ 447 public static final int STATE_CHANNEL_CONNECTING = 1; 448 /** Health Channel Connection State - Connected */ 449 public static final int STATE_CHANNEL_CONNECTED = 2; 450 /** Health Channel Connection State - Disconnecting */ 451 public static final int STATE_CHANNEL_DISCONNECTING = 3; 452 453 /** Health App Configuration registration success */ 454 public static final int APP_CONFIG_REGISTRATION_SUCCESS = 0; 455 /** Health App Configuration registration failure */ 456 public static final int APP_CONFIG_REGISTRATION_FAILURE = 1; 457 /** Health App Configuration un-registration success */ 458 public static final int APP_CONFIG_UNREGISTRATION_SUCCESS = 2; 459 /** Health App Configuration un-registration failure */ 460 public static final int APP_CONFIG_UNREGISTRATION_FAILURE = 3; 461 462 private Context mContext; 463 private ServiceListener mServiceListener; 464 private IBluetoothHealth mService; 465 BluetoothAdapter mAdapter; 466 467 /** 468 * Create a BluetoothHealth proxy object. 469 */ BluetoothHealth(Context context, ServiceListener l)470 /*package*/ BluetoothHealth(Context context, ServiceListener l) { 471 mContext = context; 472 mServiceListener = l; 473 mAdapter = BluetoothAdapter.getDefaultAdapter(); 474 IBluetoothManager mgr = mAdapter.getBluetoothManager(); 475 if (mgr != null) { 476 try { 477 mgr.registerStateChangeCallback(mBluetoothStateChangeCallback); 478 } catch (RemoteException e) { 479 Log.e(TAG,"",e); 480 } 481 } 482 483 doBind(); 484 } 485 doBind()486 boolean doBind() { 487 Intent intent = new Intent(IBluetoothHealth.class.getName()); 488 ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); 489 intent.setComponent(comp); 490 if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0, 491 android.os.Process.myUserHandle())) { 492 Log.e(TAG, "Could not bind to Bluetooth Health Service with " + intent); 493 return false; 494 } 495 return true; 496 } 497 close()498 /*package*/ void close() { 499 if (VDBG) log("close()"); 500 IBluetoothManager mgr = mAdapter.getBluetoothManager(); 501 if (mgr != null) { 502 try { 503 mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback); 504 } catch (Exception e) { 505 Log.e(TAG,"",e); 506 } 507 } 508 509 synchronized (mConnection) { 510 if (mService != null) { 511 try { 512 mService = null; 513 mContext.unbindService(mConnection); 514 } catch (Exception re) { 515 Log.e(TAG,"",re); 516 } 517 } 518 } 519 mServiceListener = null; 520 } 521 522 private final ServiceConnection mConnection = new ServiceConnection() { 523 public void onServiceConnected(ComponentName className, IBinder service) { 524 if (DBG) Log.d(TAG, "Proxy object connected"); 525 mService = IBluetoothHealth.Stub.asInterface(service); 526 527 if (mServiceListener != null) { 528 mServiceListener.onServiceConnected(BluetoothProfile.HEALTH, BluetoothHealth.this); 529 } 530 } 531 public void onServiceDisconnected(ComponentName className) { 532 if (DBG) Log.d(TAG, "Proxy object disconnected"); 533 mService = null; 534 if (mServiceListener != null) { 535 mServiceListener.onServiceDisconnected(BluetoothProfile.HEALTH); 536 } 537 } 538 }; 539 isEnabled()540 private boolean isEnabled() { 541 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 542 543 if (adapter != null && adapter.getState() == BluetoothAdapter.STATE_ON) return true; 544 log("Bluetooth is Not enabled"); 545 return false; 546 } 547 isValidDevice(BluetoothDevice device)548 private boolean isValidDevice(BluetoothDevice device) { 549 if (device == null) return false; 550 551 if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true; 552 return false; 553 } 554 checkAppParam(String name, int role, int channelType, BluetoothHealthCallback callback)555 private boolean checkAppParam(String name, int role, int channelType, 556 BluetoothHealthCallback callback) { 557 if (name == null || (role != SOURCE_ROLE && role != SINK_ROLE) || 558 (channelType != CHANNEL_TYPE_RELIABLE && 559 channelType != CHANNEL_TYPE_STREAMING && 560 channelType != CHANNEL_TYPE_ANY) || callback == null) { 561 return false; 562 } 563 if (role == SOURCE_ROLE && channelType == CHANNEL_TYPE_ANY) return false; 564 return true; 565 } 566 log(String msg)567 private static void log(String msg) { 568 Log.d(TAG, msg); 569 } 570 } 571