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