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