altosdroid: Track device location in app, not telemetry service
[fw/altos] / altosdroid / src / org / altusmetrum / AltosDroid / TelemetryService.java
1 /*
2  * Copyright © 2012 Mike Beattie <mike@ethernal.org>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; version 2 of the License.
7  *
8  * This program is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11  * General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License along
14  * with this program; if not, write to the Free Software Foundation, Inc.,
15  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
16  */
17
18 package org.altusmetrum.AltosDroid;
19
20 import java.lang.ref.WeakReference;
21 import java.util.concurrent.TimeoutException;
22 import java.util.*;
23
24 import android.app.Notification;
25 //import android.app.NotificationManager;
26 import android.app.PendingIntent;
27 import android.app.Service;
28 import android.bluetooth.BluetoothDevice;
29 import android.bluetooth.BluetoothAdapter;
30 import android.hardware.usb.*;
31 import android.content.Intent;
32 import android.content.Context;
33 import android.os.Bundle;
34 import android.os.IBinder;
35 import android.os.Handler;
36 import android.os.Message;
37 import android.os.Messenger;
38 import android.os.RemoteException;
39 import android.os.Looper;
40 import android.widget.Toast;
41 import android.location.Criteria;
42
43 import org.altusmetrum.altoslib_10.*;
44
45 public class TelemetryService extends Service {
46
47         static final int MSG_REGISTER_CLIENT   = 1;
48         static final int MSG_UNREGISTER_CLIENT = 2;
49         static final int MSG_CONNECT           = 3;
50         static final int MSG_OPEN_USB          = 4;
51         static final int MSG_CONNECTED         = 5;
52         static final int MSG_CONNECT_FAILED    = 6;
53         static final int MSG_DISCONNECTED      = 7;
54         static final int MSG_TELEMETRY         = 8;
55         static final int MSG_SETFREQUENCY      = 9;
56         static final int MSG_CRC_ERROR         = 10;
57         static final int MSG_SETBAUD           = 11;
58         static final int MSG_DISCONNECT        = 12;
59         static final int MSG_DELETE_SERIAL     = 13;
60
61         // Unique Identification Number for the Notification.
62         // We use it on Notification start, and to cancel it.
63         private int NOTIFICATION = R.string.telemetry_service_label;
64         //private NotificationManager mNM;
65
66         ArrayList<Messenger> clients = new ArrayList<Messenger>(); // Keeps track of all current registered clients.
67         final Handler   handler   = new IncomingHandler(this);
68         final Messenger messenger = new Messenger(handler); // Target we publish for clients to send messages to IncomingHandler.
69
70         // Name of the connected device
71         DeviceAddress address;
72         private AltosDroidLink  altos_link  = null;
73         private TelemetryReader telemetry_reader = null;
74         private TelemetryLogger telemetry_logger = null;
75
76         // Local Bluetooth adapter
77         private BluetoothAdapter bluetooth_adapter = null;
78
79         // Last data seen; send to UI when it starts
80         private TelemetryState  telemetry_state;
81
82         // Handler of incoming messages from clients.
83         static class IncomingHandler extends Handler {
84                 private final WeakReference<TelemetryService> service;
85                 IncomingHandler(TelemetryService s) { service = new WeakReference<TelemetryService>(s); }
86
87                 @Override
88                 public void handleMessage(Message msg) {
89                         TelemetryService s = service.get();
90                         AltosDroidLink bt = null;
91                         if (s == null)
92                                 return;
93                         switch (msg.what) {
94
95                                 /* Messages from application */
96                         case MSG_REGISTER_CLIENT:
97                                 s.add_client(msg.replyTo);
98                                 break;
99                         case MSG_UNREGISTER_CLIENT:
100                                 s.remove_client(msg.replyTo);
101                                 break;
102                         case MSG_CONNECT:
103                                 AltosDebug.debug("Connect command received");
104                                 DeviceAddress address = (DeviceAddress) msg.obj;
105                                 AltosDroidPreferences.set_active_device(address);
106                                 s.start_altos_bluetooth(address, false);
107                                 break;
108                         case MSG_OPEN_USB:
109                                 AltosDebug.debug("Open USB command received");
110                                 UsbDevice device = (UsbDevice) msg.obj;
111                                 s.start_usb(device);
112                                 break;
113                         case MSG_DISCONNECT:
114                                 AltosDebug.debug("Disconnect command received");
115                                 s.address = null;
116                                 s.disconnect(true);
117                                 break;
118                         case MSG_DELETE_SERIAL:
119                                 AltosDebug.debug("Delete Serial command received");
120                                 s.delete_serial((Integer) msg.obj);
121                                 break;
122                         case MSG_SETFREQUENCY:
123                                 AltosDebug.debug("MSG_SETFREQUENCY");
124                                 s.telemetry_state.frequency = (Double) msg.obj;
125                                 if (s.telemetry_state.connect == TelemetryState.CONNECT_CONNECTED) {
126                                         try {
127                                                 s.altos_link.set_radio_frequency(s.telemetry_state.frequency);
128                                                 s.altos_link.save_frequency();
129                                         } catch (InterruptedException e) {
130                                         } catch (TimeoutException e) {
131                                         }
132                                 }
133                                 s.send_to_clients();
134                                 break;
135                         case MSG_SETBAUD:
136                                 AltosDebug.debug("MSG_SETBAUD");
137                                 s.telemetry_state.telemetry_rate = (Integer) msg.obj;
138                                 if (s.telemetry_state.connect == TelemetryState.CONNECT_CONNECTED) {
139                                         s.altos_link.set_telemetry_rate(s.telemetry_state.telemetry_rate);
140                                         s.altos_link.save_telemetry_rate();
141                                 }
142                                 s.send_to_clients();
143                                 break;
144
145                                 /*
146                                  *Messages from AltosBluetooth
147                                  */
148                         case MSG_CONNECTED:
149                                 AltosDebug.debug("MSG_CONNECTED");
150                                 bt = (AltosDroidLink) msg.obj;
151
152                                 if (bt != s.altos_link) {
153                                         AltosDebug.debug("Stale message");
154                                         break;
155                                 }
156                                 AltosDebug.debug("Connected to device");
157                                 try {
158                                         s.connected();
159                                 } catch (InterruptedException ie) {
160                                 }
161                                 break;
162                         case MSG_CONNECT_FAILED:
163                                 AltosDebug.debug("MSG_CONNECT_FAILED");
164                                 bt = (AltosDroidLink) msg.obj;
165
166                                 if (bt != s.altos_link) {
167                                         AltosDebug.debug("Stale message");
168                                         break;
169                                 }
170                                 if (s.address != null) {
171                                         AltosDebug.debug("Connection failed... retrying");
172                                         s.start_altos_bluetooth(s.address, true);
173                                 } else {
174                                         s.disconnect(true);
175                                 }
176                                 break;
177                         case MSG_DISCONNECTED:
178
179                                 /* This can be sent by either AltosDroidLink or TelemetryReader */
180                                 AltosDebug.debug("MSG_DISCONNECTED");
181                                 bt = (AltosDroidLink) msg.obj;
182
183                                 if (bt != s.altos_link) {
184                                         AltosDebug.debug("Stale message");
185                                         break;
186                                 }
187                                 if (s.address != null) {
188                                         AltosDebug.debug("Connection lost... retrying");
189                                         s.start_altos_bluetooth(s.address, true);
190                                 } else {
191                                         s.disconnect(true);
192                                 }
193                                 break;
194
195                                 /*
196                                  * Messages from TelemetryReader
197                                  */
198                         case MSG_TELEMETRY:
199                                 s.telemetry((AltosTelemetry) msg.obj);
200                                 break;
201                         case MSG_CRC_ERROR:
202                                 // forward crc error messages
203                                 s.telemetry_state.crc_errors = (Integer) msg.obj;
204                                 s.send_to_clients();
205                                 break;
206                         default:
207                                 super.handleMessage(msg);
208                         }
209                 }
210         }
211
212         /* Handle telemetry packet
213          */
214         private void telemetry(AltosTelemetry telem) {
215                 AltosState      state;
216
217                 if (telemetry_state.states.containsKey(telem.serial))
218                         state = telemetry_state.states.get(telem.serial).clone();
219                 else
220                         state = new AltosState();
221                 telem.update_state(state);
222                 telemetry_state.states.put(telem.serial, state);
223                 if (state != null) {
224                         AltosPreferences.set_state(telem.serial, state, null);
225                 }
226                 send_to_clients();
227         }
228
229         /* Construct the message to deliver to clients
230          */
231         private Message message() {
232                 if (telemetry_state == null)
233                         AltosDebug.debug("telemetry_state null!");
234                 if (telemetry_state.states == null)
235                         AltosDebug.debug("telemetry_state.states null!");
236                 return Message.obtain(null, AltosDroid.MSG_STATE, telemetry_state);
237         }
238
239         /* A new friend has connected
240          */
241         private void add_client(Messenger client) {
242
243                 clients.add(client);
244                 AltosDebug.debug("Client bound to service");
245
246                 /* On connect, send the current state to the new client
247                  */
248                 send_to_client(client);
249
250                 /* If we've got an address from a previous session, then
251                  * go ahead and try to reconnect to the device
252                  */
253                 if (address != null && telemetry_state.connect == TelemetryState.CONNECT_DISCONNECTED) {
254                         AltosDebug.debug("Reconnecting now...");
255                         start_altos_bluetooth(address, false);
256                 }
257         }
258
259         /* A client has disconnected, clean up
260          */
261         private void remove_client(Messenger client) {
262                 clients.remove(client);
263                 AltosDebug.debug("Client unbound from service");
264
265                 /* When the list of clients is empty, stop the service if
266                  * we have no current telemetry source
267                  */
268
269                  if (clients.isEmpty() && telemetry_state.connect == TelemetryState.CONNECT_DISCONNECTED) {
270                          AltosDebug.debug("No clients, no connection. Stopping\n");
271                          stopSelf();
272                  }
273         }
274
275         private void send_to_client(Messenger client) {
276                 Message m = message();
277                 try {
278                         client.send(m);
279                 } catch (RemoteException e) {
280                         AltosDebug.error("Client %s disappeared", client.toString());
281                         remove_client(client);
282                 }
283         }
284
285         private void send_to_clients() {
286                 for (Messenger client : clients)
287                         send_to_client(client);
288         }
289
290         private void disconnect(boolean notify) {
291                 AltosDebug.debug("disconnect(): begin");
292
293                 telemetry_state.connect = TelemetryState.CONNECT_DISCONNECTED;
294                 telemetry_state.address = null;
295
296                 if (altos_link != null)
297                         altos_link.closing();
298
299                 stop_receiver_voltage_timer();
300
301                 if (telemetry_reader != null) {
302                         AltosDebug.debug("disconnect(): stopping TelemetryReader");
303                         telemetry_reader.interrupt();
304                         try {
305                                 telemetry_reader.join();
306                         } catch (InterruptedException e) {
307                         }
308                         telemetry_reader = null;
309                 }
310                 if (telemetry_logger != null) {
311                         AltosDebug.debug("disconnect(): stopping TelemetryLogger");
312                         telemetry_logger.stop();
313                         telemetry_logger = null;
314                 }
315                 if (altos_link != null) {
316                         AltosDebug.debug("disconnect(): stopping AltosDroidLink");
317                         altos_link.close();
318                         altos_link = null;
319                 }
320                 telemetry_state.config = null;
321                 if (notify) {
322                         AltosDebug.debug("disconnect(): send message to clients");
323                         send_to_clients();
324                         if (clients.isEmpty()) {
325                                 AltosDebug.debug("disconnect(): no clients, terminating");
326                                 stopSelf();
327                         }
328                 }
329         }
330
331         private void start_usb(UsbDevice device) {
332                 AltosUsb        d = new AltosUsb(this, device, handler);
333
334                 if (d != null) {
335                         disconnect(false);
336                         altos_link = d;
337                         try {
338                                 connected();
339                         } catch (InterruptedException ie) {
340                         }
341                 }
342         }
343
344         private void delete_serial(int serial) {
345                 telemetry_state.states.remove((Integer) serial);
346                 AltosPreferences.remove_state(serial);
347                 send_to_clients();
348         }
349
350         private void start_altos_bluetooth(DeviceAddress address, boolean pause) {
351                 // Get the BLuetoothDevice object
352                 BluetoothDevice device = bluetooth_adapter.getRemoteDevice(address.address);
353
354                 disconnect(false);
355                 this.address = address;
356                 AltosDebug.debug("start_altos_bluetooth(): Connecting to %s (%s)", device.getName(), device.getAddress());
357                 altos_link = new AltosBluetooth(device, handler, pause);
358                 telemetry_state.connect = TelemetryState.CONNECT_CONNECTING;
359                 telemetry_state.address = address;
360                 send_to_clients();
361         }
362
363         // Timer for receiver battery voltage monitoring
364         Timer receiver_voltage_timer;
365
366         private void update_receiver_voltage() {
367                 if (altos_link != null) {
368                         try {
369                                 double  voltage = altos_link.monitor_battery();
370                                 telemetry_state.receiver_battery = voltage;
371                         } catch (InterruptedException ie) {
372                         }
373                 }
374         }
375
376         private void stop_receiver_voltage_timer() {
377                 if (receiver_voltage_timer != null) {
378                         receiver_voltage_timer.cancel();
379                         receiver_voltage_timer.purge();
380                         receiver_voltage_timer = null;
381                 }
382         }
383
384         private void start_receiver_voltage_timer() {
385                 if (receiver_voltage_timer == null && altos_link.has_monitor_battery()) {
386                         receiver_voltage_timer = new Timer();
387                         receiver_voltage_timer.scheduleAtFixedRate(new TimerTask() { public void run() {update_receiver_voltage();}}, 1000L, 10000L);
388                 }
389         }
390
391         private void connected() throws InterruptedException {
392                 AltosDebug.debug("connected top");
393                 AltosDebug.check_ui("connected\n");
394                 try {
395                         if (altos_link == null)
396                                 throw new InterruptedException("no bluetooth");
397                         telemetry_state.config = altos_link.config_data();
398                         altos_link.set_radio_frequency(telemetry_state.frequency);
399                         altos_link.set_telemetry_rate(telemetry_state.telemetry_rate);
400                 } catch (TimeoutException e) {
401                         // If this timed out, then we really want to retry it, but
402                         // probably safer to just retry the connection from scratch.
403                         AltosDebug.debug("connected timeout");
404                         if (address != null) {
405                                 AltosDebug.debug("connected timeout, retrying");
406                                 start_altos_bluetooth(address, true);
407                         } else {
408                                 handler.obtainMessage(MSG_CONNECT_FAILED).sendToTarget();
409                                 disconnect(true);
410                         }
411                         return;
412                 }
413
414                 AltosDebug.debug("connected bluetooth configured");
415                 telemetry_state.connect = TelemetryState.CONNECT_CONNECTED;
416                 telemetry_state.address = address;
417
418                 telemetry_reader = new TelemetryReader(altos_link, handler);
419                 telemetry_reader.start();
420
421                 AltosDebug.debug("connected TelemetryReader started");
422
423                 telemetry_logger = new TelemetryLogger(this, altos_link);
424
425                 start_receiver_voltage_timer();
426
427                 AltosDebug.debug("Notify UI of connection");
428
429                 send_to_clients();
430         }
431
432
433         @Override
434         public void onCreate() {
435
436                 AltosDebug.init(this);
437
438                 // Initialise preferences
439                 AltosDroidPreferences.init(this);
440
441                 // Get local Bluetooth adapter
442                 bluetooth_adapter = BluetoothAdapter.getDefaultAdapter();
443
444                 // If the adapter is null, then Bluetooth is not supported
445                 if (bluetooth_adapter == null) {
446                         Toast.makeText(this, "Bluetooth is not available", Toast.LENGTH_LONG).show();
447                 }
448
449                 telemetry_state = new TelemetryState();
450
451                 // Create a reference to the NotificationManager so that we can update our notifcation text later
452                 //mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
453
454                 telemetry_state.connect = TelemetryState.CONNECT_DISCONNECTED;
455                 telemetry_state.address = null;
456
457                 /* Pull the saved state information out of the preferences database
458                  */
459                 ArrayList<Integer> serials = AltosPreferences.list_states();
460
461                 telemetry_state.latest_serial = AltosPreferences.latest_state();
462
463                 for (int serial : serials) {
464                         AltosSavedState saved_state = AltosPreferences.state(serial);
465                         if (saved_state != null) {
466                                 if (serial == 0) {
467                                         serial = saved_state.state.serial;
468                                         AltosPreferences.set_state(serial, saved_state.state, saved_state.listener_state);
469                                         AltosPreferences.remove_state(0);
470                                 }
471                                 if (telemetry_state.latest_serial == 0)
472                                         telemetry_state.latest_serial = serial;
473
474                                 AltosDebug.debug("recovered old state serial %d flight %d\n",
475                                                  serial,
476                                                  saved_state.state.flight);
477                                 if (saved_state.state.gps != null)
478                                         AltosDebug.debug("\tposition %f,%f\n",
479                                                          saved_state.state.gps.lat,
480                                                          saved_state.state.gps.lon);
481                                 telemetry_state.states.put(serial, saved_state.state);
482                         }
483                 }
484         }
485
486         @Override
487         public int onStartCommand(Intent intent, int flags, int startId) {
488                 AltosDebug.debug("Received start id %d: %s", startId, intent);
489
490                 CharSequence text = getText(R.string.telemetry_service_started);
491
492                 // Create notification to be displayed while the service runs
493                 Notification notification = new Notification(R.drawable.am_status_c, text, 0);
494
495                 // The PendingIntent to launch our activity if the user selects this notification
496                 PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
497                                 new Intent(this, AltosDroid.class), 0);
498
499                 // Set the info for the views that show in the notification panel.
500                 notification.setLatestEventInfo(this, getText(R.string.telemetry_service_label), text, contentIntent);
501
502                 // Set the notification to be in the "Ongoing" section.
503                 notification.flags |= Notification.FLAG_ONGOING_EVENT;
504
505                 // Move us into the foreground.
506                 startForeground(NOTIFICATION, notification);
507
508                 /* Start bluetooth if we don't have a connection already */
509                 if (intent != null &&
510                     (telemetry_state.connect == TelemetryState.CONNECT_NONE ||
511                      telemetry_state.connect == TelemetryState.CONNECT_DISCONNECTED))
512                 {
513                         String  action = intent.getAction();
514
515                         if (action.equals(AltosDroid.ACTION_BLUETOOTH)) {
516                                 DeviceAddress address = AltosDroidPreferences.active_device();
517                                 if (address != null && !address.address.startsWith("USB"))
518                                         start_altos_bluetooth(address, false);
519                         }
520                 }
521
522                 // We want this service to continue running until it is explicitly
523                 // stopped, so return sticky.
524                 return START_STICKY;
525         }
526
527         @Override
528         public void onDestroy() {
529
530                 // Stop the bluetooth Comms threads
531                 disconnect(true);
532
533                 // Demote us from the foreground, and cancel the persistent notification.
534                 stopForeground(true);
535
536                 // Tell the user we stopped.
537                 Toast.makeText(this, R.string.telemetry_service_stopped, Toast.LENGTH_SHORT).show();
538         }
539
540         @Override
541         public IBinder onBind(Intent intent) {
542                 return messenger.getBinder();
543         }
544 }