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.util.Log;
27
28import java.util.ArrayList;
29import 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 */
57public 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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
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
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
425        public BluetoothHealthCallbackWrapper(BluetoothHealthCallback callback) {
426            mCallback = callback;
427        }
428
429        @Override
430        public void onHealthAppConfigurationStatusChange(BluetoothHealthAppConfiguration config,
431                                                         int status) {
432           mCallback.onHealthAppConfigurationStatusChange(config, status);
433        }
434
435        @Override
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     */
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
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
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
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
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
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
567    private static void log(String msg) {
568        Log.d(TAG, msg);
569    }
570}
571