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.Binder;
24import android.os.IBinder;
25import android.os.ParcelFileDescriptor;
26import android.os.RemoteException;
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 */
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    private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback =
101            new IBluetoothStateChangeCallback.Stub() {
102                public void onBluetoothStateChange(boolean up) {
103                    if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up);
104                    if (!up) {
105                        if (VDBG) Log.d(TAG, "Unbinding service...");
106                        synchronized (mConnection) {
107                            try {
108                                mService = null;
109                                mContext.unbindService(mConnection);
110                            } catch (Exception re) {
111                                Log.e(TAG, "", re);
112                            }
113                        }
114                    } else {
115                        synchronized (mConnection) {
116                            try {
117                                if (mService == null) {
118                                    if (VDBG) Log.d(TAG, "Binding service...");
119                                    doBind();
120                                }
121                            } catch (Exception re) {
122                                Log.e(TAG, "", re);
123                            }
124                        }
125                    }
126                }
127            };
128
129
130    /**
131     * Register an application configuration that acts as a Health SINK.
132     * This is the configuration that will be used to communicate with health devices
133     * which will act as the {@link #SOURCE_ROLE}. This is an asynchronous call and so
134     * the callback is used to notify success or failure if the function returns true.
135     *
136     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
137     *
138     * @param name The friendly name associated with the application or configuration.
139     * @param dataType The dataType of the Source role of Health Profile to which the sink wants to
140     * connect to.
141     * @param callback A callback to indicate success or failure of the registration and all
142     * operations done on this application configuration.
143     * @return If true, callback will be called.
144     */
145    public boolean registerSinkAppConfiguration(String name, int dataType,
146            BluetoothHealthCallback callback) {
147        if (!isEnabled() || name == null) return false;
148
149        if (VDBG) log("registerSinkApplication(" + name + ":" + dataType + ")");
150        return registerAppConfiguration(name, dataType, SINK_ROLE,
151                CHANNEL_TYPE_ANY, callback);
152    }
153
154    /**
155     * Register an application configuration that acts as a Health SINK or in a Health
156     * SOURCE role.This is an asynchronous call and so
157     * the callback is used to notify success or failure if the function returns true.
158     *
159     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
160     *
161     * @param name The friendly name associated with the application or configuration.
162     * @param dataType The dataType of the Source role of Health Profile.
163     * @param channelType The channel type. Will be one of {@link #CHANNEL_TYPE_RELIABLE}  or {@link
164     * #CHANNEL_TYPE_STREAMING}
165     * @param callback - A callback to indicate success or failure.
166     * @return If true, callback will be called.
167     * @hide
168     */
169    public boolean registerAppConfiguration(String name, int dataType, int role,
170            int channelType, BluetoothHealthCallback callback) {
171        boolean result = false;
172        if (!isEnabled() || !checkAppParam(name, role, channelType, callback)) return result;
173
174        if (VDBG) log("registerApplication(" + name + ":" + dataType + ")");
175        BluetoothHealthCallbackWrapper wrapper = new BluetoothHealthCallbackWrapper(callback);
176        BluetoothHealthAppConfiguration config =
177                new BluetoothHealthAppConfiguration(name, dataType, role, channelType);
178
179        final IBluetoothHealth service = mService;
180        if (service != null) {
181            try {
182                result = service.registerAppConfiguration(config, wrapper);
183            } catch (RemoteException e) {
184                Log.e(TAG, e.toString());
185            }
186        } else {
187            Log.w(TAG, "Proxy not attached to service");
188            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
189        }
190        return result;
191    }
192
193    /**
194     * Unregister an application configuration that has been registered using
195     * {@link #registerSinkAppConfiguration}
196     *
197     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
198     *
199     * @param config The health app configuration
200     * @return Success or failure.
201     */
202    public boolean unregisterAppConfiguration(BluetoothHealthAppConfiguration config) {
203        boolean result = false;
204        final IBluetoothHealth service = mService;
205        if (service != null && isEnabled() && config != null) {
206            try {
207                result = service.unregisterAppConfiguration(config);
208            } catch (RemoteException e) {
209                Log.e(TAG, e.toString());
210            }
211        } else {
212            Log.w(TAG, "Proxy not attached to service");
213            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
214        }
215
216        return result;
217    }
218
219    /**
220     * Connect to a health device which has the {@link #SOURCE_ROLE}.
221     * This is an asynchronous call. If this function returns true, the callback
222     * associated with the application configuration will be called.
223     *
224     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
225     *
226     * @param device The remote Bluetooth device.
227     * @param config The application configuration which has been registered using {@link
228     * #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) }
229     * @return If true, the callback associated with the application config will be called.
230     */
231    public boolean connectChannelToSource(BluetoothDevice device,
232            BluetoothHealthAppConfiguration config) {
233        final IBluetoothHealth service = mService;
234        if (service != null && isEnabled() && isValidDevice(device) && config != null) {
235            try {
236                return service.connectChannelToSource(device, config);
237            } catch (RemoteException e) {
238                Log.e(TAG, e.toString());
239            }
240        } else {
241            Log.w(TAG, "Proxy not attached to service");
242            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
243        }
244        return false;
245    }
246
247    /**
248     * Connect to a health device which has the {@link #SINK_ROLE}.
249     * This is an asynchronous call. If this function returns true, the callback
250     * associated with the application configuration will be called.
251     *
252     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
253     *
254     * @param device The remote Bluetooth device.
255     * @param config The application configuration which has been registered using {@link
256     * #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) }
257     * @return If true, the callback associated with the application config will be called.
258     * @hide
259     */
260    public boolean connectChannelToSink(BluetoothDevice device,
261            BluetoothHealthAppConfiguration config, int channelType) {
262        final IBluetoothHealth service = mService;
263        if (service != null && isEnabled() && isValidDevice(device) && config != null) {
264            try {
265                return service.connectChannelToSink(device, config, channelType);
266            } catch (RemoteException e) {
267                Log.e(TAG, e.toString());
268            }
269        } else {
270            Log.w(TAG, "Proxy not attached to service");
271            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
272        }
273        return false;
274    }
275
276    /**
277     * Disconnect a connected health channel.
278     * This is an asynchronous call. If this function returns true, the callback
279     * associated with the application configuration will be called.
280     *
281     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
282     *
283     * @param device The remote Bluetooth device.
284     * @param config The application configuration which has been registered using {@link
285     * #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) }
286     * @param channelId The channel id associated with the channel
287     * @return If true, the callback associated with the application config will be called.
288     */
289    public boolean disconnectChannel(BluetoothDevice device,
290            BluetoothHealthAppConfiguration config, int channelId) {
291        final IBluetoothHealth service = mService;
292        if (service != null && isEnabled() && isValidDevice(device) && config != null) {
293            try {
294                return service.disconnectChannel(device, config, channelId);
295            } catch (RemoteException e) {
296                Log.e(TAG, e.toString());
297            }
298        } else {
299            Log.w(TAG, "Proxy not attached to service");
300            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
301        }
302        return false;
303    }
304
305    /**
306     * Get the file descriptor of the main channel associated with the remote device
307     * and application configuration.
308     *
309     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
310     *
311     * <p> Its the responsibility of the caller to close the ParcelFileDescriptor
312     * when done.
313     *
314     * @param device The remote Bluetooth health device
315     * @param config The application configuration
316     * @return null on failure, ParcelFileDescriptor on success.
317     */
318    public ParcelFileDescriptor getMainChannelFd(BluetoothDevice device,
319            BluetoothHealthAppConfiguration config) {
320        final IBluetoothHealth service = mService;
321        if (service != null && isEnabled() && isValidDevice(device) && config != null) {
322            try {
323                return service.getMainChannelFd(device, config);
324            } catch (RemoteException e) {
325                Log.e(TAG, e.toString());
326            }
327        } else {
328            Log.w(TAG, "Proxy not attached to service");
329            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
330        }
331        return null;
332    }
333
334    /**
335     * Get the current connection state of the profile.
336     *
337     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
338     *
339     * This is not specific to any application configuration but represents the connection
340     * state of the local Bluetooth adapter with the remote device. This can be used
341     * by applications like status bar which would just like to know the state of the
342     * local adapter.
343     *
344     * @param device Remote bluetooth device.
345     * @return State of the profile connection. One of {@link #STATE_CONNECTED}, {@link
346     * #STATE_CONNECTING}, {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING}
347     */
348    @Override
349    public int getConnectionState(BluetoothDevice device) {
350        final IBluetoothHealth service = mService;
351        if (service != null && isEnabled() && isValidDevice(device)) {
352            try {
353                return service.getHealthDeviceConnectionState(device);
354            } catch (RemoteException e) {
355                Log.e(TAG, e.toString());
356            }
357        } else {
358            Log.w(TAG, "Proxy not attached to service");
359            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
360        }
361        return STATE_DISCONNECTED;
362    }
363
364    /**
365     * Get connected devices for the health profile.
366     *
367     * <p> Return the set of devices which are in state {@link #STATE_CONNECTED}
368     *
369     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
370     *
371     * This is not specific to any application configuration but represents the connection
372     * state of the local Bluetooth adapter for this profile. This can be used
373     * by applications like status bar which would just like to know the state of the
374     * local adapter.
375     *
376     * @return List of devices. The list will be empty on error.
377     */
378    @Override
379    public List<BluetoothDevice> getConnectedDevices() {
380        final IBluetoothHealth service = mService;
381        if (service != null && isEnabled()) {
382            try {
383                return service.getConnectedHealthDevices();
384            } catch (RemoteException e) {
385                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
386                return new ArrayList<BluetoothDevice>();
387            }
388        }
389        if (service == null) Log.w(TAG, "Proxy not attached to service");
390        return new ArrayList<BluetoothDevice>();
391    }
392
393    /**
394     * Get a list of devices that match any of the given connection
395     * states.
396     *
397     * <p> If none of the devices match any of the given states,
398     * an empty list will be returned.
399     *
400     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
401     * This is not specific to any application configuration but represents the connection
402     * state of the local Bluetooth adapter for this profile. This can be used
403     * by applications like status bar which would just like to know the state of the
404     * local adapter.
405     *
406     * @param states Array of states. States can be one of {@link #STATE_CONNECTED}, {@link
407     * #STATE_CONNECTING}, {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING},
408     * @return List of devices. The list will be empty on error.
409     */
410    @Override
411    public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
412        final IBluetoothHealth service = mService;
413        if (service != null && isEnabled()) {
414            try {
415                return service.getHealthDevicesMatchingConnectionStates(states);
416            } catch (RemoteException e) {
417                Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
418                return new ArrayList<BluetoothDevice>();
419            }
420        }
421        if (service == null) Log.w(TAG, "Proxy not attached to service");
422        return new ArrayList<BluetoothDevice>();
423    }
424
425    private static class BluetoothHealthCallbackWrapper extends IBluetoothHealthCallback.Stub {
426        private BluetoothHealthCallback mCallback;
427
428        public BluetoothHealthCallbackWrapper(BluetoothHealthCallback callback) {
429            mCallback = callback;
430        }
431
432        @Override
433        public void onHealthAppConfigurationStatusChange(BluetoothHealthAppConfiguration config,
434                int status) {
435            mCallback.onHealthAppConfigurationStatusChange(config, status);
436        }
437
438        @Override
439        public void onHealthChannelStateChange(BluetoothHealthAppConfiguration config,
440                BluetoothDevice device, int prevState, int newState,
441                ParcelFileDescriptor fd, int channelId) {
442            mCallback.onHealthChannelStateChange(config, device, prevState, newState, fd,
443                    channelId);
444        }
445    }
446
447    /** Health Channel Connection State - Disconnected */
448    public static final int STATE_CHANNEL_DISCONNECTED = 0;
449    /** Health Channel Connection State - Connecting */
450    public static final int STATE_CHANNEL_CONNECTING = 1;
451    /** Health Channel Connection State - Connected */
452    public static final int STATE_CHANNEL_CONNECTED = 2;
453    /** Health Channel Connection State - Disconnecting */
454    public static final int STATE_CHANNEL_DISCONNECTING = 3;
455
456    /** Health App Configuration registration success */
457    public static final int APP_CONFIG_REGISTRATION_SUCCESS = 0;
458    /** Health App Configuration registration failure */
459    public static final int APP_CONFIG_REGISTRATION_FAILURE = 1;
460    /** Health App Configuration un-registration success */
461    public static final int APP_CONFIG_UNREGISTRATION_SUCCESS = 2;
462    /** Health App Configuration un-registration failure */
463    public static final int APP_CONFIG_UNREGISTRATION_FAILURE = 3;
464
465    private Context mContext;
466    private ServiceListener mServiceListener;
467    private volatile IBluetoothHealth mService;
468    BluetoothAdapter mAdapter;
469
470    /**
471     * Create a BluetoothHealth proxy object.
472     */
473    /*package*/ BluetoothHealth(Context context, ServiceListener l) {
474        mContext = context;
475        mServiceListener = l;
476        mAdapter = BluetoothAdapter.getDefaultAdapter();
477        IBluetoothManager mgr = mAdapter.getBluetoothManager();
478        if (mgr != null) {
479            try {
480                mgr.registerStateChangeCallback(mBluetoothStateChangeCallback);
481            } catch (RemoteException e) {
482                Log.e(TAG, "", e);
483            }
484        }
485
486        doBind();
487    }
488
489    boolean doBind() {
490        Intent intent = new Intent(IBluetoothHealth.class.getName());
491        ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0);
492        intent.setComponent(comp);
493        if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0,
494                mContext.getUser())) {
495            Log.e(TAG, "Could not bind to Bluetooth Health Service with " + intent);
496            return false;
497        }
498        return true;
499    }
500
501    /*package*/ void close() {
502        if (VDBG) log("close()");
503        IBluetoothManager mgr = mAdapter.getBluetoothManager();
504        if (mgr != null) {
505            try {
506                mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback);
507            } catch (Exception e) {
508                Log.e(TAG, "", e);
509            }
510        }
511
512        synchronized (mConnection) {
513            if (mService != null) {
514                try {
515                    mService = null;
516                    mContext.unbindService(mConnection);
517                } catch (Exception re) {
518                    Log.e(TAG, "", re);
519                }
520            }
521        }
522        mServiceListener = null;
523    }
524
525    private final ServiceConnection mConnection = new ServiceConnection() {
526        public void onServiceConnected(ComponentName className, IBinder service) {
527            if (DBG) Log.d(TAG, "Proxy object connected");
528            mService = IBluetoothHealth.Stub.asInterface(Binder.allowBlocking(service));
529
530            if (mServiceListener != null) {
531                mServiceListener.onServiceConnected(BluetoothProfile.HEALTH, BluetoothHealth.this);
532            }
533        }
534
535        public void onServiceDisconnected(ComponentName className) {
536            if (DBG) Log.d(TAG, "Proxy object disconnected");
537            mService = null;
538            if (mServiceListener != null) {
539                mServiceListener.onServiceDisconnected(BluetoothProfile.HEALTH);
540            }
541        }
542    };
543
544    private boolean isEnabled() {
545        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
546
547        if (adapter != null && adapter.getState() == BluetoothAdapter.STATE_ON) return true;
548        log("Bluetooth is Not enabled");
549        return false;
550    }
551
552    private static boolean isValidDevice(BluetoothDevice device) {
553        return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
554    }
555
556    private boolean checkAppParam(String name, int role, int channelType,
557            BluetoothHealthCallback callback) {
558        if (name == null || (role != SOURCE_ROLE && role != SINK_ROLE)
559                || (channelType != CHANNEL_TYPE_RELIABLE && channelType != CHANNEL_TYPE_STREAMING
560                    && channelType != CHANNEL_TYPE_ANY)
561                || callback == null) {
562            return false;
563        }
564        if (role == SOURCE_ROLE && channelType == CHANNEL_TYPE_ANY) return false;
565        return true;
566    }
567
568    private static void log(String msg) {
569        Log.d(TAG, msg);
570    }
571}
572