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
17package android.bluetooth;
18
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.Intent;
22import android.content.ServiceConnection;
23import android.os.IBinder;
24import android.os.ParcelFileDescriptor;
25import android.os.RemoteException;
26import android.os.ServiceManager;
27import android.util.Log;
28
29import java.util.ArrayList;
30import 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 */
58public 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