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