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 com.example.bluetooth.health;
18
19import android.app.Service;
20import android.bluetooth.BluetoothAdapter;
21import android.bluetooth.BluetoothDevice;
22import android.bluetooth.BluetoothHealth;
23import android.bluetooth.BluetoothHealthAppConfiguration;
24import android.bluetooth.BluetoothHealthCallback;
25import android.bluetooth.BluetoothProfile;
26import android.content.Intent;
27import android.os.Handler;
28import android.os.IBinder;
29import android.os.Message;
30import android.os.Messenger;
31import android.os.ParcelFileDescriptor;
32import android.os.RemoteException;
33import android.util.Log;
34import android.widget.Toast;
35
36import java.io.FileInputStream;
37import java.io.IOException;
38
39/**
40 * This Service encapsulates Bluetooth Health API to establish, manage, and disconnect
41 * communication between the Android device and a Bluetooth HDP-enabled device.  Possible HDP
42 * device type includes blood pressure monitor, glucose meter, thermometer, etc.
43 *
44 * As outlined in the
45 * <a href="http://developer.android.com/reference/android/bluetooth/BluetoothHealth.html">BluetoothHealth</a>
46 * documentation, the steps involve:
47 * 1. Get a reference to the BluetoothHealth proxy object.
48 * 2. Create a BluetoothHealth callback and register an application configuration that acts as a
49 *    Health SINK.
50 * 3. Establish connection to a health device.  Some devices will initiate the connection.  It is
51 *    unnecessary to carry out this step for those devices.
52 * 4. When connected successfully, read / write to the health device using the file descriptor.
53 *    The received data needs to be interpreted using a health manager which implements the
54 *    IEEE 11073-xxxxx specifications.
55 * 5. When done, close the health channel and unregister the application.  The channel will
56 *    also close when there is extended inactivity.
57 */
58public class BluetoothHDPService extends Service {
59    private static final String TAG = "BluetoothHDPService";
60
61    public static final int RESULT_OK = 0;
62    public static final int RESULT_FAIL = -1;
63
64    // Status codes sent back to the UI client.
65    // Application registration complete.
66    public static final int STATUS_HEALTH_APP_REG = 100;
67    // Application unregistration complete.
68    public static final int STATUS_HEALTH_APP_UNREG = 101;
69    // Channel creation complete.
70    public static final int STATUS_CREATE_CHANNEL = 102;
71    // Channel destroy complete.
72    public static final int STATUS_DESTROY_CHANNEL = 103;
73    // Reading data from Bluetooth HDP device.
74    public static final int STATUS_READ_DATA = 104;
75    // Done with reading data.
76    public static final int STATUS_READ_DATA_DONE = 105;
77
78    // Message codes received from the UI client.
79    // Register client with this service.
80    public static final int MSG_REG_CLIENT = 200;
81    // Unregister client from this service.
82    public static final int MSG_UNREG_CLIENT = 201;
83    // Register health application.
84    public static final int MSG_REG_HEALTH_APP = 300;
85    // Unregister health application.
86    public static final int MSG_UNREG_HEALTH_APP = 301;
87    // Connect channel.
88    public static final int MSG_CONNECT_CHANNEL = 400;
89    // Disconnect channel.
90    public static final int MSG_DISCONNECT_CHANNEL = 401;
91
92    private BluetoothHealthAppConfiguration mHealthAppConfig;
93    private BluetoothAdapter mBluetoothAdapter;
94    private BluetoothHealth mBluetoothHealth;
95    private BluetoothDevice mDevice;
96    private int mChannelId;
97
98    private Messenger mClient;
99
100    // Handles events sent by {@link HealthHDPActivity}.
101    private class IncomingHandler extends Handler {
102        @Override
103        public void handleMessage(Message msg) {
104            switch (msg.what) {
105                // Register UI client to this service so the client can receive messages.
106                case MSG_REG_CLIENT:
107                    Log.d(TAG, "Activity client registered");
108                    mClient = msg.replyTo;
109                    break;
110                // Unregister UI client from this service.
111                case MSG_UNREG_CLIENT:
112                    mClient = null;
113                    break;
114                // Register health application.
115                case MSG_REG_HEALTH_APP:
116                    registerApp(msg.arg1);
117                    break;
118                // Unregister health application.
119                case MSG_UNREG_HEALTH_APP:
120                    unregisterApp();
121                    break;
122                // Connect channel.
123                case MSG_CONNECT_CHANNEL:
124                    mDevice = (BluetoothDevice) msg.obj;
125                    connectChannel();
126                    break;
127                // Disconnect channel.
128                case MSG_DISCONNECT_CHANNEL:
129                    mDevice = (BluetoothDevice) msg.obj;
130                    disconnectChannel();
131                    break;
132                default:
133                    super.handleMessage(msg);
134            }
135        }
136    }
137
138    final Messenger mMessenger = new Messenger(new IncomingHandler());
139
140    /**
141     * Make sure Bluetooth and health profile are available on the Android device.  Stop service
142     * if they are not available.
143     */
144    @Override
145    public void onCreate() {
146        super.onCreate();
147        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
148        if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
149            // Bluetooth adapter isn't available.  The client of the service is supposed to
150            // verify that it is available and activate before invoking this service.
151            stopSelf();
152            return;
153        }
154        if (!mBluetoothAdapter.getProfileProxy(this, mBluetoothServiceListener,
155                BluetoothProfile.HEALTH)) {
156            Toast.makeText(this, R.string.bluetooth_health_profile_not_available,
157                    Toast.LENGTH_LONG);
158            stopSelf();
159            return;
160        }
161    }
162
163    @Override
164    public int onStartCommand(Intent intent, int flags, int startId) {
165        Log.d(TAG, "BluetoothHDPService is running.");
166        return START_STICKY;
167    }
168
169    @Override
170    public IBinder onBind(Intent intent) {
171        return mMessenger.getBinder();
172    };
173
174    // Register health application through the Bluetooth Health API.
175    private void registerApp(int dataType) {
176        mBluetoothHealth.registerSinkAppConfiguration(TAG, dataType, mHealthCallback);
177    }
178
179    // Unregister health application through the Bluetooth Health API.
180    private void unregisterApp() {
181        mBluetoothHealth.unregisterAppConfiguration(mHealthAppConfig);
182    }
183
184    // Connect channel through the Bluetooth Health API.
185    private void connectChannel() {
186        Log.i(TAG, "connectChannel()");
187        mBluetoothHealth.connectChannelToSource(mDevice, mHealthAppConfig);
188    }
189
190    // Disconnect channel through the Bluetooth Health API.
191    private void disconnectChannel() {
192        Log.i(TAG, "disconnectChannel()");
193        mBluetoothHealth.disconnectChannel(mDevice, mHealthAppConfig, mChannelId);
194    }
195
196    // Callbacks to handle connection set up and disconnection clean up.
197    private final BluetoothProfile.ServiceListener mBluetoothServiceListener =
198            new BluetoothProfile.ServiceListener() {
199        public void onServiceConnected(int profile, BluetoothProfile proxy) {
200            if (profile == BluetoothProfile.HEALTH) {
201                mBluetoothHealth = (BluetoothHealth) proxy;
202                if (Log.isLoggable(TAG, Log.DEBUG))
203                    Log.d(TAG, "onServiceConnected to profile: " + profile);
204            }
205        }
206
207        public void onServiceDisconnected(int profile) {
208            if (profile == BluetoothProfile.HEALTH) {
209                mBluetoothHealth = null;
210            }
211        }
212    };
213
214    private final BluetoothHealthCallback mHealthCallback = new BluetoothHealthCallback() {
215        // Callback to handle application registration and unregistration events.  The service
216        // passes the status back to the UI client.
217        public void onHealthAppConfigurationStatusChange(BluetoothHealthAppConfiguration config,
218                int status) {
219            if (status == BluetoothHealth.APP_CONFIG_REGISTRATION_FAILURE) {
220                mHealthAppConfig = null;
221                sendMessage(STATUS_HEALTH_APP_REG, RESULT_FAIL);
222            } else if (status == BluetoothHealth.APP_CONFIG_REGISTRATION_SUCCESS) {
223                mHealthAppConfig = config;
224                sendMessage(STATUS_HEALTH_APP_REG, RESULT_OK);
225            } else if (status == BluetoothHealth.APP_CONFIG_UNREGISTRATION_FAILURE ||
226                    status == BluetoothHealth.APP_CONFIG_UNREGISTRATION_SUCCESS) {
227                sendMessage(STATUS_HEALTH_APP_UNREG,
228                        status == BluetoothHealth.APP_CONFIG_UNREGISTRATION_SUCCESS ?
229                        RESULT_OK : RESULT_FAIL);
230            }
231        }
232
233        // Callback to handle channel connection state changes.
234        // Note that the logic of the state machine may need to be modified based on the HDP device.
235        // When the HDP device is connected, the received file descriptor is passed to the
236        // ReadThread to read the content.
237        public void onHealthChannelStateChange(BluetoothHealthAppConfiguration config,
238                BluetoothDevice device, int prevState, int newState, ParcelFileDescriptor fd,
239                int channelId) {
240            if (Log.isLoggable(TAG, Log.DEBUG))
241                Log.d(TAG, String.format("prevState\t%d ----------> newState\t%d",
242                        prevState, newState));
243            if (prevState == BluetoothHealth.STATE_CHANNEL_DISCONNECTED &&
244                    newState == BluetoothHealth.STATE_CHANNEL_CONNECTED) {
245                if (config.equals(mHealthAppConfig)) {
246                    mChannelId = channelId;
247                    sendMessage(STATUS_CREATE_CHANNEL, RESULT_OK);
248                    (new ReadThread(fd)).start();
249                } else {
250                    sendMessage(STATUS_CREATE_CHANNEL, RESULT_FAIL);
251                }
252            } else if (prevState == BluetoothHealth.STATE_CHANNEL_CONNECTING &&
253                       newState == BluetoothHealth.STATE_CHANNEL_DISCONNECTED) {
254                sendMessage(STATUS_CREATE_CHANNEL, RESULT_FAIL);
255            } else if (newState == BluetoothHealth.STATE_CHANNEL_DISCONNECTED) {
256                if (config.equals(mHealthAppConfig)) {
257                    sendMessage(STATUS_DESTROY_CHANNEL, RESULT_OK);
258                } else {
259                    sendMessage(STATUS_DESTROY_CHANNEL, RESULT_FAIL);
260                }
261            }
262        }
263    };
264
265    // Sends an update message to registered UI client.
266    private void sendMessage(int what, int value) {
267        if (mClient == null) {
268            Log.d(TAG, "No clients registered.");
269            return;
270        }
271
272        try {
273            mClient.send(Message.obtain(null, what, value, 0));
274        } catch (RemoteException e) {
275            // Unable to reach client.
276            e.printStackTrace();
277        }
278    }
279
280    // Thread to read incoming data received from the HDP device.  This sample application merely
281    // reads the raw byte from the incoming file descriptor.  The data should be interpreted using
282    // a health manager which implements the IEEE 11073-xxxxx specifications.
283    private class ReadThread extends Thread {
284        private ParcelFileDescriptor mFd;
285
286        public ReadThread(ParcelFileDescriptor fd) {
287            super();
288            mFd = fd;
289        }
290
291        @Override
292        public void run() {
293            FileInputStream fis = new FileInputStream(mFd.getFileDescriptor());
294            final byte data[] = new byte[8192];
295            try {
296                while(fis.read(data) > -1) {
297                    // At this point, the application can pass the raw data to a parser that
298                    // has implemented the IEEE 11073-xxxxx specifications.  Instead, this sample
299                    // simply indicates that some data has been received.
300                    sendMessage(STATUS_READ_DATA, 0);
301                }
302            } catch(IOException ioe) {}
303            if (mFd != null) {
304                try {
305                    mFd.close();
306                } catch (IOException e) { /* Do nothing. */ }
307            }
308            sendMessage(STATUS_READ_DATA_DONE, 0);
309        }
310    }
311}
312