5caee5f87aab5323ea0032cf54f865649ff22a8b
[fw/altos] / altosdroid / app / src / main / java / 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; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program; if not, write to the Free Software Foundation, Inc.,
16  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
17  */
18
19 package org.altusmetrum.AltosDroid;
20
21 import java.lang.ref.WeakReference;
22 import java.util.*;
23
24 import android.Manifest;
25 import android.app.Activity;
26 import android.app.PendingIntent;
27 import android.bluetooth.BluetoothAdapter;
28 import android.content.Intent;
29 import android.content.Context;
30 import android.content.ComponentName;
31 import android.content.ServiceConnection;
32 import android.content.DialogInterface;
33 import android.os.IBinder;
34 import android.os.Bundle;
35 import android.os.Handler;
36 import android.os.Message;
37 import android.os.Messenger;
38 import android.os.RemoteException;
39 import android.os.Parcelable;
40 import androidx.fragment.app.FragmentActivity;
41 import androidx.fragment.app.FragmentManager;
42 import android.view.*;
43 import android.widget.*;
44 import android.app.AlertDialog;
45 import android.location.Location;
46 import android.location.LocationManager;
47 import android.location.LocationListener;
48 import android.hardware.usb.*;
49 import android.content.pm.PackageManager;
50 import androidx.core.app.ActivityCompat;
51 import org.altusmetrum.altoslib_14.*;
52
53 class SavedState {
54         long    received_time;
55         int     state;
56         boolean locked;
57         String  callsign;
58         int     serial;
59         int     flight;
60         int     rssi;
61
62         SavedState(AltosState state) {
63                 received_time = state.received_time;
64                 this.state = state.state();
65                 if (state.gps != null)
66                         locked = state.gps.locked;
67                 else
68                         locked = false;
69                 callsign = state.cal_data().callsign;
70                 serial = state.cal_data().serial;
71                 flight = state.cal_data().flight;
72                 rssi = state.rssi;
73         }
74 }
75
76 public class AltosDroid extends FragmentActivity implements AltosUnitsListener, LocationListener, ActivityCompat.OnRequestPermissionsResultCallback {
77
78         // Actions sent to the telemetry server at startup time
79
80         public static final String ACTION_BLUETOOTH = "org.altusmetrum.AltosDroid.BLUETOOTH";
81         public static final String ACTION_USB = "org.altusmetrum.AltosDroid.USB";
82
83         // Message types received by our Handler
84
85         public static final int MSG_STATE           = 1;
86         public static final int MSG_UPDATE_AGE      = 2;
87         public static final int MSG_IDLE_MODE       = 3;
88         public static final int MSG_IGNITER_STATUS  = 4;
89
90         // Intent request codes
91         public static final int REQUEST_CONNECT_DEVICE = 1;
92         public static final int REQUEST_ENABLE_BT      = 2;
93         public static final int REQUEST_PRELOAD_MAPS   = 3;
94         public static final int REQUEST_IDLE_MODE      = 5;
95         public static final int REQUEST_IGNITERS       = 6;
96         public static final int REQUEST_SETUP          = 7;
97         public static final int REQUEST_SELECT_TRACKER = 8;
98         public static final int REQUEST_DELETE_TRACKER = 9;
99
100         public static final String EXTRA_IDLE_MODE = "idle_mode";
101         public static final String EXTRA_IDLE_RESULT = "idle_result";
102         public static final String EXTRA_FREQUENCY = "frequency";
103         public static final String EXTRA_TELEMETRY_SERVICE = "telemetry_service";
104         public static final String EXTRA_TRACKERS = "trackers";
105         public static final String EXTRA_TRACKERS_TITLE = "trackers_title";
106
107         // Setup result bits
108         public static final int SETUP_BAUD = 1;
109         public static final int SETUP_UNITS = 2;
110         public static final int SETUP_MAP_SOURCE = 4;
111         public static final int SETUP_MAP_TYPE = 8;
112         public static final int SETUP_FONT_SIZE = 16;
113
114         public static FragmentManager   fm;
115
116         private BluetoothAdapter mBluetoothAdapter = null;
117
118         // Flight state values
119         private TextView mCallsignView;
120         private TextView mRSSIView;
121         private TextView mSerialView;
122         private TextView mFlightView;
123         private RelativeLayout mStateLayout;
124         private TextView mStateView;
125         private TextView mAgeView;
126         private boolean  mAgeViewOld;
127         private int mAgeNewColor;
128         private int mAgeOldColor;
129
130         public static final String      tab_pad_name = "pad";
131         public static final String      tab_flight_name = "flight";
132         public static final String      tab_recover_name = "recover";
133         public static final String      tab_map_name = "map";
134
135         // field to display the version at the bottom of the screen
136         private TextView mVersion;
137
138         private boolean idle_mode = false;
139
140         public Location location = null;
141
142         private AltosState      state;
143         private SavedState      saved_state;
144
145         // Tabs
146         TabHost         mTabHost;
147         AltosViewPager  mViewPager;
148         TabsAdapter     mTabsAdapter;
149         ArrayList<AltosDroidTab> mTabs = new ArrayList<AltosDroidTab>();
150         int             tabHeight;
151
152         // Timer and Saved flight state for Age calculation
153         private Timer timer;
154
155         TelemetryState  telemetry_state;
156         Tracker[]       trackers;
157
158         UsbDevice       pending_usb_device;
159         boolean         start_with_usb;
160
161         // Service
162         private boolean mIsBound   = false;
163         private Messenger mService = null;
164         final Messenger mMessenger = new Messenger(new IncomingHandler(this));
165
166         // Text to Speech
167         private AltosVoice mAltosVoice = null;
168
169         // The Handler that gets information back from the Telemetry Service
170         static class IncomingHandler extends Handler {
171                 private final WeakReference<AltosDroid> mAltosDroid;
172                 IncomingHandler(AltosDroid ad) { mAltosDroid = new WeakReference<AltosDroid>(ad); }
173
174                 @Override
175                 public void handleMessage(Message msg) {
176                         AltosDroid ad = mAltosDroid.get();
177
178                         switch (msg.what) {
179                         case MSG_STATE:
180                                 if (msg.obj == null) {
181                                         AltosDebug.debug("telemetry_state null!");
182                                         return;
183                                 }
184                                 ad.update_state((TelemetryState) msg.obj);
185                                 break;
186                         case MSG_UPDATE_AGE:
187                                 ad.update_age();
188                                 break;
189                         case MSG_IDLE_MODE:
190                                 ad.idle_mode = (Boolean) msg.obj;
191                                 ad.update_state(null);
192                                 break;
193                         }
194                 }
195         };
196
197
198         private ServiceConnection mConnection = new ServiceConnection() {
199                 public void onServiceConnected(ComponentName className, IBinder service) {
200                         mService = new Messenger(service);
201                         try {
202                                 Message msg = Message.obtain(null, TelemetryService.MSG_REGISTER_CLIENT);
203                                 msg.replyTo = mMessenger;
204                                 mService.send(msg);
205                         } catch (RemoteException e) {
206                                 // In this case the service has crashed before we could even do anything with it
207                         }
208                         if (pending_usb_device != null) {
209                                 try {
210                                         mService.send(Message.obtain(null, TelemetryService.MSG_OPEN_USB, pending_usb_device));
211                                         pending_usb_device = null;
212                                 } catch (RemoteException e) {
213                                 }
214                         }
215                 }
216
217                 public void onServiceDisconnected(ComponentName className) {
218                         // This is called when the connection with the service has been unexpectedly disconnected - process crashed.
219                         mService = null;
220                 }
221         };
222
223         void doBindService() {
224                 bindService(new Intent(this, TelemetryService.class), mConnection, Context.BIND_AUTO_CREATE);
225                 mIsBound = true;
226         }
227
228         void doUnbindService() {
229                 if (mIsBound) {
230                         // If we have received the service, and hence registered with it, then now is the time to unregister.
231                         if (mService != null) {
232                                 try {
233                                         Message msg = Message.obtain(null, TelemetryService.MSG_UNREGISTER_CLIENT);
234                                         msg.replyTo = mMessenger;
235                                         mService.send(msg);
236                                 } catch (RemoteException e) {
237                                         // There is nothing special we need to do if the service has crashed.
238                                 }
239                         }
240                         // Detach our existing connection.
241                         unbindService(mConnection);
242                         mIsBound = false;
243                 }
244         }
245
246         public void registerTab(AltosDroidTab mTab) {
247                 mTabs.add(mTab);
248         }
249
250         public void unregisterTab(AltosDroidTab mTab) {
251                 mTabs.remove(mTab);
252         }
253
254         public void units_changed(boolean imperial_units) {
255                 for (AltosDroidTab mTab : mTabs)
256                         mTab.units_changed(imperial_units);
257         }
258
259         void update_title(TelemetryState telemetry_state) {
260                 switch (telemetry_state.connect) {
261                 case TelemetryState.CONNECT_CONNECTED:
262                         if (telemetry_state.config != null) {
263                                 String str = String.format("S/N %d %6.3f MHz%s", telemetry_state.config.serial,
264                                                            telemetry_state.frequency, telemetry_state.idle_mode ? " (idle)" : "");
265                                 if (telemetry_state.telemetry_rate != AltosLib.ao_telemetry_rate_38400)
266                                         str = str.concat(String.format(" %d bps",
267                                                                        AltosLib.ao_telemetry_rate_values[telemetry_state.telemetry_rate]));
268                                 setTitle(str);
269                         } else {
270                                 setTitle(R.string.title_connected_to);
271                         }
272                         break;
273                 case TelemetryState.CONNECT_CONNECTING:
274                         if (telemetry_state.address != null)
275                                 setTitle(String.format("Connecting to %s...", telemetry_state.address.name));
276                         else
277                                 setTitle("Connecting to something...");
278                         break;
279                 case TelemetryState.CONNECT_DISCONNECTED:
280                 case TelemetryState.CONNECT_NONE:
281                         setTitle(R.string.title_not_connected);
282                         break;
283                 }
284         }
285
286         void start_timer() {
287                 if (timer == null) {
288                         timer = new Timer();
289                         timer.scheduleAtFixedRate(new TimerTask(){ public void run() {onTimerTick();}}, 1000L, 1000L);
290                 }
291         }
292
293         void stop_timer() {
294                 if (timer != null) {
295                         timer.cancel();
296                         timer.purge();
297                         timer = null;
298                 }
299         }
300
301         int     selected_serial = 0;
302         long    switch_time;
303
304         void set_switch_time() {
305                 switch_time = System.currentTimeMillis();
306                 selected_serial = 0;
307         }
308
309         boolean registered_units_listener;
310
311         void update_state(TelemetryState new_telemetry_state) {
312
313                 if (new_telemetry_state != null)
314                         telemetry_state = new_telemetry_state;
315
316                 if (selected_frequency != AltosLib.MISSING) {
317                         AltosState selected_state = telemetry_state.get(selected_serial);
318                         AltosState latest_state = telemetry_state.get(telemetry_state.latest_serial);
319
320                         if (selected_state != null && selected_state.frequency == selected_frequency) {
321                                 selected_frequency = AltosLib.MISSING;
322                         } else if ((selected_state == null || selected_state.frequency != selected_frequency) &&
323                                    (latest_state != null && latest_state.frequency == selected_frequency))
324                         {
325                                 selected_frequency = AltosLib.MISSING;
326                                 selected_serial = telemetry_state.latest_serial;
327                         }
328                 }
329
330                 if (!telemetry_state.containsKey(selected_serial)) {
331                         selected_serial = telemetry_state.latest_serial;
332                         AltosDebug.debug("selected serial set to %d", selected_serial);
333                 }
334
335                 int shown_serial = selected_serial;
336
337                 if (telemetry_state.idle_mode)
338                         shown_serial = telemetry_state.latest_serial;
339
340                 if (!registered_units_listener) {
341                         registered_units_listener = true;
342                         AltosPreferences.register_units_listener(this);
343                 }
344
345                 int     num_trackers = 0;
346
347                 for (AltosState s : telemetry_state.values()) {
348                         num_trackers++;
349                 }
350
351                 trackers = new Tracker[num_trackers + 1];
352
353                 int n = 0;
354                 trackers[n++] = new Tracker(0, "auto", 0.0);
355
356                 for (AltosState s : telemetry_state.values())
357                         trackers[n++] = new Tracker(s);
358
359                 Arrays.sort(trackers);
360
361                 if (telemetry_state.frequency != AltosLib.MISSING)
362                         telem_frequency = telemetry_state.frequency;
363
364                 update_title(telemetry_state);
365
366                 AltosState      state = telemetry_state.get(shown_serial);
367
368                 update_ui(telemetry_state, state, telemetry_state.quiet);
369
370                 start_timer();
371         }
372
373         boolean same_string(String a, String b) {
374                 if (a == null) {
375                         if (b == null)
376                                 return true;
377                         return false;
378                 } else {
379                         if (b == null)
380                                 return false;
381                         return a.equals(b);
382                 }
383         }
384
385
386         private int blend_component(int a, int b, double r, int shift, int mask) {
387                 return ((int) (((a >> shift) & mask) * r + ((b >> shift) & mask) * (1 - r)) & mask) << shift;
388         }
389         private int blend_color(int a, int b, double r) {
390                 return (blend_component(a, b, r, 0, 0xff) |
391                         blend_component(a, b, r, 8, 0xff) |
392                         blend_component(a, b, r, 16, 0xff) |
393                         blend_component(a, b, r, 24, 0xff));
394         }
395
396         int state_age(long received_time) {
397                 return (int) ((System.currentTimeMillis() - received_time + 500) / 1000);
398         }
399
400         void set_screen_on(int age) {
401                 if (age < 60)
402                         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
403                 else
404                         getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
405         }
406
407         static String age_string(int age) {
408                 String  text;
409                 if (age < 60)
410                         text = String.format("%ds", age);
411                 else if (age < 60 * 60)
412                         text = String.format("%dm", age / 60);
413                 else if (age < 60 * 60 * 24)
414                         text = String.format("%dh", age / (60 * 60));
415                 else
416                         text = String.format("%dd", age / (24 * 60 * 60));
417                 return text;
418         }
419
420         void update_age() {
421                 if (saved_state != null) {
422                         int age = state_age(saved_state.received_time);
423
424                         double age_scale = age / 100.0;
425
426                         if (age_scale > 1.0)
427                                 age_scale = 1.0;
428
429                         mAgeView.setTextColor(blend_color(mAgeOldColor, mAgeNewColor, age_scale));
430
431                         set_screen_on(age);
432
433                         mAgeView.setText(age_string(age));
434                 }
435         }
436
437         void update_ui(TelemetryState telem_state, AltosState state, boolean quiet) {
438
439                 this.state = state;
440
441                 int prev_state = AltosLib.ao_flight_invalid;
442
443                 AltosGreatCircle from_receiver = null;
444
445                 if (saved_state != null)
446                         prev_state = saved_state.state;
447
448                 if (state != null) {
449                         set_screen_on(state_age(state.received_time));
450
451                         if (state.state() == AltosLib.ao_flight_stateless) {
452                                 boolean prev_locked = false;
453                                 boolean locked = false;
454
455                                 if(state.gps != null)
456                                         locked = state.gps.locked;
457                                 if (saved_state != null)
458                                         prev_locked = saved_state.locked;
459                                 if (prev_locked != locked) {
460                                         String currentTab = mTabHost.getCurrentTabTag();
461                                         if (locked) {
462                                                 if (currentTab.equals(tab_pad_name)) mTabHost.setCurrentTabByTag(tab_flight_name);
463                                         } else {
464                                                 if (currentTab.equals(tab_flight_name)) mTabHost.setCurrentTabByTag(tab_pad_name);
465                                         }
466                                 }
467                         } else {
468                                 if (prev_state != state.state()) {
469                                         String currentTab = mTabHost.getCurrentTabTag();
470                                         switch (state.state()) {
471                                         case AltosLib.ao_flight_boost:
472                                                 if (currentTab.equals(tab_pad_name)) mTabHost.setCurrentTabByTag(tab_flight_name);
473                                                 break;
474                                         case AltosLib.ao_flight_landed:
475                                                 if (currentTab.equals(tab_flight_name)) mTabHost.setCurrentTabByTag(tab_recover_name);
476                                                 break;
477                                         case AltosLib.ao_flight_stateless:
478                                                 if (currentTab.equals(tab_pad_name)) mTabHost.setCurrentTabByTag(tab_flight_name);
479                                                 break;
480                                         }
481                                 }
482                         }
483
484                         if (location != null && state.gps != null && state.gps.locked) {
485                                 double altitude = 0;
486                                 if (location.hasAltitude())
487                                         altitude = location.getAltitude();
488                                 from_receiver = new AltosGreatCircle(location.getLatitude(),
489                                                                      location.getLongitude(),
490                                                                      altitude,
491                                                                      state.gps.lat,
492                                                                      state.gps.lon,
493                                                                      state.gps.alt);
494                         }
495
496                         if (saved_state == null || !same_string(saved_state.callsign, state.cal_data().callsign)) {
497                                 mCallsignView.setText(state.cal_data().callsign);
498                         }
499                         if (saved_state == null || state.cal_data().serial != saved_state.serial) {
500                                 if (state.cal_data().serial == AltosLib.MISSING)
501                                         mSerialView.setText("");
502                                 else
503                                         mSerialView.setText(String.format("%d", state.cal_data().serial));
504                         }
505                         if (saved_state == null || state.cal_data().flight != saved_state.flight) {
506                                 if (state.cal_data().flight == AltosLib.MISSING)
507                                         mFlightView.setText("");
508                                 else
509                                         mFlightView.setText(String.format("%d", state.cal_data().flight));
510                         }
511                         if (saved_state == null || state.state() != saved_state.state) {
512                                 if (state.state() == AltosLib.ao_flight_stateless) {
513                                         mStateLayout.setVisibility(View.GONE);
514                                 } else {
515                                         mStateView.setText(state.state_name());
516                                         mStateLayout.setVisibility(View.VISIBLE);
517                                 }
518                         }
519                         if (saved_state == null || state.rssi != saved_state.rssi) {
520                                 if (state.rssi == AltosLib.MISSING)
521                                         mRSSIView.setText("");
522                                 else
523                                         mRSSIView.setText(String.format("%d", state.rssi));
524                         }
525                         saved_state = new SavedState(state);
526                 }
527
528                 for (AltosDroidTab mTab : mTabs)
529                         mTab.update_ui(telem_state, state, from_receiver, location, mTab == mTabsAdapter.currentItem());
530
531                 if (mAltosVoice != null && mTabsAdapter.currentItem() != null)
532                         mAltosVoice.tell(telem_state, state, from_receiver, location, (AltosDroidTab) mTabsAdapter.currentItem(), quiet);
533
534         }
535
536         private void onTimerTick() {
537                 try {
538                         mMessenger.send(Message.obtain(null, MSG_UPDATE_AGE));
539                 } catch (RemoteException e) {
540                 }
541         }
542
543         static String pos(double p, String pos, String neg) {
544                 String  h = pos;
545                 if (p == AltosLib.MISSING)
546                         return "";
547                 if (p < 0) {
548                         h = neg;
549                         p = -p;
550                 }
551                 int deg = (int) Math.floor(p);
552                 double min = (p - Math.floor(p)) * 60.0;
553                 return String.format("%d° %7.4f\" %s", deg, min, h);
554         }
555
556         static String number(String format, double value) {
557                 if (value == AltosLib.MISSING)
558                         return "";
559                 return String.format(format, value);
560         }
561
562         static String integer(String format, int value) {
563                 if (value == AltosLib.MISSING)
564                         return "";
565                 return String.format(format, value);
566         }
567
568         private View create_tab_view(String label) {
569                 LayoutInflater inflater = (LayoutInflater) this.getLayoutInflater();
570                 View tab_view = inflater.inflate(R.layout.tab_layout, null);
571                 TextView text_view = (TextView) tab_view.findViewById (R.id.tabLabel);
572                 text_view.setText(label);
573                 return tab_view;
574         }
575
576         static public int[] themes = {
577                 R.style.Small,
578                 R.style.Medium,
579                 R.style.Large,
580                 R.style.Extra
581         };
582
583         static public int[] dialog_themes = {
584                 R.style.Small_Dialog,
585                 R.style.Medium_Dialog,
586                 R.style.Large_Dialog,
587                 R.style.Extra_Dialog
588         };
589
590         @Override
591         public void onCreate(Bundle savedInstanceState) {
592                 // Initialise preferences
593                 AltosDroidPreferences.init(this);
594                 setTheme(themes[AltosDroidPreferences.font_size()]);
595                 super.onCreate(savedInstanceState);
596                 AltosDebug.init(this);
597                 AltosDebug.debug("+++ ON CREATE +++");
598
599
600                 fm = getSupportFragmentManager();
601
602                 // Set up the window layout
603                 setContentView(R.layout.altosdroid);
604
605                 // Create the Tabs and ViewPager
606                 mTabHost = (TabHost)findViewById(android.R.id.tabhost);
607                 mTabHost.setup();
608
609                 mViewPager = (AltosViewPager)findViewById(R.id.pager);
610                 mViewPager.setOffscreenPageLimit(4);
611
612                 mTabsAdapter = new TabsAdapter(this, mTabHost, mViewPager);
613
614                 mTabsAdapter.addTab(mTabHost.newTabSpec(tab_pad_name).setIndicator(create_tab_view("Pad")), TabPad.class, null);
615                 mTabsAdapter.addTab(mTabHost.newTabSpec(tab_flight_name).setIndicator(create_tab_view("Flight")), TabFlight.class, null);
616                 mTabsAdapter.addTab(mTabHost.newTabSpec(tab_recover_name).setIndicator(create_tab_view("Recover")), TabRecover.class, null);
617                 mTabsAdapter.addTab(mTabHost.newTabSpec(tab_map_name).setIndicator(create_tab_view("Map")), TabMap.class, null);
618
619                 // Display the Version
620                 mVersion = (TextView) findViewById(R.id.version);
621                 mVersion.setText("Version: " + BuildInfo.version +
622                                  " Built: " + BuildInfo.builddate + " " + BuildInfo.buildtime + " " + BuildInfo.buildtz +
623                                  " (" + BuildInfo.branch + "-" + BuildInfo.commitnum + "-" + BuildInfo.commithash + ")");
624
625                 mCallsignView  = (TextView) findViewById(R.id.callsign_value);
626                 mRSSIView      = (TextView) findViewById(R.id.rssi_value);
627                 mSerialView    = (TextView) findViewById(R.id.serial_value);
628                 mFlightView    = (TextView) findViewById(R.id.flight_value);
629                 mStateLayout   = (RelativeLayout) findViewById(R.id.state_container);
630                 mStateView     = (TextView) findViewById(R.id.state_value);
631                 mAgeView       = (TextView) findViewById(R.id.age_value);
632                 mAgeNewColor   = mAgeView.getTextColors().getDefaultColor();
633                 mAgeOldColor   = getResources().getColor(R.color.old_color);
634         }
635
636         private void ensureBluetooth() {
637                 // Get local Bluetooth adapter
638                 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
639
640                 /* if there is a BT adapter and it isn't turned on, then turn it on */
641                 if (mBluetoothAdapter != null && !mBluetoothAdapter.isEnabled()) {
642                         Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
643                         startActivityForResult(enableIntent, AltosDroid.REQUEST_ENABLE_BT);
644                 }
645         }
646
647         private boolean check_usb() {
648                 UsbDevice       device = AltosUsb.find_device(this, AltosLib.product_basestation);
649
650                 if (device != null) {
651                         Intent          i = new Intent(this, AltosDroid.class);
652                         PendingIntent   pi = PendingIntent.getActivity(this, 0, new Intent("hello world", null, this, AltosDroid.class), 0);
653
654                         if (AltosUsb.request_permission(this, device, pi)) {
655                                 connectUsb(device);
656                         }
657                         start_with_usb = true;
658                         return true;
659                 }
660
661                 start_with_usb = false;
662
663                 return false;
664         }
665
666         private void noticeIntent(Intent intent) {
667
668                 /* Ok, this is pretty convenient.
669                  *
670                  * When a USB device is plugged in, and our 'hotplug'
671                  * intent registration fires, we get an Intent with
672                  * EXTRA_DEVICE set.
673                  *
674                  * When we start up and see a usb device and request
675                  * permission to access it, that queues a
676                  * PendingIntent, which has the EXTRA_DEVICE added in,
677                  * along with the EXTRA_PERMISSION_GRANTED field as
678                  * well.
679                  *
680                  * So, in both cases, we get the device name using the
681                  * same call. We check to see if access was granted,
682                  * in which case we ignore the device field and do our
683                  * usual startup thing.
684                  */
685
686                 UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
687                 boolean granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, true);
688
689                 AltosDebug.debug("intent %s device %s granted %s", intent, device, granted);
690
691                 if (!granted)
692                         device = null;
693
694                 if (device != null) {
695                         AltosDebug.debug("intent has usb device " + device.toString());
696                         connectUsb(device);
697                 } else {
698
699                         /* 'granted' is only false if this intent came
700                          * from the request_permission call and
701                          * permission was denied. In which case, we
702                          * don't want to loop forever...
703                          */
704                         if (granted) {
705                                 AltosDebug.debug("check for a USB device at startup");
706                                 if (check_usb())
707                                         return;
708                         }
709                         AltosDebug.debug("Starting by looking for bluetooth devices");
710                         ensureBluetooth();
711                 }
712         }
713
714         @Override
715         public void onStart() {
716                 super.onStart();
717                 AltosDebug.debug("++ ON START ++");
718
719                 set_switch_time();
720
721                 noticeIntent(getIntent());
722
723                 // Start Telemetry Service
724                 String  action = start_with_usb ? ACTION_USB : ACTION_BLUETOOTH;
725
726                 startService(new Intent(action, null, AltosDroid.this, TelemetryService.class));
727
728                 doBindService();
729
730                 if (mAltosVoice == null)
731                         mAltosVoice = new AltosVoice(this);
732
733         }
734
735         @Override
736         public void onNewIntent(Intent intent) {
737                 super.onNewIntent(intent);
738                 AltosDebug.debug("onNewIntent");
739                 noticeIntent(intent);
740         }
741
742         private void enable_location_updates() {
743                 // Listen for GPS and Network position updates
744                 LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
745
746                 if (locationManager != null)
747                 {
748                         try {
749                                 locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 1, this);
750                                 location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
751                         } catch (Exception e) {
752                                 locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 1000, 1, this);
753                                 location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
754                         }
755
756                         if (location != null)
757                                 AltosDebug.debug("Resume, location is %f,%f\n",
758                                                  location.getLatitude(),
759                                                  location.getLongitude());
760                         AltosDebug.debug("Failed to get GPS updates\n");
761                 }
762
763                 update_ui(telemetry_state, state, true);
764         }
765
766         static final int MY_PERMISSION_REQUEST = 1001;
767
768         public boolean have_location_permission = false;
769         public boolean have_storage_permission = false;
770         public boolean asked_permission = false;
771
772         AltosMapOnline map_online;
773
774         void
775         tell_map_permission(AltosMapOnline map_online) {
776                 this.map_online = map_online;
777         }
778
779         @Override
780         public void onRequestPermissionsResult(int requestCode, String[] permissions,
781                                                int[] grantResults) {
782                 if (requestCode == MY_PERMISSION_REQUEST) {
783                         for (int i = 0; i < grantResults.length; i++) {
784                                 if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
785                                         if (permissions[i].equals(Manifest.permission.ACCESS_FINE_LOCATION)) {
786                                                 have_location_permission = true;
787                                                 enable_location_updates();
788                                                 if (map_online != null)
789                                                         map_online.position_permission();
790                                         }
791                                         if (permissions[i].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
792                                                 have_storage_permission = true;
793                                         }
794                                 }
795                         }
796                 }
797         }
798
799         @Override
800         public void onResume() {
801                 super.onResume();
802                 AltosDebug.debug("+ ON RESUME +");
803
804                 if (!asked_permission) {
805                         asked_permission = true;
806                         if (ActivityCompat.checkSelfPermission(this,
807                                                               Manifest.permission.ACCESS_FINE_LOCATION)
808                             == PackageManager.PERMISSION_GRANTED)
809                         {
810                                 have_location_permission = true;
811                         }
812                         if (ActivityCompat.checkSelfPermission(this,
813                                                                Manifest.permission.WRITE_EXTERNAL_STORAGE)
814                             == PackageManager.PERMISSION_GRANTED)
815                         {
816                                 have_storage_permission = true;
817                         }
818                         int count = (have_location_permission ? 0 : 1) + (have_storage_permission ? 0 : 1);
819                         if (count > 0)
820                         {
821                                 String[] permissions = new String[count];
822                                 int i = 0;
823                                 if (!have_location_permission)
824                                         permissions[i++] = Manifest.permission.ACCESS_FINE_LOCATION;
825                                 if (!have_location_permission)
826                                         permissions[i++] = Manifest.permission.WRITE_EXTERNAL_STORAGE;
827                                 ActivityCompat.requestPermissions(this, permissions, MY_PERMISSION_REQUEST);
828                         }
829                 }
830                 if (have_location_permission)
831                         enable_location_updates();
832         }
833
834         @Override
835         public void onPause() {
836                 super.onPause();
837                 AltosDebug.debug("- ON PAUSE -");
838                 // Stop listening for location updates
839                 if (have_location_permission)
840                         ((LocationManager) getSystemService(Context.LOCATION_SERVICE)).removeUpdates(this);
841         }
842
843         @Override
844         public void onStop() {
845                 super.onStop();
846                 AltosDebug.debug("-- ON STOP --");
847         }
848
849         @Override
850         public void onDestroy() {
851                 super.onDestroy();
852                 AltosDebug.debug("--- ON DESTROY ---");
853
854                 doUnbindService();
855                 if (mAltosVoice != null) {
856                         mAltosVoice.stop();
857                         mAltosVoice = null;
858                 }
859                 stop_timer();
860         }
861
862         protected void onActivityResult(int requestCode, int resultCode, Intent data) {
863                 AltosDebug.debug("onActivityResult request %d result %d", requestCode, resultCode);
864                 switch (requestCode) {
865                 case REQUEST_CONNECT_DEVICE:
866                         // When DeviceListActivity returns with a device to connect to
867                         if (resultCode == Activity.RESULT_OK) {
868                                 connectDevice(data);
869                         }
870                         break;
871                 case REQUEST_ENABLE_BT:
872                         // When the request to enable Bluetooth returns
873                         if (resultCode == Activity.RESULT_OK) {
874                                 // Bluetooth is now enabled, so set up a chat session
875                                 //setupChat();
876                                 AltosDebug.debug("BT enabled");
877                                 bluetoothEnabled(data);
878                         } else {
879                                 // User did not enable Bluetooth or an error occured
880                                 AltosDebug.debug("BT not enabled");
881                         }
882                         break;
883                 case REQUEST_IDLE_MODE:
884                         if (resultCode == Activity.RESULT_OK)
885                                 idle_mode(data);
886                         break;
887                 case REQUEST_IGNITERS:
888                         break;
889                 case REQUEST_SETUP:
890                         if (resultCode == Activity.RESULT_OK)
891                                 note_setup_changes(data);
892                         break;
893                 case REQUEST_SELECT_TRACKER:
894                         if (resultCode == Activity.RESULT_OK)
895                                 select_tracker(data);
896                         break;
897                 case REQUEST_DELETE_TRACKER:
898                         if (resultCode == Activity.RESULT_OK)
899                                 delete_track(data);
900                         break;
901                 }
902         }
903
904         private void note_setup_changes(Intent data) {
905                 int changes = data.getIntExtra(SetupActivity.EXTRA_SETUP_CHANGES, 0);
906
907                 AltosDebug.debug("note_setup_changes changes %d\n", changes);
908
909                 if ((changes & SETUP_BAUD) != 0) {
910                         try {
911                                 mService.send(Message.obtain(null, TelemetryService.MSG_SETBAUD,
912                                                              AltosPreferences.telemetry_rate(1)));
913                         } catch (RemoteException re) {
914                         }
915                 }
916                 if ((changes & SETUP_UNITS) != 0) {
917                         /* nothing to do here */
918                 }
919                 if ((changes & SETUP_MAP_SOURCE) != 0) {
920                         /* nothing to do here */
921                 }
922                 if ((changes & SETUP_MAP_TYPE) != 0) {
923                         /* nothing to do here */
924                 }
925                 set_switch_time();
926                 if ((changes & SETUP_FONT_SIZE) != 0) {
927                         AltosDebug.debug(" ==== Recreate to switch font sizes ==== ");
928                         finish();
929                         startActivity(getIntent());
930                 }
931         }
932
933         private void connectUsb(UsbDevice device) {
934                 if (mService == null)
935                         pending_usb_device = device;
936                 else {
937                         // Attempt to connect to the device
938                         try {
939                                 mService.send(Message.obtain(null, TelemetryService.MSG_OPEN_USB, device));
940                                 AltosDebug.debug("Sent OPEN_USB message");
941                         } catch (RemoteException e) {
942                                 AltosDebug.debug("connect device message failed");
943                         }
944                 }
945         }
946
947         private void bluetoothEnabled(Intent data) {
948                 if (mService != null) {
949                         try {
950                                 mService.send(Message.obtain(null, TelemetryService.MSG_BLUETOOTH_ENABLED, null));
951                         } catch (RemoteException e) {
952                                 AltosDebug.debug("send BT enabled message failed");
953                         }
954                 }
955         }
956
957         private void connectDevice(Intent data) {
958                 // Attempt to connect to the device
959                 try {
960                         String address = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
961                         String name = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_NAME);
962
963                         AltosDebug.debug("Connecting to " + address + " " + name);
964                         DeviceAddress   a = new DeviceAddress(address, name);
965                         mService.send(Message.obtain(null, TelemetryService.MSG_CONNECT, a));
966                         AltosDebug.debug("Sent connecting message");
967                 } catch (RemoteException e) {
968                         AltosDebug.debug("connect device message failed");
969                 }
970         }
971
972         private void disconnectDevice(boolean remember) {
973                 try {
974                         mService.send(Message.obtain(null, TelemetryService.MSG_DISCONNECT, (Boolean) remember));
975                 } catch (RemoteException e) {
976                 }
977         }
978
979         private void idle_mode(Intent data) {
980                 int type = data.getIntExtra(IdleModeActivity.EXTRA_IDLE_RESULT, -1);
981                 Message msg;
982
983                 AltosDebug.debug("intent idle_mode %d", type);
984                 switch (type) {
985                 case IdleModeActivity.IDLE_MODE_CONNECT:
986                         msg = Message.obtain(null, TelemetryService.MSG_MONITOR_IDLE_START);
987                         try {
988                                 mService.send(msg);
989                         } catch (RemoteException re) {
990                         }
991                         break;
992                 case IdleModeActivity.IDLE_MODE_DISCONNECT:
993                         msg = Message.obtain(null, TelemetryService.MSG_MONITOR_IDLE_STOP);
994                         try {
995                                 mService.send(msg);
996                         } catch (RemoteException re) {
997                         }
998                         break;
999                 case IdleModeActivity.IDLE_MODE_REBOOT:
1000                         msg = Message.obtain(null, TelemetryService.MSG_REBOOT);
1001                         try {
1002                                 mService.send(msg);
1003                         } catch (RemoteException re) {
1004                         }
1005                         break;
1006                 case IdleModeActivity.IDLE_MODE_IGNITERS:
1007                         Intent serverIntent = new Intent(this, IgniterActivity.class);
1008                         startActivityForResult(serverIntent, REQUEST_IGNITERS);
1009                         break;
1010                 }
1011         }
1012
1013         @Override
1014         public boolean onCreateOptionsMenu(Menu menu) {
1015                 MenuInflater inflater = getMenuInflater();
1016                 inflater.inflate(R.menu.option_menu, menu);
1017                 return true;
1018         }
1019
1020         double telem_frequency = 434.550;
1021         double selected_frequency = AltosLib.MISSING;
1022
1023         void setFrequency(double freq) {
1024                 telem_frequency = freq;
1025                 selected_frequency = AltosLib.MISSING;
1026                 try {
1027                         mService.send(Message.obtain(null, TelemetryService.MSG_SETFREQUENCY, freq));
1028                         set_switch_time();
1029                 } catch (RemoteException e) {
1030                 }
1031         }
1032
1033         void setFrequency(AltosFrequency frequency) {
1034                 setFrequency (frequency.frequency);
1035         }
1036
1037         void setBaud(int baud) {
1038                 try {
1039                         mService.send(Message.obtain(null, TelemetryService.MSG_SETBAUD, baud));
1040                         set_switch_time();
1041                 } catch (RemoteException e) {
1042                 }
1043         }
1044
1045         void setBaud(String baud) {
1046                 try {
1047                         int     value = Integer.parseInt(baud);
1048                         int     rate = AltosLib.ao_telemetry_rate_38400;
1049                         switch (value) {
1050                         case 2400:
1051                                 rate = AltosLib.ao_telemetry_rate_2400;
1052                                 break;
1053                         case 9600:
1054                                 rate = AltosLib.ao_telemetry_rate_9600;
1055                                 break;
1056                         case 38400:
1057                                 rate = AltosLib.ao_telemetry_rate_38400;
1058                                 break;
1059                         }
1060                         setBaud(rate);
1061                 } catch (NumberFormatException e) {
1062                 }
1063         }
1064
1065         void select_tracker(int serial, double frequency) {
1066
1067                 AltosDebug.debug("select tracker %d %7.3f\n", serial, frequency);
1068
1069                 if (serial == selected_serial) {
1070                         AltosDebug.debug("%d already selected\n", serial);
1071                         return;
1072                 }
1073
1074                 if (serial != 0) {
1075                         int i;
1076                         for (i = 0; i < trackers.length; i++)
1077                                 if (trackers[i].serial == serial)
1078                                         break;
1079
1080                         if (i == trackers.length) {
1081                                 AltosDebug.debug("attempt to select unknown tracker %d\n", serial);
1082                                 return;
1083                         }
1084                         if (frequency != 0.0 && frequency != AltosLib.MISSING)
1085                                 setFrequency(frequency);
1086                 }
1087
1088                 selected_serial = serial;
1089                 update_state(null);
1090         }
1091
1092         void select_tracker(Intent data) {
1093                 int serial = data.getIntExtra(SelectTrackerActivity.EXTRA_SERIAL_NUMBER, 0);
1094                 double frequency = data.getDoubleExtra(SelectTrackerActivity.EXTRA_FREQUENCY, 0.0);
1095                 select_tracker(serial, frequency);
1096         }
1097
1098         void delete_track(int serial) {
1099                 try {
1100                         mService.send(Message.obtain(null, TelemetryService.MSG_DELETE_SERIAL, (Integer) serial));
1101                 } catch (Exception ex) {
1102                 }
1103         }
1104
1105         void delete_track(Intent data) {
1106                 int serial = data.getIntExtra(SelectTrackerActivity.EXTRA_SERIAL_NUMBER, 0);
1107                 if (serial != 0)
1108                         delete_track(serial);
1109         }
1110
1111         void start_select_tracker(Tracker[] select_trackers, int title_id, int request) {
1112                 Intent intent = new Intent(this, SelectTrackerActivity.class);
1113                 AltosDebug.debug("put title id 0x%x %s", title_id, getResources().getString(title_id));
1114                 intent.putExtra(EXTRA_TRACKERS_TITLE, title_id);
1115                 if (select_trackers != null) {
1116                         ArrayList<Tracker> tracker_array = new ArrayList<Tracker>(Arrays.asList(select_trackers));
1117                         intent.putParcelableArrayListExtra(EXTRA_TRACKERS, tracker_array);
1118                 } else {
1119                         intent.putExtra(EXTRA_TRACKERS, (Parcelable[]) null);
1120                 }
1121                 startActivityForResult(intent, request);
1122         }
1123
1124         void start_select_tracker(Tracker[] select_trackers) {
1125                 start_select_tracker(select_trackers, R.string.select_tracker, REQUEST_SELECT_TRACKER);
1126         }
1127
1128         void touch_trackers(Integer[] serials) {
1129                 Tracker[] my_trackers = new Tracker[serials.length];
1130
1131                 for (int i = 0; i < serials.length; i++) {
1132                         AltosState      s = telemetry_state.get(serials[i]);
1133                         my_trackers[i] = new Tracker(s);
1134                 }
1135                 start_select_tracker(my_trackers);
1136         }
1137
1138         @Override
1139         public boolean onOptionsItemSelected(MenuItem item) {
1140                 Intent serverIntent = null;
1141                 switch (item.getItemId()) {
1142                 case R.id.connect_scan:
1143                         ensureBluetooth();
1144                         // Launch the DeviceListActivity to see devices and do scan
1145                         serverIntent = new Intent(this, DeviceListActivity.class);
1146                         startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);
1147                         return true;
1148                 case R.id.disconnect:
1149                         /* Disconnect the device
1150                          */
1151                         disconnectDevice(false);
1152                         return true;
1153                 case R.id.quit:
1154                         AltosDebug.debug("R.id.quit");
1155                         disconnectDevice(true);
1156                         finish();
1157                         return true;
1158                 case R.id.setup:
1159                         serverIntent = new Intent(this, SetupActivity.class);
1160                         startActivityForResult(serverIntent, REQUEST_SETUP);
1161                         return true;
1162                 case R.id.select_freq:
1163                         // Set the TBT radio frequency
1164
1165                         final AltosFrequency[] frequencies = AltosPreferences.common_frequencies();
1166                         String[] frequency_strings = new String[frequencies.length];
1167                         for (int i = 0; i < frequencies.length; i++)
1168                                 frequency_strings[i] = frequencies[i].toString();
1169
1170                         AlertDialog.Builder builder_freq = new AlertDialog.Builder(this);
1171                         builder_freq.setTitle("Select Frequency");
1172                         builder_freq.setItems(frequency_strings,
1173                                          new DialogInterface.OnClickListener() {
1174                                                  public void onClick(DialogInterface dialog, int item) {
1175                                                          setFrequency(frequencies[item]);
1176                                                          selected_frequency = frequencies[item].frequency;
1177                                                  }
1178                                          });
1179                         AlertDialog alert_freq = builder_freq.create();
1180                         alert_freq.show();
1181                         return true;
1182                 case R.id.select_tracker:
1183                         start_select_tracker(trackers);
1184                         return true;
1185                 case R.id.delete_track:
1186                         if (trackers != null && trackers.length > 0)
1187                                 start_select_tracker(trackers, R.string.delete_track, REQUEST_DELETE_TRACKER);
1188                         return true;
1189                 case R.id.idle_mode:
1190                         serverIntent = new Intent(this, IdleModeActivity.class);
1191                         serverIntent.putExtra(EXTRA_IDLE_MODE, idle_mode);
1192                         serverIntent.putExtra(EXTRA_FREQUENCY, telem_frequency);
1193                         startActivityForResult(serverIntent, REQUEST_IDLE_MODE);
1194                         return true;
1195                 }
1196                 return false;
1197         }
1198
1199         static String direction(AltosGreatCircle from_receiver,
1200                                 Location receiver) {
1201                 if (from_receiver == null)
1202                         return null;
1203
1204                 if (receiver == null)
1205                         return null;
1206
1207                 if (!receiver.hasBearing())
1208                         return null;
1209
1210                 float   bearing = receiver.getBearing();
1211                 float   heading = (float) from_receiver.bearing - bearing;
1212
1213                 while (heading <= -180.0f)
1214                         heading += 360.0f;
1215                 while (heading > 180.0f)
1216                         heading -= 360.0f;
1217
1218                 int iheading = (int) (heading + 0.5f);
1219
1220                 if (-1 < iheading && iheading < 1)
1221                         return "ahead";
1222                 else if (iheading < -179 || 179 < iheading)
1223                         return "backwards";
1224                 else if (iheading < 0)
1225                         return String.format("left %d°", -iheading);
1226                 else
1227                         return String.format("right %d°", iheading);
1228         }
1229
1230         public void onLocationChanged(Location location) {
1231                 this.location = location;
1232                 AltosDebug.debug("Location changed to %f,%f",
1233                                  location.getLatitude(),
1234                                  location.getLongitude());
1235                 update_ui(telemetry_state, state, false);
1236         }
1237
1238         public void onStatusChanged(String provider, int status, Bundle extras) {
1239                 AltosDebug.debug("Location status now %d\n", status);
1240         }
1241
1242         public void onProviderEnabled(String provider) {
1243                 AltosDebug.debug("Location provider enabled %s\n", provider);
1244         }
1245
1246         public void onProviderDisabled(String provider) {
1247                 AltosDebug.debug("Location provider disabled %s\n", provider);
1248         }
1249 }