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