first cut at turnon scripts for EasyTimer v2
[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                         int             flag;
678
679                         if (android.os.Build.VERSION.SDK_INT >= 31) // android.os.Build.VERSION_CODES.S
680                                 flag = 33554432; // PendingIntent.FLAG_MUTABLE
681                         else
682                                 flag = 0;
683                         PendingIntent   pi = PendingIntent.getActivity(this, 0, new Intent("hello world", null, this, AltosDroid.class), flag);
684
685                         if (AltosUsb.request_permission(this, device, pi)) {
686                                 connectUsb(device);
687                         }
688                         start_with_usb = true;
689                         return true;
690                 }
691
692                 start_with_usb = false;
693
694                 return false;
695         }
696
697         private void noticeIntent(Intent intent) {
698
699                 /* Ok, this is pretty convenient.
700                  *
701                  * When a USB device is plugged in, and our 'hotplug'
702                  * intent registration fires, we get an Intent with
703                  * EXTRA_DEVICE set.
704                  *
705                  * When we start up and see a usb device and request
706                  * permission to access it, that queues a
707                  * PendingIntent, which has the EXTRA_DEVICE added in,
708                  * along with the EXTRA_PERMISSION_GRANTED field as
709                  * well.
710                  *
711                  * So, in both cases, we get the device name using the
712                  * same call. We check to see if access was granted,
713                  * in which case we ignore the device field and do our
714                  * usual startup thing.
715                  */
716
717                 UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
718                 boolean granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, true);
719
720                 AltosDebug.debug("intent %s device %s granted %s", intent, device, granted);
721
722                 if (!granted)
723                         device = null;
724
725                 if (device != null) {
726                         AltosDebug.debug("intent has usb device " + device.toString());
727                         connectUsb(device);
728                 } else {
729
730                         /* 'granted' is only false if this intent came
731                          * from the request_permission call and
732                          * permission was denied. In which case, we
733                          * don't want to loop forever...
734                          */
735                         if (granted) {
736                                 AltosDebug.debug("check for a USB device at startup");
737                                 if (check_usb())
738                                         return;
739                         }
740                         AltosDebug.debug("Starting by looking for bluetooth devices");
741                         ensureBluetooth();
742                 }
743         }
744
745         @Override
746         public void onStart() {
747                 super.onStart();
748                 AltosDebug.debug("++ ON START ++");
749
750                 set_switch_time();
751
752                 noticeIntent(getIntent());
753
754                 // Start Telemetry Service
755                 String  action = start_with_usb ? ACTION_USB : ACTION_BLUETOOTH;
756
757                 startService(new Intent(action, null, AltosDroid.this, TelemetryService.class));
758
759                 doBindService();
760
761                 if (mAltosVoice == null)
762                         mAltosVoice = new AltosVoice(this);
763
764         }
765
766         @Override
767         public void onNewIntent(Intent intent) {
768                 super.onNewIntent(intent);
769                 AltosDebug.debug("+ ON NEW INTENT +");
770                 noticeIntent(intent);
771         }
772
773         private void enable_location_updates(boolean do_update) {
774                 // Listen for GPS and Network position updates
775                 LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
776
777                 if (locationManager != null)
778                 {
779                         try {
780                                 locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 1, this);
781                                 location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
782                         } catch (Exception e) {
783                                 locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 1000, 1, this);
784                                 location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
785                         }
786
787                         if (location != null)
788                                 AltosDebug.debug("Resume, location is %f,%f\n",
789                                                  location.getLatitude(),
790                                                  location.getLongitude());
791                         AltosDebug.debug("Failed to get GPS updates\n");
792                 }
793
794                 if (do_update)
795                         update_ui(telemetry_state, state, true);
796         }
797
798         static final int MY_PERMISSION_REQUEST = 1001;
799
800         public boolean have_location_permission = false;
801         public boolean have_storage_permission = false;
802         public boolean have_bluetooth_permission = false;
803         public boolean have_bluetooth_connect_permission = false;
804         public boolean have_bluetooth_scan_permission = false;
805         public boolean asked_permission = false;
806
807         static final String BLUETOOTH_CONNECT = "android.permission.BLUETOOTH_CONNECT";
808         static final String BLUETOOTH_SCAN = "android.permission.BLUETOOTH_SCAN";
809
810         AltosMapOnline map_online;
811
812         void
813         tell_map_permission(AltosMapOnline map_online) {
814                 this.map_online = map_online;
815         }
816
817         @Override
818         public void onRequestPermissionsResult(int requestCode, String[] permissions,
819                                                int[] grantResults) {
820                 if (requestCode == MY_PERMISSION_REQUEST) {
821                         for (int i = 0; i < grantResults.length; i++) {
822                                 if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
823                                         if (permissions[i].equals(Manifest.permission.ACCESS_FINE_LOCATION)) {
824                                                 have_location_permission = true;
825                                                 enable_location_updates(true);
826                                                 if (map_online != null)
827                                                         map_online.position_permission();
828                                         }
829                                         if (permissions[i].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
830                                                 have_storage_permission = true;
831                                         }
832                                         if (permissions[i].equals(Manifest.permission.BLUETOOTH)) {
833                                                 have_bluetooth_permission = true;
834                                         }
835                                         if (permissions[i].equals(BLUETOOTH_CONNECT)) {
836                                                 have_bluetooth_connect_permission = true;
837                                         }
838                                         if (permissions[i].equals(BLUETOOTH_SCAN)) {
839                                                 have_bluetooth_scan_permission = true;
840                                         }
841                                 }
842                         }
843                 }
844         }
845
846         @Override
847         public void onResume() {
848                 AltosDebug.debug("+ ON RESUME +");
849
850                 super.onResume();
851
852                 if (!asked_permission) {
853                         asked_permission = true;
854                         if (ActivityCompat.checkSelfPermission(this,
855                                                               Manifest.permission.ACCESS_FINE_LOCATION)
856                             == PackageManager.PERMISSION_GRANTED)
857                         {
858                                 have_location_permission = true;
859                         }
860                         if (ActivityCompat.checkSelfPermission(this,
861                                                                Manifest.permission.WRITE_EXTERNAL_STORAGE)
862                             == PackageManager.PERMISSION_GRANTED)
863                         {
864                                 have_storage_permission = true;
865                         }
866                         if (ActivityCompat.checkSelfPermission(this,
867                                                                Manifest.permission.BLUETOOTH)
868                             == PackageManager.PERMISSION_GRANTED)
869                         {
870                                 have_bluetooth_permission = true;
871                         }
872                         if (ActivityCompat.checkSelfPermission(this,
873                                                                BLUETOOTH_CONNECT)
874                             == PackageManager.PERMISSION_GRANTED)
875                         {
876                                 have_bluetooth_connect_permission = true;
877                         }
878                         if (ActivityCompat.checkSelfPermission(this,
879                                                                BLUETOOTH_SCAN)
880                             == PackageManager.PERMISSION_GRANTED)
881                         {
882                                 have_bluetooth_scan_permission = true;
883                         }
884                         int count = 0;
885                         if (!have_location_permission)
886                                 count += 1;
887                         if (!have_storage_permission)
888                                 count += 1;
889                         if (!have_bluetooth_permission)
890                                 count += 1;
891                         if (!have_bluetooth_connect_permission)
892                                 count += 1;
893                         if (!have_bluetooth_scan_permission)
894                                 count += 1;
895                         if (count > 0)
896                         {
897                                 String[] permissions = new String[count];
898                                 int i = 0;
899                                 if (!have_location_permission)
900                                         permissions[i++] = Manifest.permission.ACCESS_FINE_LOCATION;
901                                 if (!have_storage_permission)
902                                         permissions[i++] = Manifest.permission.WRITE_EXTERNAL_STORAGE;
903                                 if (!have_bluetooth_permission)
904                                         permissions[i++] = Manifest.permission.BLUETOOTH;
905                                 if (!have_bluetooth_connect_permission)
906                                         permissions[i++] = BLUETOOTH_CONNECT;
907                                 if (!have_bluetooth_scan_permission)
908                                         permissions[i++] = BLUETOOTH_SCAN;
909                                 ActivityCompat.requestPermissions(this, permissions, MY_PERMISSION_REQUEST);
910                         }
911                 }
912                 if (have_location_permission)
913                         enable_location_updates(false);
914         }
915
916         @Override
917         public void onPause() {
918                 AltosDebug.debug("- ON PAUSE -");
919
920                 super.onPause();
921
922                 // Stop listening for location updates
923                 if (have_location_permission)
924                         ((LocationManager) getSystemService(Context.LOCATION_SERVICE)).removeUpdates(this);
925         }
926
927         @Override
928         public void onStop() {
929                 AltosDebug.debug("-- ON STOP --");
930
931                 super.onStop();
932         }
933
934         @Override
935         public void onDestroy() {
936                 AltosDebug.debug("--- ON DESTROY ---");
937
938                 super.onDestroy();
939
940                 saved_state = null;
941
942                 doUnbindService();
943                 if (mAltosVoice != null) {
944                         mAltosVoice.stop();
945                         mAltosVoice = null;
946                 }
947                 stop_timer();
948         }
949
950         protected void onActivityResult(int requestCode, int resultCode, Intent data) {
951                 AltosDebug.debug("onActivityResult request %d result %d", requestCode, resultCode);
952                 switch (requestCode) {
953                 case REQUEST_CONNECT_DEVICE:
954                         // When DeviceListActivity returns with a device to connect to
955                         if (resultCode == Activity.RESULT_OK) {
956                                 connectDevice(data);
957                         }
958                         break;
959                 case REQUEST_ENABLE_BT:
960                         // When the request to enable Bluetooth returns
961                         if (resultCode == Activity.RESULT_OK) {
962                                 // Bluetooth is now enabled, so set up a chat session
963                                 //setupChat();
964                                 AltosDebug.debug("BT enabled");
965                                 bluetoothEnabled(data);
966                         } else {
967                                 // User did not enable Bluetooth or an error occured
968                                 AltosDebug.debug("BT not enabled");
969                         }
970                         break;
971                 case REQUEST_IDLE_MODE:
972                         if (resultCode == Activity.RESULT_OK)
973                                 idle_mode(data);
974                         break;
975                 case REQUEST_IGNITERS:
976                         break;
977                 case REQUEST_SETUP:
978                         if (resultCode == Activity.RESULT_OK)
979                                 note_setup_changes(data);
980                         break;
981                 case REQUEST_SELECT_TRACKER:
982                         if (resultCode == Activity.RESULT_OK)
983                                 select_tracker(data);
984                         break;
985                 case REQUEST_DELETE_TRACKER:
986                         if (resultCode == Activity.RESULT_OK)
987                                 delete_track(data);
988                         break;
989                 }
990         }
991
992         private void note_setup_changes(Intent data) {
993                 int changes = data.getIntExtra(SetupActivity.EXTRA_SETUP_CHANGES, 0);
994
995                 AltosDebug.debug("note_setup_changes changes %d\n", changes);
996
997                 if ((changes & SETUP_BAUD) != 0) {
998                         try {
999                                 mService.send(Message.obtain(null, TelemetryService.MSG_SETBAUD,
1000                                                              AltosPreferences.telemetry_rate(1)));
1001                         } catch (RemoteException re) {
1002                         }
1003                 }
1004                 if ((changes & SETUP_UNITS) != 0) {
1005                         /* nothing to do here */
1006                 }
1007                 if ((changes & SETUP_MAP_SOURCE) != 0) {
1008                         /* nothing to do here */
1009                 }
1010                 if ((changes & SETUP_MAP_TYPE) != 0) {
1011                         /* nothing to do here */
1012                 }
1013                 set_switch_time();
1014                 if ((changes & SETUP_FONT_SIZE) != 0) {
1015                         AltosDebug.debug(" ==== Recreate to switch font sizes ==== ");
1016                         finish();
1017                         startActivity(getIntent());
1018                 }
1019         }
1020
1021         private void connectUsb(UsbDevice device) {
1022                 if (mService == null)
1023                         pending_usb_device = device;
1024                 else {
1025                         // Attempt to connect to the device
1026                         try {
1027                                 mService.send(Message.obtain(null, TelemetryService.MSG_OPEN_USB, device));
1028                                 AltosDebug.debug("Sent OPEN_USB message");
1029                         } catch (RemoteException e) {
1030                                 AltosDebug.debug("connect device message failed");
1031                         }
1032                 }
1033         }
1034
1035         private void bluetoothEnabled(Intent data) {
1036                 if (mService != null) {
1037                         try {
1038                                 mService.send(Message.obtain(null, TelemetryService.MSG_BLUETOOTH_ENABLED, null));
1039                         } catch (RemoteException e) {
1040                                 AltosDebug.debug("send BT enabled message failed");
1041                         }
1042                 }
1043         }
1044
1045         private void connectDevice(Intent data) {
1046                 // Attempt to connect to the device
1047                 try {
1048                         String address = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
1049                         String name = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_NAME);
1050
1051                         AltosDebug.debug("Connecting to " + address + " " + name);
1052                         DeviceAddress   a = new DeviceAddress(address, name);
1053                         mService.send(Message.obtain(null, TelemetryService.MSG_CONNECT, a));
1054                         AltosDebug.debug("Sent connecting message");
1055                 } catch (RemoteException e) {
1056                         AltosDebug.debug("connect device message failed");
1057                 }
1058         }
1059
1060         private void disconnectDevice(boolean remember) {
1061                 try {
1062                         mService.send(Message.obtain(null, TelemetryService.MSG_DISCONNECT, (Boolean) remember));
1063                 } catch (RemoteException e) {
1064                 }
1065         }
1066
1067         private void idle_mode(Intent data) {
1068                 int type = data.getIntExtra(IdleModeActivity.EXTRA_IDLE_RESULT, -1);
1069                 Message msg;
1070
1071                 AltosDebug.debug("intent idle_mode %d", type);
1072                 switch (type) {
1073                 case IdleModeActivity.IDLE_MODE_CONNECT:
1074                         msg = Message.obtain(null, TelemetryService.MSG_MONITOR_IDLE_START);
1075                         try {
1076                                 mService.send(msg);
1077                         } catch (RemoteException re) {
1078                         }
1079                         break;
1080                 case IdleModeActivity.IDLE_MODE_DISCONNECT:
1081                         msg = Message.obtain(null, TelemetryService.MSG_MONITOR_IDLE_STOP);
1082                         try {
1083                                 mService.send(msg);
1084                         } catch (RemoteException re) {
1085                         }
1086                         break;
1087                 case IdleModeActivity.IDLE_MODE_REBOOT:
1088                         msg = Message.obtain(null, TelemetryService.MSG_REBOOT);
1089                         try {
1090                                 mService.send(msg);
1091                         } catch (RemoteException re) {
1092                         }
1093                         break;
1094                 case IdleModeActivity.IDLE_MODE_IGNITERS:
1095                         Intent serverIntent = new Intent(this, IgniterActivity.class);
1096                         startActivityForResult(serverIntent, REQUEST_IGNITERS);
1097                         break;
1098                 }
1099         }
1100
1101         boolean fail_shown;
1102
1103         private void file_failed(File file) {
1104                 if (!fail_shown) {
1105                         fail_shown = true;
1106                         AlertDialog fail = new AlertDialog.Builder(this).create();
1107                         fail.setTitle("Failed to Create Log File");
1108                         fail.setMessage(file.getPath());
1109                         fail.setButton(AlertDialog.BUTTON_NEUTRAL, "OK",
1110                                        new DialogInterface.OnClickListener() {
1111                                                public void onClick(DialogInterface dialog, int which) {
1112                                                        dialog.dismiss();
1113                                                }
1114                                        });
1115                         fail.show();
1116                 }
1117         }
1118
1119         @Override
1120         public boolean onCreateOptionsMenu(Menu menu) {
1121                 MenuInflater inflater = getMenuInflater();
1122                 inflater.inflate(R.menu.option_menu, menu);
1123                 return true;
1124         }
1125
1126         double telem_frequency = 434.550;
1127         double selected_frequency = AltosLib.MISSING;
1128
1129         void setFrequency(double freq) {
1130                 telem_frequency = freq;
1131                 selected_frequency = AltosLib.MISSING;
1132                 try {
1133                         mService.send(Message.obtain(null, TelemetryService.MSG_SETFREQUENCY, freq));
1134                         set_switch_time();
1135                 } catch (RemoteException e) {
1136                 }
1137         }
1138
1139         void setFrequency(AltosFrequency frequency) {
1140                 setFrequency (frequency.frequency);
1141         }
1142
1143         void setBaud(int baud) {
1144                 try {
1145                         mService.send(Message.obtain(null, TelemetryService.MSG_SETBAUD, baud));
1146                         set_switch_time();
1147                 } catch (RemoteException e) {
1148                 }
1149         }
1150
1151         void setBaud(String baud) {
1152                 try {
1153                         int     value = Integer.parseInt(baud);
1154                         int     rate = AltosLib.ao_telemetry_rate_38400;
1155                         switch (value) {
1156                         case 2400:
1157                                 rate = AltosLib.ao_telemetry_rate_2400;
1158                                 break;
1159                         case 9600:
1160                                 rate = AltosLib.ao_telemetry_rate_9600;
1161                                 break;
1162                         case 38400:
1163                                 rate = AltosLib.ao_telemetry_rate_38400;
1164                                 break;
1165                         }
1166                         setBaud(rate);
1167                 } catch (NumberFormatException e) {
1168                 }
1169         }
1170
1171         void select_tracker(int serial, double frequency) {
1172
1173                 AltosDebug.debug("select tracker %d %7.3f\n", serial, frequency);
1174
1175                 if (serial == selected_serial) {
1176                         AltosDebug.debug("%d already selected\n", serial);
1177                         return;
1178                 }
1179
1180                 if (serial != 0) {
1181                         int i;
1182                         for (i = 0; i < trackers.length; i++)
1183                                 if (trackers[i].serial == serial)
1184                                         break;
1185
1186                         if (i == trackers.length) {
1187                                 AltosDebug.debug("attempt to select unknown tracker %d\n", serial);
1188                                 return;
1189                         }
1190                         if (frequency != 0.0 && frequency != AltosLib.MISSING)
1191                                 setFrequency(frequency);
1192                 }
1193
1194                 selected_serial = serial;
1195                 update_state(null);
1196         }
1197
1198         void select_tracker(Intent data) {
1199                 int serial = data.getIntExtra(SelectTrackerActivity.EXTRA_SERIAL_NUMBER, 0);
1200                 double frequency = data.getDoubleExtra(SelectTrackerActivity.EXTRA_FREQUENCY, 0.0);
1201                 select_tracker(serial, frequency);
1202         }
1203
1204         void delete_track(int serial) {
1205                 try {
1206                         mService.send(Message.obtain(null, TelemetryService.MSG_DELETE_SERIAL, (Integer) serial));
1207                 } catch (Exception ex) {
1208                 }
1209         }
1210
1211         void delete_track(Intent data) {
1212                 int serial = data.getIntExtra(SelectTrackerActivity.EXTRA_SERIAL_NUMBER, 0);
1213                 if (serial != 0)
1214                         delete_track(serial);
1215         }
1216
1217         void start_select_tracker(Tracker[] select_trackers, int title_id, int request) {
1218                 Intent intent = new Intent(this, SelectTrackerActivity.class);
1219                 AltosDebug.debug("put title id 0x%x %s", title_id, getResources().getString(title_id));
1220                 intent.putExtra(EXTRA_TRACKERS_TITLE, title_id);
1221                 if (select_trackers != null) {
1222                         ArrayList<Tracker> tracker_array = new ArrayList<Tracker>(Arrays.asList(select_trackers));
1223                         intent.putParcelableArrayListExtra(EXTRA_TRACKERS, tracker_array);
1224                 } else {
1225                         intent.putExtra(EXTRA_TRACKERS, (Parcelable[]) null);
1226                 }
1227                 startActivityForResult(intent, request);
1228         }
1229
1230         void start_select_tracker(Tracker[] select_trackers) {
1231                 start_select_tracker(select_trackers, R.string.select_tracker, REQUEST_SELECT_TRACKER);
1232         }
1233
1234         void touch_trackers(Integer[] serials) {
1235                 Tracker[] my_trackers = new Tracker[serials.length];
1236
1237                 for (int i = 0; i < serials.length; i++) {
1238                         AltosState      s = telemetry_state.get(serials[i]);
1239                         my_trackers[i] = new Tracker(s);
1240                 }
1241                 start_select_tracker(my_trackers);
1242         }
1243
1244         @Override
1245         public boolean onOptionsItemSelected(MenuItem item) {
1246                 Intent serverIntent = null;
1247                 switch (item.getItemId()) {
1248                 case R.id.connect_scan:
1249                         ensureBluetooth();
1250                         // Launch the DeviceListActivity to see devices and do scan
1251                         serverIntent = new Intent(this, DeviceListActivity.class);
1252                         startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);
1253                         return true;
1254                 case R.id.disconnect:
1255                         /* Disconnect the device
1256                          */
1257                         disconnectDevice(false);
1258                         return true;
1259                 case R.id.quit:
1260                         AltosDebug.debug("R.id.quit");
1261                         disconnectDevice(true);
1262                         finish();
1263                         return true;
1264                 case R.id.setup:
1265                         serverIntent = new Intent(this, SetupActivity.class);
1266                         startActivityForResult(serverIntent, REQUEST_SETUP);
1267                         return true;
1268                 case R.id.select_freq:
1269                         // Set the TBT radio frequency
1270
1271                         final AltosFrequency[] frequencies = AltosPreferences.common_frequencies();
1272                         String[] frequency_strings = new String[frequencies.length];
1273                         for (int i = 0; i < frequencies.length; i++)
1274                                 frequency_strings[i] = frequencies[i].toString();
1275
1276                         AlertDialog.Builder builder_freq = new AlertDialog.Builder(this);
1277                         builder_freq.setTitle("Select Frequency");
1278                         builder_freq.setItems(frequency_strings,
1279                                          new DialogInterface.OnClickListener() {
1280                                                  public void onClick(DialogInterface dialog, int item) {
1281                                                          setFrequency(frequencies[item]);
1282                                                          selected_frequency = frequencies[item].frequency;
1283                                                  }
1284                                          });
1285                         AlertDialog alert_freq = builder_freq.create();
1286                         alert_freq.show();
1287                         return true;
1288                 case R.id.select_tracker:
1289                         start_select_tracker(trackers);
1290                         return true;
1291                 case R.id.delete_track:
1292                         if (trackers != null && trackers.length > 0)
1293                                 start_select_tracker(trackers, R.string.delete_track, REQUEST_DELETE_TRACKER);
1294                         return true;
1295                 case R.id.idle_mode:
1296                         serverIntent = new Intent(this, IdleModeActivity.class);
1297                         serverIntent.putExtra(EXTRA_IDLE_MODE, idle_mode);
1298                         serverIntent.putExtra(EXTRA_FREQUENCY, telem_frequency);
1299                         startActivityForResult(serverIntent, REQUEST_IDLE_MODE);
1300                         return true;
1301                 }
1302                 return false;
1303         }
1304
1305         static String direction(AltosGreatCircle from_receiver,
1306                                 Location receiver) {
1307                 if (from_receiver == null)
1308                         return null;
1309
1310                 if (receiver == null)
1311                         return null;
1312
1313                 if (!receiver.hasBearing())
1314                         return null;
1315
1316                 float   bearing = receiver.getBearing();
1317                 float   heading = (float) from_receiver.bearing - bearing;
1318
1319                 while (heading <= -180.0f)
1320                         heading += 360.0f;
1321                 while (heading > 180.0f)
1322                         heading -= 360.0f;
1323
1324                 int iheading = (int) (heading + 0.5f);
1325
1326                 if (-1 < iheading && iheading < 1)
1327                         return "ahead";
1328                 else if (iheading < -179 || 179 < iheading)
1329                         return "backwards";
1330                 else if (iheading < 0)
1331                         return String.format("left %d°", -iheading);
1332                 else
1333                         return String.format("right %d°", iheading);
1334         }
1335
1336         public void onLocationChanged(Location location) {
1337                 this.location = location;
1338                 AltosDebug.debug("Location changed to %f,%f",
1339                                  location.getLatitude(),
1340                                  location.getLongitude());
1341                 update_ui(telemetry_state, state, false);
1342         }
1343
1344         public void onStatusChanged(String provider, int status, Bundle extras) {
1345                 AltosDebug.debug("Location status now %d\n", status);
1346         }
1347
1348         public void onProviderEnabled(String provider) {
1349                 AltosDebug.debug("Location provider enabled %s\n", provider);
1350         }
1351
1352         public void onProviderDisabled(String provider) {
1353                 AltosDebug.debug("Location provider disabled %s\n", provider);
1354         }
1355 }