71ac298e77b6d6000d4620db1a16eddd57871b5b
[fw/altos] / altosdroid / src / org / altusmetrum / AltosDroid / AltosDroid.java
1 /*
2  * Copyright © 2012-2013 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.text.*;
22 import java.util.*;
23 import java.io.*;
24
25 import android.app.Activity;
26 import android.app.PendingIntent;
27 import android.bluetooth.BluetoothAdapter;
28 import android.bluetooth.BluetoothDevice;
29 import android.content.Intent;
30 import android.content.Context;
31 import android.content.ComponentName;
32 import android.content.ServiceConnection;
33 import android.content.DialogInterface;
34 import android.os.IBinder;
35 import android.os.Bundle;
36 import android.os.Handler;
37 import android.os.Message;
38 import android.os.Messenger;
39 import android.os.RemoteException;
40 import android.content.res.Resources;
41 import android.support.v4.app.FragmentActivity;
42 import android.support.v4.app.FragmentManager;
43 import android.util.DisplayMetrics;
44 import android.view.*;
45 import android.widget.*;
46 import android.app.AlertDialog;
47 import android.location.Location;
48 import android.hardware.usb.*;
49 import android.graphics.*;
50 import android.graphics.drawable.*;
51
52 import org.altusmetrum.altoslib_7.*;
53
54 public class AltosDroid extends FragmentActivity implements AltosUnitsListener {
55
56         // Actions sent to the telemetry server at startup time
57
58         public static final String ACTION_BLUETOOTH = "org.altusmetrum.AltosDroid.BLUETOOTH";
59         public static final String ACTION_USB = "org.altusmetrum.AltosDroid.USB";
60
61         // Message types received by our Handler
62
63         public static final int MSG_STATE           = 1;
64         public static final int MSG_UPDATE_AGE      = 2;
65
66         // Intent request codes
67         public static final int REQUEST_CONNECT_DEVICE = 1;
68         public static final int REQUEST_ENABLE_BT      = 2;
69         public static final int REQUEST_PRELOAD_MAPS   = 3;
70         public static final int REQUEST_MAP_TYPE       = 4;
71
72         public int map_type = AltosMap.maptype_hybrid;
73
74         public static FragmentManager   fm;
75
76         private BluetoothAdapter mBluetoothAdapter = null;
77
78         // Flight state values
79         private TextView mCallsignView;
80         private TextView mRSSIView;
81         private TextView mSerialView;
82         private TextView mFlightView;
83         private RelativeLayout mStateLayout;
84         private TextView mStateView;
85         private TextView mAgeView;
86         private boolean  mAgeViewOld;
87         private int mAgeNewColor;
88         private int mAgeOldColor;
89
90         public static final String      tab_pad_name = "pad";
91         public static final String      tab_flight_name = "flight";
92         public static final String      tab_recover_name = "recover";
93         public static final String      tab_map_name = "map";
94
95         // field to display the version at the bottom of the screen
96         private TextView mVersion;
97
98         private double frequency;
99         private int telemetry_rate;
100
101         // Tabs
102         TabHost         mTabHost;
103         AltosViewPager  mViewPager;
104         TabsAdapter     mTabsAdapter;
105         ArrayList<AltosDroidTab> mTabs = new ArrayList<AltosDroidTab>();
106         int             tabHeight;
107
108         // Timer and Saved flight state for Age calculation
109         private Timer timer;
110         AltosState saved_state;
111         TelemetryState  telemetry_state;
112         Integer[]       serials;
113
114         UsbDevice       pending_usb_device;
115         boolean         start_with_usb;
116
117         // Service
118         private boolean mIsBound   = false;
119         private Messenger mService = null;
120         final Messenger mMessenger = new Messenger(new IncomingHandler(this));
121
122         // Text to Speech
123         private AltosVoice mAltosVoice = null;
124
125         // The Handler that gets information back from the Telemetry Service
126         static class IncomingHandler extends Handler {
127                 private final WeakReference<AltosDroid> mAltosDroid;
128                 IncomingHandler(AltosDroid ad) { mAltosDroid = new WeakReference<AltosDroid>(ad); }
129
130                 @Override
131                 public void handleMessage(Message msg) {
132                         AltosDroid ad = mAltosDroid.get();
133
134                         switch (msg.what) {
135                         case MSG_STATE:
136                                 if (msg.obj == null) {
137                                         AltosDebug.debug("telemetry_state null!");
138                                         return;
139                                 }
140                                 ad.update_state((TelemetryState) msg.obj);
141                                 break;
142                         case MSG_UPDATE_AGE:
143                                 ad.update_age();
144                                 break;
145                         }
146                 }
147         };
148
149
150         private ServiceConnection mConnection = new ServiceConnection() {
151                 public void onServiceConnected(ComponentName className, IBinder service) {
152                         mService = new Messenger(service);
153                         try {
154                                 Message msg = Message.obtain(null, TelemetryService.MSG_REGISTER_CLIENT);
155                                 msg.replyTo = mMessenger;
156                                 mService.send(msg);
157                         } catch (RemoteException e) {
158                                 // In this case the service has crashed before we could even do anything with it
159                         }
160                         if (pending_usb_device != null) {
161                                 try {
162                                         mService.send(Message.obtain(null, TelemetryService.MSG_OPEN_USB, pending_usb_device));
163                                         pending_usb_device = null;
164                                 } catch (RemoteException e) {
165                                 }
166                         }
167                 }
168
169                 public void onServiceDisconnected(ComponentName className) {
170                         // This is called when the connection with the service has been unexpectedly disconnected - process crashed.
171                         mService = null;
172                 }
173         };
174
175         void doBindService() {
176                 bindService(new Intent(this, TelemetryService.class), mConnection, Context.BIND_AUTO_CREATE);
177                 mIsBound = true;
178         }
179
180         void doUnbindService() {
181                 if (mIsBound) {
182                         // If we have received the service, and hence registered with it, then now is the time to unregister.
183                         if (mService != null) {
184                                 try {
185                                         Message msg = Message.obtain(null, TelemetryService.MSG_UNREGISTER_CLIENT);
186                                         msg.replyTo = mMessenger;
187                                         mService.send(msg);
188                                 } catch (RemoteException e) {
189                                         // There is nothing special we need to do if the service has crashed.
190                                 }
191                         }
192                         // Detach our existing connection.
193                         unbindService(mConnection);
194                         mIsBound = false;
195                 }
196         }
197
198         public void registerTab(AltosDroidTab mTab) {
199                 mTabs.add(mTab);
200         }
201
202         public void unregisterTab(AltosDroidTab mTab) {
203                 mTabs.remove(mTab);
204         }
205
206         public void units_changed(boolean imperial_units) {
207                 for (AltosDroidTab mTab : mTabs)
208                         mTab.units_changed(imperial_units);
209         }
210
211         void update_title(TelemetryState telemetry_state) {
212                 switch (telemetry_state.connect) {
213                 case TelemetryState.CONNECT_CONNECTED:
214                         if (telemetry_state.config != null) {
215                                 String str = String.format("S/N %d %6.3f MHz", telemetry_state.config.serial,
216                                                            telemetry_state.frequency);
217                                 if (telemetry_state.telemetry_rate != AltosLib.ao_telemetry_rate_38400)
218                                         str = str.concat(String.format(" %d bps",
219                                                                        AltosLib.ao_telemetry_rate_values[telemetry_state.telemetry_rate]));
220                                 setTitle(str);
221                         } else {
222                                 setTitle(R.string.title_connected_to);
223                         }
224                         break;
225                 case TelemetryState.CONNECT_CONNECTING:
226                         if (telemetry_state.address != null)
227                                 setTitle(String.format("Connecting to %s...", telemetry_state.address.name));
228                         else
229                                 setTitle("Connecting to something...");
230                         break;
231                 case TelemetryState.CONNECT_DISCONNECTED:
232                 case TelemetryState.CONNECT_NONE:
233                         setTitle(R.string.title_not_connected);
234                         break;
235                 }
236         }
237
238         void start_timer() {
239                 if (timer == null) {
240                         timer = new Timer();
241                         timer.scheduleAtFixedRate(new TimerTask(){ public void run() {onTimerTick();}}, 1000L, 1000L);
242                 }
243         }
244
245         void stop_timer() {
246                 if (timer != null) {
247                         timer.cancel();
248                         timer.purge();
249                         timer = null;
250                 }
251         }
252
253         int     selected_serial = 0;
254         int     current_serial;
255         long    switch_time;
256
257         void set_switch_time() {
258                 switch_time = System.currentTimeMillis();
259                 selected_serial = 0;
260         }
261
262         boolean registered_units_listener;
263
264         void update_state(TelemetryState new_telemetry_state) {
265
266                 if (new_telemetry_state != null)
267                         telemetry_state = new_telemetry_state;
268
269                 if (selected_serial != 0)
270                         current_serial = selected_serial;
271
272                 if (current_serial == 0)
273                         current_serial = telemetry_state.latest_serial;
274
275                 if (!registered_units_listener) {
276                         registered_units_listener = true;
277                         AltosPreferences.register_units_listener(this);
278                 }
279
280                 serials = telemetry_state.states.keySet().toArray(new Integer[0]);
281                 Arrays.sort(serials);
282
283                 update_title(telemetry_state);
284
285                 AltosState      state = null;
286                 boolean         aged = true;
287
288                 if (telemetry_state.states.containsKey(current_serial)) {
289                         state = telemetry_state.states.get(current_serial);
290                         int age = state_age(state);
291                         if (age < 20)
292                                 aged = false;
293                         if (current_serial == selected_serial)
294                                 aged = false;
295                         else if (switch_time != 0 && (switch_time - state.received_time) > 0)
296                                 aged = true;
297                 }
298
299                 if (aged) {
300                         AltosState      newest_state = null;
301                         int             newest_age = 0;
302
303                         for (int serial : telemetry_state.states.keySet()) {
304                                 AltosState      existing = telemetry_state.states.get(serial);
305                                 int             existing_age = state_age(existing);
306
307                                 if (newest_state == null || existing_age < newest_age) {
308                                         newest_state = existing;
309                                         newest_age = existing_age;
310                                 }
311                         }
312
313                         if (newest_state != null)
314                                 state = newest_state;
315                 }
316
317                 update_ui(telemetry_state, state, telemetry_state.location);
318
319                 start_timer();
320         }
321
322         boolean same_string(String a, String b) {
323                 if (a == null) {
324                         if (b == null)
325                                 return true;
326                         return false;
327                 } else {
328                         if (b == null)
329                                 return false;
330                         return a.equals(b);
331                 }
332         }
333
334
335         private int blend_component(int a, int b, double r, int shift, int mask) {
336                 return ((int) (((a >> shift) & mask) * r + ((b >> shift) & mask) * (1 - r)) & mask) << shift;
337         }
338         private int blend_color(int a, int b, double r) {
339                 return (blend_component(a, b, r, 0, 0xff) |
340                         blend_component(a, b, r, 8, 0xff) |
341                         blend_component(a, b, r, 16, 0xff) |
342                         blend_component(a, b, r, 24, 0xff));
343         }
344
345         int state_age(AltosState state) {
346                 return (int) ((System.currentTimeMillis() - state.received_time + 500) / 1000);
347         }
348
349         void set_screen_on(int age) {
350                 if (age < 60)
351                         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
352                 else
353                         getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
354         }
355
356         void update_age() {
357                 if (saved_state != null) {
358                         int age = state_age(saved_state);
359
360                         double age_scale = age / 100.0;
361
362                         if (age_scale > 1.0)
363                                 age_scale = 1.0;
364
365                         mAgeView.setTextColor(blend_color(mAgeOldColor, mAgeNewColor, age_scale));
366
367                         set_screen_on(age);
368
369                         String  text;
370                         if (age < 60)
371                                 text = String.format("%ds", age);
372                         else if (age < 60 * 60)
373                                 text = String.format("%dm", age / 60);
374                         else if (age < 60 * 60 * 24)
375                                 text = String.format("%dh", age / (60 * 60));
376                         else
377                                 text = String.format("%dd", age / (24 * 60 * 60));
378                         mAgeView.setText(text);
379                 }
380         }
381
382         void update_ui(TelemetryState telem_state, AltosState state, Location location) {
383
384                 int prev_state = AltosLib.ao_flight_invalid;
385
386                 AltosGreatCircle from_receiver = null;
387
388                 if (saved_state != null)
389                         prev_state = saved_state.state;
390
391                 if (state != null) {
392                         set_screen_on(state_age(state));
393
394                         if (state.state == AltosLib.ao_flight_stateless) {
395                                 boolean prev_locked = false;
396                                 boolean locked = false;
397
398                                 if(state.gps != null)
399                                         locked = state.gps.locked;
400                                 if (saved_state != null && saved_state.gps != null)
401                                         prev_locked = saved_state.gps.locked;
402                                 if (prev_locked != locked) {
403                                         String currentTab = mTabHost.getCurrentTabTag();
404                                         if (locked) {
405                                                 if (currentTab.equals(tab_pad_name)) mTabHost.setCurrentTabByTag(tab_flight_name);
406                                         } else {
407                                                 if (currentTab.equals(tab_flight_name)) mTabHost.setCurrentTabByTag(tab_pad_name);
408                                         }
409                                 }
410                         } else {
411                                 if (prev_state != state.state) {
412                                         String currentTab = mTabHost.getCurrentTabTag();
413                                         switch (state.state) {
414                                         case AltosLib.ao_flight_boost:
415                                                 if (currentTab.equals(tab_pad_name)) mTabHost.setCurrentTabByTag(tab_flight_name);
416                                                 break;
417                                         case AltosLib.ao_flight_landed:
418                                                 if (currentTab.equals(tab_flight_name)) mTabHost.setCurrentTabByTag(tab_recover_name);
419                                                 break;
420                                         case AltosLib.ao_flight_stateless:
421                                                 if (currentTab.equals(tab_pad_name)) mTabHost.setCurrentTabByTag(tab_flight_name);
422                                                 break;
423                                         }
424                                 }
425                         }
426
427                         if (location != null && state.gps != null && state.gps.locked) {
428                                 double altitude = 0;
429                                 if (location.hasAltitude())
430                                         altitude = location.getAltitude();
431                                 from_receiver = new AltosGreatCircle(location.getLatitude(),
432                                                                      location.getLongitude(),
433                                                                      altitude,
434                                                                      state.gps.lat,
435                                                                      state.gps.lon,
436                                                                      state.gps.alt);
437                         }
438
439                         if (saved_state == null || !same_string(saved_state.callsign, state.callsign)) {
440                                 mCallsignView.setText(state.callsign);
441                         }
442                         if (saved_state == null || state.serial != saved_state.serial) {
443                                 mSerialView.setText(String.format("%d", state.serial));
444                         }
445                         if (saved_state == null || state.flight != saved_state.flight) {
446                                 if (state.flight == AltosLib.MISSING)
447                                         mFlightView.setText("");
448                                 else
449                                         mFlightView.setText(String.format("%d", state.flight));
450                         }
451                         if (saved_state == null || state.state != saved_state.state) {
452                                 if (state.state == AltosLib.ao_flight_stateless) {
453                                         mStateLayout.setVisibility(View.GONE);
454                                 } else {
455                                         mStateView.setText(state.state_name());
456                                         mStateLayout.setVisibility(View.VISIBLE);
457                                 }
458                         }
459                         if (saved_state == null || state.rssi != saved_state.rssi) {
460                                 mRSSIView.setText(String.format("%d", state.rssi));
461                         }
462                 }
463
464                 for (AltosDroidTab mTab : mTabs)
465                         mTab.update_ui(telem_state, state, from_receiver, location, mTab == mTabsAdapter.currentItem());
466
467                 if (mAltosVoice != null)
468                         mAltosVoice.tell(telem_state, state, from_receiver, location, (AltosDroidTab) mTabsAdapter.currentItem());
469
470                 saved_state = state;
471         }
472
473         private void onTimerTick() {
474                 try {
475                         mMessenger.send(Message.obtain(null, MSG_UPDATE_AGE));
476                 } catch (RemoteException e) {
477                 }
478         }
479
480         static String pos(double p, String pos, String neg) {
481                 String  h = pos;
482                 if (p == AltosLib.MISSING)
483                         return "";
484                 if (p < 0) {
485                         h = neg;
486                         p = -p;
487                 }
488                 int deg = (int) Math.floor(p);
489                 double min = (p - Math.floor(p)) * 60.0;
490                 return String.format("%d°%9.4f\" %s", deg, min, h);
491         }
492
493         static String number(String format, double value) {
494                 if (value == AltosLib.MISSING)
495                         return "";
496                 return String.format(format, value);
497         }
498
499         static String integer(String format, int value) {
500                 if (value == AltosLib.MISSING)
501                         return "";
502                 return String.format(format, value);
503         }
504
505         private View create_tab_view(String label) {
506                 LayoutInflater inflater = (LayoutInflater) this.getLayoutInflater();
507                 View tab_view = inflater.inflate(R.layout.tab_layout, null);
508                 TextView text_view = (TextView) tab_view.findViewById (R.id.tabLabel);
509                 text_view.setText(label);
510                 return tab_view;
511         }
512
513         public void set_map_source(int source) {
514                 for (AltosDroidTab mTab : mTabs)
515                         mTab.set_map_source(source);
516         }
517
518         @Override
519         public void onCreate(Bundle savedInstanceState) {
520                 super.onCreate(savedInstanceState);
521                 AltosDebug.init(this);
522                 AltosDebug.debug("+++ ON CREATE +++");
523
524                 // Initialise preferences
525                 AltosDroidPreferences.init(this);
526
527                 fm = getSupportFragmentManager();
528
529                 // Set up the window layout
530                 setContentView(R.layout.altosdroid);
531
532                 // Create the Tabs and ViewPager
533                 mTabHost = (TabHost)findViewById(android.R.id.tabhost);
534                 mTabHost.setup();
535
536                 mViewPager = (AltosViewPager)findViewById(R.id.pager);
537                 mViewPager.setOffscreenPageLimit(4);
538
539                 mTabsAdapter = new TabsAdapter(this, mTabHost, mViewPager);
540
541                 mTabsAdapter.addTab(mTabHost.newTabSpec(tab_pad_name).setIndicator(create_tab_view("Pad")), TabPad.class, null);
542                 mTabsAdapter.addTab(mTabHost.newTabSpec(tab_flight_name).setIndicator(create_tab_view("Flight")), TabFlight.class, null);
543                 mTabsAdapter.addTab(mTabHost.newTabSpec(tab_recover_name).setIndicator(create_tab_view("Recover")), TabRecover.class, null);
544                 mTabsAdapter.addTab(mTabHost.newTabSpec(tab_map_name).setIndicator(create_tab_view("Map")), TabMap.class, null);
545
546                 // Display the Version
547                 mVersion = (TextView) findViewById(R.id.version);
548                 mVersion.setText("Version: " + BuildInfo.version +
549                                  "  Built: " + BuildInfo.builddate + " " + BuildInfo.buildtime + " " + BuildInfo.buildtz +
550                                  "  (" + BuildInfo.branch + "-" + BuildInfo.commitnum + "-" + BuildInfo.commithash + ")");
551
552                 mCallsignView  = (TextView) findViewById(R.id.callsign_value);
553                 mRSSIView      = (TextView) findViewById(R.id.rssi_value);
554                 mSerialView    = (TextView) findViewById(R.id.serial_value);
555                 mFlightView    = (TextView) findViewById(R.id.flight_value);
556                 mStateLayout   = (RelativeLayout) findViewById(R.id.state_container);
557                 mStateView     = (TextView) findViewById(R.id.state_value);
558                 mAgeView       = (TextView) findViewById(R.id.age_value);
559                 mAgeNewColor   = mAgeView.getTextColors().getDefaultColor();
560                 mAgeOldColor   = getResources().getColor(R.color.old_color);
561         }
562
563         private boolean ensureBluetooth() {
564                 // Get local Bluetooth adapter
565                 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
566
567                 // If the adapter is null, then Bluetooth is not supported
568                 if (mBluetoothAdapter == null) {
569                         Toast.makeText(this, "Bluetooth is not available", Toast.LENGTH_LONG).show();
570                         return false;
571                 }
572
573                 if (!mBluetoothAdapter.isEnabled()) {
574                         Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
575                         startActivityForResult(enableIntent, AltosDroid.REQUEST_ENABLE_BT);
576                 }
577
578                 return true;
579         }
580
581         private boolean check_usb() {
582                 UsbDevice       device = AltosUsb.find_device(this, AltosLib.product_basestation);
583
584                 if (device != null) {
585                         Intent          i = new Intent(this, AltosDroid.class);
586                         PendingIntent   pi = PendingIntent.getActivity(this, 0, new Intent("hello world", null, this, AltosDroid.class), 0);
587
588                         if (AltosUsb.request_permission(this, device, pi)) {
589                                 connectUsb(device);
590                         }
591                         start_with_usb = true;
592                         return true;
593                 }
594
595                 start_with_usb = false;
596
597                 return false;
598         }
599
600         private void noticeIntent(Intent intent) {
601
602                 /* Ok, this is pretty convenient.
603                  *
604                  * When a USB device is plugged in, and our 'hotplug'
605                  * intent registration fires, we get an Intent with
606                  * EXTRA_DEVICE set.
607                  *
608                  * When we start up and see a usb device and request
609                  * permission to access it, that queues a
610                  * PendingIntent, which has the EXTRA_DEVICE added in,
611                  * along with the EXTRA_PERMISSION_GRANTED field as
612                  * well.
613                  *
614                  * So, in both cases, we get the device name using the
615                  * same call. We check to see if access was granted,
616                  * in which case we ignore the device field and do our
617                  * usual startup thing.
618                  */
619
620                 UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
621                 boolean granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, true);
622
623                 AltosDebug.debug("intent %s device %s granted %s", intent, device, granted);
624
625                 if (!granted)
626                         device = null;
627
628                 if (device != null) {
629                         AltosDebug.debug("intent has usb device " + device.toString());
630                         connectUsb(device);
631                 } else {
632
633                         /* 'granted' is only false if this intent came
634                          * from the request_permission call and
635                          * permission was denied. In which case, we
636                          * don't want to loop forever...
637                          */
638                         if (granted) {
639                                 AltosDebug.debug("check for a USB device at startup");
640                                 if (check_usb())
641                                         return;
642                         }
643                         AltosDebug.debug("Starting by looking for bluetooth devices");
644                         if (ensureBluetooth())
645                                 return;
646                         finish();
647                 }
648         }
649
650         @Override
651         public void onStart() {
652                 super.onStart();
653                 AltosDebug.debug("++ ON START ++");
654
655                 set_switch_time();
656
657                 noticeIntent(getIntent());
658
659                 // Start Telemetry Service
660                 String  action = start_with_usb ? ACTION_USB : ACTION_BLUETOOTH;
661
662                 startService(new Intent(action, null, AltosDroid.this, TelemetryService.class));
663
664                 doBindService();
665
666                 if (mAltosVoice == null)
667                         mAltosVoice = new AltosVoice(this);
668
669         }
670
671         @Override
672         public void onNewIntent(Intent intent) {
673                 super.onNewIntent(intent);
674                 AltosDebug.debug("onNewIntent");
675                 noticeIntent(intent);
676         }
677
678         @Override
679         public void onResume() {
680                 super.onResume();
681                 AltosDebug.debug("+ ON RESUME +");
682         }
683
684         @Override
685         public void onPause() {
686                 super.onPause();
687                 AltosDebug.debug("- ON PAUSE -");
688         }
689
690         @Override
691         public void onStop() {
692                 super.onStop();
693                 AltosDebug.debug("-- ON STOP --");
694         }
695
696         @Override
697         public void onDestroy() {
698                 super.onDestroy();
699                 AltosDebug.debug("--- ON DESTROY ---");
700
701                 doUnbindService();
702                 if (mAltosVoice != null) {
703                         mAltosVoice.stop();
704                         mAltosVoice = null;
705                 }
706                 stop_timer();
707         }
708
709         protected void onActivityResult(int requestCode, int resultCode, Intent data) {
710                 AltosDebug.debug("onActivityResult " + resultCode);
711                 switch (requestCode) {
712                 case REQUEST_CONNECT_DEVICE:
713                         // When DeviceListActivity returns with a device to connect to
714                         if (resultCode == Activity.RESULT_OK) {
715                                 connectDevice(data);
716                         }
717                         break;
718                 case REQUEST_ENABLE_BT:
719                         // When the request to enable Bluetooth returns
720                         if (resultCode == Activity.RESULT_OK) {
721                                 // Bluetooth is now enabled, so set up a chat session
722                                 //setupChat();
723                         } else {
724                                 // User did not enable Bluetooth or an error occured
725                                 AltosDebug.error("BT not enabled");
726                                 stopService(new Intent(AltosDroid.this, TelemetryService.class));
727                                 Toast.makeText(this, R.string.bt_not_enabled, Toast.LENGTH_SHORT).show();
728                                 finish();
729                         }
730                         break;
731                 case REQUEST_MAP_TYPE:
732                         if (resultCode == Activity.RESULT_OK)
733                                 set_map_type(data);
734                         break;
735                 }
736         }
737
738         private void connectUsb(UsbDevice device) {
739                 if (mService == null)
740                         pending_usb_device = device;
741                 else {
742                         // Attempt to connect to the device
743                         try {
744                                 mService.send(Message.obtain(null, TelemetryService.MSG_OPEN_USB, device));
745                                 AltosDebug.debug("Sent OPEN_USB message");
746                         } catch (RemoteException e) {
747                                 AltosDebug.debug("connect device message failed");
748                         }
749                 }
750         }
751
752         private void connectDevice(Intent data) {
753                 // Attempt to connect to the device
754                 try {
755                         String address = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
756                         String name = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_NAME);
757
758                         AltosDebug.debug("Connecting to " + address + " " + name);
759                         DeviceAddress   a = new DeviceAddress(address, name);
760                         mService.send(Message.obtain(null, TelemetryService.MSG_CONNECT, a));
761                         AltosDebug.debug("Sent connecting message");
762                 } catch (RemoteException e) {
763                         AltosDebug.debug("connect device message failed");
764                 }
765         }
766
767         private void disconnectDevice() {
768                 try {
769                         mService.send(Message.obtain(null, TelemetryService.MSG_DISCONNECT, null));
770                 } catch (RemoteException e) {
771                 }
772         }
773
774         private void set_map_type(Intent data) {
775                 int type = data.getIntExtra(MapTypeActivity.EXTRA_MAP_TYPE, -1);
776
777                 AltosDebug.debug("intent set_map_type %d\n", type);
778                 if (type != -1) {
779                         map_type = type;
780                         for (AltosDroidTab mTab : mTabs)
781                                 mTab.set_map_type(map_type);
782                 }
783         }
784
785         @Override
786         public boolean onCreateOptionsMenu(Menu menu) {
787                 MenuInflater inflater = getMenuInflater();
788                 inflater.inflate(R.menu.option_menu, menu);
789                 return true;
790         }
791
792         void setFrequency(double freq) {
793                 try {
794                         mService.send(Message.obtain(null, TelemetryService.MSG_SETFREQUENCY, freq));
795                         set_switch_time();
796                 } catch (RemoteException e) {
797                 }
798         }
799
800         void setFrequency(String freq) {
801                 try {
802                         setFrequency (AltosParse.parse_double_net(freq.substring(11, 17)));
803                 } catch (ParseException e) {
804                 }
805         }
806
807         void setBaud(int baud) {
808                 try {
809                         mService.send(Message.obtain(null, TelemetryService.MSG_SETBAUD, baud));
810                         set_switch_time();
811                 } catch (RemoteException e) {
812                 }
813         }
814
815         void setBaud(String baud) {
816                 try {
817                         int     value = Integer.parseInt(baud);
818                         int     rate = AltosLib.ao_telemetry_rate_38400;
819                         switch (value) {
820                         case 2400:
821                                 rate = AltosLib.ao_telemetry_rate_2400;
822                                 break;
823                         case 9600:
824                                 rate = AltosLib.ao_telemetry_rate_9600;
825                                 break;
826                         case 38400:
827                                 rate = AltosLib.ao_telemetry_rate_38400;
828                                 break;
829                         }
830                         setBaud(rate);
831                 } catch (NumberFormatException e) {
832                 }
833         }
834
835         void select_tracker(int serial) {
836                 int i;
837
838                 AltosDebug.debug("select tracker %d\n", serial);
839
840                 if (serial == selected_serial) {
841                         AltosDebug.debug("%d already selected\n", serial);
842                         return;
843                 }
844
845                 if (serial != 0) {
846                         for (i = 0; i < serials.length; i++)
847                                 if (serials[i] == serial)
848                                         break;
849
850                         if (i == serials.length) {
851                                 AltosDebug.debug("attempt to select unknown tracker %d\n", serial);
852                                 return;
853                         }
854                 }
855
856                 current_serial = selected_serial = serial;
857                 update_state(null);
858         }
859
860         void touch_trackers(Integer[] serials) {
861                 AlertDialog.Builder builder_tracker = new AlertDialog.Builder(this);
862                 builder_tracker.setTitle("Select Tracker");
863                 final String[] trackers = new String[serials.length + 1];
864                 trackers[0] = "Auto";
865                 for (int i = 0; i < serials.length; i++)
866                         trackers[i+1] = String.format("%d", serials[i]);
867                 builder_tracker.setItems(trackers,
868                                          new DialogInterface.OnClickListener() {
869                                                  public void onClick(DialogInterface dialog, int item) {
870                                                          if (item == 0)
871                                                                  select_tracker(0);
872                                                          else
873                                                                  select_tracker(Integer.parseInt(trackers[item]));
874                                                  }
875                                          });
876                 AlertDialog alert_tracker = builder_tracker.create();
877                 alert_tracker.show();
878         }
879
880         void delete_track(int serial) {
881                 try {
882                         mService.send(Message.obtain(null, TelemetryService.MSG_DELETE_SERIAL, (Integer) serial));
883                 } catch (Exception ex) {
884                 }
885         }
886
887         @Override
888         public boolean onOptionsItemSelected(MenuItem item) {
889                 Intent serverIntent = null;
890                 switch (item.getItemId()) {
891                 case R.id.connect_scan:
892                         if (ensureBluetooth()) {
893                                 // Launch the DeviceListActivity to see devices and do scan
894                                 serverIntent = new Intent(this, DeviceListActivity.class);
895                                 startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);
896                         }
897                         return true;
898                 case R.id.disconnect:
899                         /* Disconnect the device
900                          */
901                         disconnectDevice();
902                         return true;
903                 case R.id.quit:
904                         AltosDebug.debug("R.id.quit");
905                         disconnectDevice();
906                         finish();
907                         return true;
908                 case R.id.select_freq:
909                         // Set the TBT radio frequency
910
911                         final String[] frequencies = {
912                                 "Channel 0 (434.550MHz)",
913                                 "Channel 1 (434.650MHz)",
914                                 "Channel 2 (434.750MHz)",
915                                 "Channel 3 (434.850MHz)",
916                                 "Channel 4 (434.950MHz)",
917                                 "Channel 5 (435.050MHz)",
918                                 "Channel 6 (435.150MHz)",
919                                 "Channel 7 (435.250MHz)",
920                                 "Channel 8 (435.350MHz)",
921                                 "Channel 9 (435.450MHz)"
922                         };
923
924                         AlertDialog.Builder builder_freq = new AlertDialog.Builder(this);
925                         builder_freq.setTitle("Pick a frequency");
926                         builder_freq.setItems(frequencies,
927                                          new DialogInterface.OnClickListener() {
928                                                  public void onClick(DialogInterface dialog, int item) {
929                                                          setFrequency(frequencies[item]);
930                                                  }
931                                          });
932                         AlertDialog alert_freq = builder_freq.create();
933                         alert_freq.show();
934                         return true;
935                 case R.id.select_rate:
936                         // Set the TBT baud rate
937
938                         final String[] rates = {
939                                 "38400",
940                                 "9600",
941                                 "2400",
942                         };
943
944                         AlertDialog.Builder builder_rate = new AlertDialog.Builder(this);
945                         builder_rate.setTitle("Pick a baud rate");
946                         builder_rate.setItems(rates,
947                                          new DialogInterface.OnClickListener() {
948                                                  public void onClick(DialogInterface dialog, int item) {
949                                                          setBaud(rates[item]);
950                                                  }
951                                          });
952                         AlertDialog alert_rate = builder_rate.create();
953                         alert_rate.show();
954                         return true;
955                 case R.id.change_units:
956                         boolean imperial = AltosPreferences.imperial_units();
957                         AltosPreferences.set_imperial_units(!imperial);
958                         return true;
959                 case R.id.preload_maps:
960                         serverIntent = new Intent(this, PreloadMapActivity.class);
961                         startActivityForResult(serverIntent, REQUEST_PRELOAD_MAPS);
962                         return true;
963                 case R.id.map_type:
964                         serverIntent = new Intent(this, MapTypeActivity.class);
965                         startActivityForResult(serverIntent, REQUEST_MAP_TYPE);
966                         return true;
967                 case R.id.map_source:
968                         int source = AltosDroidPreferences.map_source();
969                         int new_source = source == AltosDroidPreferences.MAP_SOURCE_ONLINE ? AltosDroidPreferences.MAP_SOURCE_OFFLINE : AltosDroidPreferences.MAP_SOURCE_ONLINE;
970                         AltosDroidPreferences.set_map_source(new_source);
971                         set_map_source(new_source);
972                         return true;
973                 case R.id.select_tracker:
974                         if (serials != null) {
975                                 String[] trackers = new String[serials.length+1];
976                                 trackers[0] = "Auto";
977                                 for (int i = 0; i < serials.length; i++)
978                                         trackers[i+1] = String.format("%d", serials[i]);
979                                 AlertDialog.Builder builder_serial = new AlertDialog.Builder(this);
980                                 builder_serial.setTitle("Select a tracker");
981                                 builder_serial.setItems(trackers,
982                                                         new DialogInterface.OnClickListener() {
983                                                                 public void onClick(DialogInterface dialog, int item) {
984                                                                         if (item == 0)
985                                                                                 select_tracker(0);
986                                                                         else
987                                                                                 select_tracker(serials[item-1]);
988                                                                 }
989                                                         });
990                                 AlertDialog alert_serial = builder_serial.create();
991                                 alert_serial.show();
992
993                         }
994                         return true;
995                 case R.id.delete_track:
996                         if (serials != null) {
997                                 String[] trackers = new String[serials.length];
998                                 for (int i = 0; i < serials.length; i++)
999                                         trackers[i] = String.format("%d", serials[i]);
1000                                 AlertDialog.Builder builder_serial = new AlertDialog.Builder(this);
1001                                 builder_serial.setTitle("Delete a track");
1002                                 builder_serial.setItems(trackers,
1003                                                         new DialogInterface.OnClickListener() {
1004                                                                 public void onClick(DialogInterface dialog, int item) {
1005                                                                         delete_track(serials[item]);
1006                                                                 }
1007                                                         });
1008                                 AlertDialog alert_serial = builder_serial.create();
1009                                 alert_serial.show();
1010
1011                         }
1012                         return true;
1013                 }
1014                 return false;
1015         }
1016
1017         static String direction(AltosGreatCircle from_receiver,
1018                              Location receiver) {
1019                 if (from_receiver == null)
1020                         return null;
1021
1022                 if (receiver == null)
1023                         return null;
1024
1025                 if (!receiver.hasBearing())
1026                         return null;
1027
1028                 float   bearing = receiver.getBearing();
1029                 float   heading = (float) from_receiver.bearing - bearing;
1030
1031                 while (heading <= -180.0f)
1032                         heading += 360.0f;
1033                 while (heading > 180.0f)
1034                         heading -= 360.0f;
1035
1036                 int iheading = (int) (heading + 0.5f);
1037
1038                 if (-1 < iheading && iheading < 1)
1039                         return "ahead";
1040                 else if (iheading < -179 || 179 < iheading)
1041                         return "backwards";
1042                 else if (iheading < 0)
1043                         return String.format("left %d°", -iheading);
1044                 else
1045                         return String.format("right %d°", iheading);
1046         }
1047 }