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