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