Bump java lib versions in preparation for 1.9.2
[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, getTheme());
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                 locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 1, this);
746
747                 location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
748
749                 if (location != null)
750                         AltosDebug.debug("Resume, location is %f,%f\n",
751                                          location.getLatitude(),
752                                          location.getLongitude());
753
754                 update_ui(telemetry_state, state, true);
755         }
756
757         static final int MY_PERMISSION_REQUEST = 1001;
758
759         public boolean have_location_permission = false;
760         public boolean have_storage_permission = false;
761         public boolean asked_permission = false;
762
763         AltosMapOnline map_online;
764
765         void
766         tell_map_permission(AltosMapOnline map_online) {
767                 this.map_online = map_online;
768         }
769
770         @Override
771         public void onRequestPermissionsResult(int requestCode, String[] permissions,
772                                                int[] grantResults) {
773                 if (requestCode == MY_PERMISSION_REQUEST) {
774                         for (int i = 0; i < grantResults.length; i++) {
775                                 if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
776                                         if (permissions[i].equals(Manifest.permission.ACCESS_FINE_LOCATION)) {
777                                                 have_location_permission = true;
778                                                 enable_location_updates();
779                                                 if (map_online != null)
780                                                         map_online.position_permission();
781                                         }
782                                         if (permissions[i].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
783                                                 have_storage_permission = true;
784                                         }
785                                 }
786                         }
787                 }
788         }
789
790         @Override
791         public void onResume() {
792                 super.onResume();
793                 AltosDebug.debug("+ ON RESUME +");
794
795                 if (!asked_permission) {
796                         asked_permission = true;
797                         if (ActivityCompat.checkSelfPermission(this,
798                                                               Manifest.permission.ACCESS_FINE_LOCATION)
799                             == PackageManager.PERMISSION_GRANTED)
800                         {
801                                 have_location_permission = true;
802                         }
803                         if (ActivityCompat.checkSelfPermission(this,
804                                                                Manifest.permission.WRITE_EXTERNAL_STORAGE)
805                             == PackageManager.PERMISSION_GRANTED)
806                         {
807                                 have_storage_permission = true;
808                         }
809                         int count = (have_location_permission ? 0 : 1) + (have_storage_permission ? 0 : 1);
810                         if (count > 0)
811                         {
812                                 String[] permissions = new String[count];
813                                 int i = 0;
814                                 if (!have_location_permission)
815                                         permissions[i++] = Manifest.permission.ACCESS_FINE_LOCATION;
816                                 if (!have_location_permission)
817                                         permissions[i++] = Manifest.permission.WRITE_EXTERNAL_STORAGE;
818                                 ActivityCompat.requestPermissions(this, permissions, MY_PERMISSION_REQUEST);
819                         }
820                 }
821                 if (have_location_permission)
822                         enable_location_updates();
823         }
824
825         @Override
826         public void onPause() {
827                 super.onPause();
828                 AltosDebug.debug("- ON PAUSE -");
829                 // Stop listening for location updates
830                 if (have_location_permission)
831                         ((LocationManager) getSystemService(Context.LOCATION_SERVICE)).removeUpdates(this);
832         }
833
834         @Override
835         public void onStop() {
836                 super.onStop();
837                 AltosDebug.debug("-- ON STOP --");
838         }
839
840         @Override
841         public void onDestroy() {
842                 super.onDestroy();
843                 AltosDebug.debug("--- ON DESTROY ---");
844
845                 doUnbindService();
846                 if (mAltosVoice != null) {
847                         mAltosVoice.stop();
848                         mAltosVoice = null;
849                 }
850                 stop_timer();
851         }
852
853         protected void onActivityResult(int requestCode, int resultCode, Intent data) {
854                 AltosDebug.debug("onActivityResult request %d result %d", requestCode, resultCode);
855                 switch (requestCode) {
856                 case REQUEST_CONNECT_DEVICE:
857                         // When DeviceListActivity returns with a device to connect to
858                         if (resultCode == Activity.RESULT_OK) {
859                                 connectDevice(data);
860                         }
861                         break;
862                 case REQUEST_ENABLE_BT:
863                         // When the request to enable Bluetooth returns
864                         if (resultCode == Activity.RESULT_OK) {
865                                 // Bluetooth is now enabled, so set up a chat session
866                                 //setupChat();
867                                 AltosDebug.debug("BT enabled");
868                                 bluetoothEnabled(data);
869                         } else {
870                                 // User did not enable Bluetooth or an error occured
871                                 AltosDebug.debug("BT not enabled");
872                         }
873                         break;
874                 case REQUEST_IDLE_MODE:
875                         if (resultCode == Activity.RESULT_OK)
876                                 idle_mode(data);
877                         break;
878                 case REQUEST_IGNITERS:
879                         break;
880                 case REQUEST_SETUP:
881                         if (resultCode == Activity.RESULT_OK)
882                                 note_setup_changes(data);
883                         break;
884                 case REQUEST_SELECT_TRACKER:
885                         if (resultCode == Activity.RESULT_OK)
886                                 select_tracker(data);
887                         break;
888                 case REQUEST_DELETE_TRACKER:
889                         if (resultCode == Activity.RESULT_OK)
890                                 delete_track(data);
891                         break;
892                 }
893         }
894
895         private void note_setup_changes(Intent data) {
896                 int changes = data.getIntExtra(SetupActivity.EXTRA_SETUP_CHANGES, 0);
897
898                 AltosDebug.debug("note_setup_changes changes %d\n", changes);
899
900                 if ((changes & SETUP_BAUD) != 0) {
901                         try {
902                                 mService.send(Message.obtain(null, TelemetryService.MSG_SETBAUD,
903                                                              AltosPreferences.telemetry_rate(1)));
904                         } catch (RemoteException re) {
905                         }
906                 }
907                 if ((changes & SETUP_UNITS) != 0) {
908                         /* nothing to do here */
909                 }
910                 if ((changes & SETUP_MAP_SOURCE) != 0) {
911                         /* nothing to do here */
912                 }
913                 if ((changes & SETUP_MAP_TYPE) != 0) {
914                         /* nothing to do here */
915                 }
916                 set_switch_time();
917                 if ((changes & SETUP_FONT_SIZE) != 0) {
918                         AltosDebug.debug(" ==== Recreate to switch font sizes ==== ");
919                         finish();
920                         startActivity(getIntent());
921                 }
922         }
923
924         private void connectUsb(UsbDevice device) {
925                 if (mService == null)
926                         pending_usb_device = device;
927                 else {
928                         // Attempt to connect to the device
929                         try {
930                                 mService.send(Message.obtain(null, TelemetryService.MSG_OPEN_USB, device));
931                                 AltosDebug.debug("Sent OPEN_USB message");
932                         } catch (RemoteException e) {
933                                 AltosDebug.debug("connect device message failed");
934                         }
935                 }
936         }
937
938         private void bluetoothEnabled(Intent data) {
939                 if (mService != null) {
940                         try {
941                                 mService.send(Message.obtain(null, TelemetryService.MSG_BLUETOOTH_ENABLED, null));
942                         } catch (RemoteException e) {
943                                 AltosDebug.debug("send BT enabled message failed");
944                         }
945                 }
946         }
947
948         private void connectDevice(Intent data) {
949                 // Attempt to connect to the device
950                 try {
951                         String address = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
952                         String name = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_NAME);
953
954                         AltosDebug.debug("Connecting to " + address + " " + name);
955                         DeviceAddress   a = new DeviceAddress(address, name);
956                         mService.send(Message.obtain(null, TelemetryService.MSG_CONNECT, a));
957                         AltosDebug.debug("Sent connecting message");
958                 } catch (RemoteException e) {
959                         AltosDebug.debug("connect device message failed");
960                 }
961         }
962
963         private void disconnectDevice(boolean remember) {
964                 try {
965                         mService.send(Message.obtain(null, TelemetryService.MSG_DISCONNECT, (Boolean) remember));
966                 } catch (RemoteException e) {
967                 }
968         }
969
970         private void idle_mode(Intent data) {
971                 int type = data.getIntExtra(IdleModeActivity.EXTRA_IDLE_RESULT, -1);
972                 Message msg;
973
974                 AltosDebug.debug("intent idle_mode %d", type);
975                 switch (type) {
976                 case IdleModeActivity.IDLE_MODE_CONNECT:
977                         msg = Message.obtain(null, TelemetryService.MSG_MONITOR_IDLE_START);
978                         try {
979                                 mService.send(msg);
980                         } catch (RemoteException re) {
981                         }
982                         break;
983                 case IdleModeActivity.IDLE_MODE_DISCONNECT:
984                         msg = Message.obtain(null, TelemetryService.MSG_MONITOR_IDLE_STOP);
985                         try {
986                                 mService.send(msg);
987                         } catch (RemoteException re) {
988                         }
989                         break;
990                 case IdleModeActivity.IDLE_MODE_REBOOT:
991                         msg = Message.obtain(null, TelemetryService.MSG_REBOOT);
992                         try {
993                                 mService.send(msg);
994                         } catch (RemoteException re) {
995                         }
996                         break;
997                 case IdleModeActivity.IDLE_MODE_IGNITERS:
998                         Intent serverIntent = new Intent(this, IgniterActivity.class);
999                         startActivityForResult(serverIntent, REQUEST_IGNITERS);
1000                         break;
1001                 }
1002         }
1003
1004         @Override
1005         public boolean onCreateOptionsMenu(Menu menu) {
1006                 MenuInflater inflater = getMenuInflater();
1007                 inflater.inflate(R.menu.option_menu, menu);
1008                 return true;
1009         }
1010
1011         double telem_frequency = 434.550;
1012         double selected_frequency = AltosLib.MISSING;
1013
1014         void setFrequency(double freq) {
1015                 telem_frequency = freq;
1016                 selected_frequency = AltosLib.MISSING;
1017                 try {
1018                         mService.send(Message.obtain(null, TelemetryService.MSG_SETFREQUENCY, freq));
1019                         set_switch_time();
1020                 } catch (RemoteException e) {
1021                 }
1022         }
1023
1024         void setFrequency(AltosFrequency frequency) {
1025                 setFrequency (frequency.frequency);
1026         }
1027
1028         void setBaud(int baud) {
1029                 try {
1030                         mService.send(Message.obtain(null, TelemetryService.MSG_SETBAUD, baud));
1031                         set_switch_time();
1032                 } catch (RemoteException e) {
1033                 }
1034         }
1035
1036         void setBaud(String baud) {
1037                 try {
1038                         int     value = Integer.parseInt(baud);
1039                         int     rate = AltosLib.ao_telemetry_rate_38400;
1040                         switch (value) {
1041                         case 2400:
1042                                 rate = AltosLib.ao_telemetry_rate_2400;
1043                                 break;
1044                         case 9600:
1045                                 rate = AltosLib.ao_telemetry_rate_9600;
1046                                 break;
1047                         case 38400:
1048                                 rate = AltosLib.ao_telemetry_rate_38400;
1049                                 break;
1050                         }
1051                         setBaud(rate);
1052                 } catch (NumberFormatException e) {
1053                 }
1054         }
1055
1056         void select_tracker(int serial, double frequency) {
1057
1058                 AltosDebug.debug("select tracker %d %7.3f\n", serial, frequency);
1059
1060                 if (serial == selected_serial) {
1061                         AltosDebug.debug("%d already selected\n", serial);
1062                         return;
1063                 }
1064
1065                 if (serial != 0) {
1066                         int i;
1067                         for (i = 0; i < trackers.length; i++)
1068                                 if (trackers[i].serial == serial)
1069                                         break;
1070
1071                         if (i == trackers.length) {
1072                                 AltosDebug.debug("attempt to select unknown tracker %d\n", serial);
1073                                 return;
1074                         }
1075                         if (frequency != 0.0 && frequency != AltosLib.MISSING)
1076                                 setFrequency(frequency);
1077                 }
1078
1079                 selected_serial = serial;
1080                 update_state(null);
1081         }
1082
1083         void select_tracker(Intent data) {
1084                 int serial = data.getIntExtra(SelectTrackerActivity.EXTRA_SERIAL_NUMBER, 0);
1085                 double frequency = data.getDoubleExtra(SelectTrackerActivity.EXTRA_FREQUENCY, 0.0);
1086                 select_tracker(serial, frequency);
1087         }
1088
1089         void delete_track(int serial) {
1090                 try {
1091                         mService.send(Message.obtain(null, TelemetryService.MSG_DELETE_SERIAL, (Integer) serial));
1092                 } catch (Exception ex) {
1093                 }
1094         }
1095
1096         void delete_track(Intent data) {
1097                 int serial = data.getIntExtra(SelectTrackerActivity.EXTRA_SERIAL_NUMBER, 0);
1098                 if (serial != 0)
1099                         delete_track(serial);
1100         }
1101
1102         void start_select_tracker(Tracker[] select_trackers, int title_id, int request) {
1103                 Intent intent = new Intent(this, SelectTrackerActivity.class);
1104                 AltosDebug.debug("put title id 0x%x %s", title_id, getResources().getString(title_id));
1105                 intent.putExtra(EXTRA_TRACKERS_TITLE, title_id);
1106                 if (select_trackers != null) {
1107                         ArrayList<Tracker> tracker_array = new ArrayList<Tracker>(Arrays.asList(select_trackers));
1108                         intent.putParcelableArrayListExtra(EXTRA_TRACKERS, tracker_array);
1109                 } else {
1110                         intent.putExtra(EXTRA_TRACKERS, (Parcelable[]) null);
1111                 }
1112                 startActivityForResult(intent, request);
1113         }
1114
1115         void start_select_tracker(Tracker[] select_trackers) {
1116                 start_select_tracker(select_trackers, R.string.select_tracker, REQUEST_SELECT_TRACKER);
1117         }
1118
1119         void touch_trackers(Integer[] serials) {
1120                 Tracker[] my_trackers = new Tracker[serials.length];
1121
1122                 for (int i = 0; i < serials.length; i++) {
1123                         AltosState      s = telemetry_state.get(serials[i]);
1124                         my_trackers[i] = new Tracker(s);
1125                 }
1126                 start_select_tracker(my_trackers);
1127         }
1128
1129         @Override
1130         public boolean onOptionsItemSelected(MenuItem item) {
1131                 Intent serverIntent = null;
1132                 switch (item.getItemId()) {
1133                 case R.id.connect_scan:
1134                         ensureBluetooth();
1135                         // Launch the DeviceListActivity to see devices and do scan
1136                         serverIntent = new Intent(this, DeviceListActivity.class);
1137                         startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);
1138                         return true;
1139                 case R.id.disconnect:
1140                         /* Disconnect the device
1141                          */
1142                         disconnectDevice(false);
1143                         return true;
1144                 case R.id.quit:
1145                         AltosDebug.debug("R.id.quit");
1146                         disconnectDevice(true);
1147                         finish();
1148                         return true;
1149                 case R.id.setup:
1150                         serverIntent = new Intent(this, SetupActivity.class);
1151                         startActivityForResult(serverIntent, REQUEST_SETUP);
1152                         return true;
1153                 case R.id.select_freq:
1154                         // Set the TBT radio frequency
1155
1156                         final AltosFrequency[] frequencies = AltosPreferences.common_frequencies();
1157                         String[] frequency_strings = new String[frequencies.length];
1158                         for (int i = 0; i < frequencies.length; i++)
1159                                 frequency_strings[i] = frequencies[i].toString();
1160
1161                         AlertDialog.Builder builder_freq = new AlertDialog.Builder(this);
1162                         builder_freq.setTitle("Select Frequency");
1163                         builder_freq.setItems(frequency_strings,
1164                                          new DialogInterface.OnClickListener() {
1165                                                  public void onClick(DialogInterface dialog, int item) {
1166                                                          setFrequency(frequencies[item]);
1167                                                          selected_frequency = frequencies[item].frequency;
1168                                                  }
1169                                          });
1170                         AlertDialog alert_freq = builder_freq.create();
1171                         alert_freq.show();
1172                         return true;
1173                 case R.id.select_tracker:
1174                         start_select_tracker(trackers);
1175                         return true;
1176                 case R.id.delete_track:
1177                         if (trackers != null && trackers.length > 0)
1178                                 start_select_tracker(trackers, R.string.delete_track, REQUEST_DELETE_TRACKER);
1179                         return true;
1180                 case R.id.idle_mode:
1181                         serverIntent = new Intent(this, IdleModeActivity.class);
1182                         serverIntent.putExtra(EXTRA_IDLE_MODE, idle_mode);
1183                         serverIntent.putExtra(EXTRA_FREQUENCY, telem_frequency);
1184                         startActivityForResult(serverIntent, REQUEST_IDLE_MODE);
1185                         return true;
1186                 }
1187                 return false;
1188         }
1189
1190         static String direction(AltosGreatCircle from_receiver,
1191                                 Location receiver) {
1192                 if (from_receiver == null)
1193                         return null;
1194
1195                 if (receiver == null)
1196                         return null;
1197
1198                 if (!receiver.hasBearing())
1199                         return null;
1200
1201                 float   bearing = receiver.getBearing();
1202                 float   heading = (float) from_receiver.bearing - bearing;
1203
1204                 while (heading <= -180.0f)
1205                         heading += 360.0f;
1206                 while (heading > 180.0f)
1207                         heading -= 360.0f;
1208
1209                 int iheading = (int) (heading + 0.5f);
1210
1211                 if (-1 < iheading && iheading < 1)
1212                         return "ahead";
1213                 else if (iheading < -179 || 179 < iheading)
1214                         return "backwards";
1215                 else if (iheading < 0)
1216                         return String.format("left %d°", -iheading);
1217                 else
1218                         return String.format("right %d°", iheading);
1219         }
1220
1221         public void onLocationChanged(Location location) {
1222                 this.location = location;
1223                 AltosDebug.debug("Location changed to %f,%f",
1224                                  location.getLatitude(),
1225                                  location.getLongitude());
1226                 update_ui(telemetry_state, state, false);
1227         }
1228
1229         public void onStatusChanged(String provider, int status, Bundle extras) {
1230                 AltosDebug.debug("Location status now %d\n", status);
1231         }
1232
1233         public void onProviderEnabled(String provider) {
1234                 AltosDebug.debug("Location provider enabled %s\n", provider);
1235         }
1236
1237         public void onProviderDisabled(String provider) {
1238                 AltosDebug.debug("Location provider disabled %s\n", provider);
1239         }
1240 }