package com.algobase.service;

import java.lang.reflect.Method;

import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Date;
import java.util.TimeZone;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Locale;
import java.util.Set;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Comparator;
import java.util.Iterator;
import java.util.UUID;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FileFilter;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.FileOutputStream;
import java.io.StringWriter;
import java.io.PrintWriter;

import org.json.JSONObject;

import android.provider.Settings;

import android.text.Html;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.RelativeSizeSpan;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.NotificationChannel;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.app.PendingIntent;
import android.app.Service;

import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ComponentName;
import android.content.res.Resources;
import android.content.res.Configuration;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.content.SharedPreferences;
import android.content.BroadcastReceiver;
import android.content.ServiceConnection;

import android.view.View;

import android.net.Uri;
import android.net.ConnectivityManager;

import android.net.Network;
import android.net.NetworkCapabilities;

import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;

import android.location.GnssStatus;
import android.location.GpsStatus;
import android.location.OnNmeaMessageListener;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;

import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.hardware.usb.UsbManager;
import android.hardware.usb.UsbDevice;


import android.os.Build;
import android.os.Environment;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Looper;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.BatteryManager;
import android.os.Vibrator;
import android.os.CountDownTimer;
import android.os.Process;


import android.media.Ringtone;
import android.media.RingtoneManager;
import android.media.MediaPlayer;
import android.media.AudioManager;

import android.util.Log;
import android.widget.Toast;
import android.widget.RemoteViews;


import android.speech.tts.TextToSpeech;
import android.speech.tts.Voice;
import android.speech.tts.UtteranceProgressListener;

import com.algobase.gpx.GpsReader;
import com.algobase.gpx.TrkReader;
import com.algobase.ascent.TotalAscent;
import com.algobase.stracks.sTracksActivity;

import com.algobase.share.system.*;
import com.algobase.share.compat.*;
import com.algobase.share.network.*;
import com.algobase.share.geo.*;
import com.algobase.share.dialog.*;
import com.algobase.share.app.*;
import com.algobase.share.bluetooth.*;

import com.algobase.share1.ant.*;

import androidx.core.content.FileProvider;

import com.algobase.stracks.R;



public class DataService extends Service {

  public static final String UPDATE_TRACKPOINT = "STRACKS_UPDATE_TRACKPOINT";
  public static final String UPDATE_DEVICE_MESSAGE = "STRACKS_UPDATE_DEVICE_MESSAGE";
  public static final String UPDATE_PROVIDER = "STRACKS_UPDATE_PROVIDER";


  static final double DISTANCE_UNDEFINED = sTracksActivity.DISTANCE_UNDEFINED;

  static final float GPS_EPS   = TotalAscent.ASCENT_EPS[0];
  static final float SRTM3_EPS = TotalAscent.ASCENT_EPS[1];
  static final float BARO_EPS  = TotalAscent.ASCENT_EPS[2];

  static final String CHANNEL1_ID = "sTracks_Channel_ID";
  static final String CHANNEL1_NAME = "sTracks Data Service";

  static final String CHANNEL2_ID = "sTracks_Popup_Channel_ID";
  static final String CHANNEL2_NAME = "sTracks Popup Data Service";

  static final int NOTIFICATION_ID1 = 9661;
  static final int NOTIFICATION_ID2 = 9662;

  static final int BT_CONNECT_LIMIT = 5;

  static final int CALIBRATION_DELTA_MAX = 25;


  int trim_memory_level = 0;

  String string_gps_available = "GPS signal available.";
  String string_gps_waiting = "Waiting for GPS signal.";

  String string_gps_deactivated =
                      "Location is disabled.\nPlease activate it.";


  String server_host = sTracksActivity.server_host_list[0];
  int server_port = sTracksActivity.server_port;
  int tracking_port = sTracksActivity.tracking_port;

  CountDownTimer cdt = null;
  boolean cdt_enabled = true;

  Notification.Builder notificationBuilder;
  NotificationManager notificationManager; 

  AudioManager am;
  MediaPlayer mp;

  int sound_playing_count = 0;

  int stream_type_media = 0;
  int stream_volume_media = 0;

  int stream_type_notify = 0;
  int stream_volume_notify = 0;

  TextToSpeech tts = null;

  long  locationMinTime = 1000;
//float locationMinDist = 1.0f;
  float locationMinDist = 0.0f; 


  LocationManager locationManager;
  LocationListener locationListener;

  OnNmeaMessageListener nmea_msg_listener;

  GnssStatus.Callback gnssStatusCallback;

  GpsStatus.Listener gpsStatusListener;
  GpsStatus.NmeaListener nmea_listener;

  Location current_loc;
  long current_time;
  long stop_time;

  Location last_known_loc;

  float current_speed = 0;
  float current_heading = 0;

  long total_break = 0;
  long current_break = 0;
  long break_limit = 10000;

  int current_hrate = 0;
  int sum_hrate = 0;

//int current_power = -1;
  int current_power = 0;

  int start_crank_revolutions = -1;
  int current_crank_revolutions = 0;
  int current_crank_revolution_t = 0;

  long start_wheel_revolutions = -1;
  long current_wheel_revolutions = 0;
  int current_wheel_revolution_t = 0;

  int last_accumulated_torque = -1;

  float current_torque = 0;
  float current_wheel_speed = 0;
  float current_cadence = 0;
  float current_wheel_dist = 0;

  float current_temp = 0;

  Ant ant;

  int ant_restart_timeout = -1;
  int ant_restart_count = 0;

  String  bt_hrate_connect_address = "";
  String  bt_hrate_connect_name = "";
  boolean bt_hrate_auto_connect = true;
  Bluetooth bt_hrt = null;

  String  bt_power_connect_address = "";
  String  bt_power_connect_name = "";
  boolean bt_power_auto_connect = true;
  Bluetooth bt_pwr = null;

  String  bt_cadence_connect_address = "";
  boolean bt_cadence_auto_connect = true;
  Bluetooth bt_cad = null;

  String  bt_temp_connect_address = "";
  boolean bt_temp_auto_connect = true;
  Bluetooth bt_tmp = null;

  String  bt_fitness_connect_address = "";
  boolean bt_fitness_auto_connect = true;
  Bluetooth bt_fit = null;


  Bluetooth bt_any = null;

  String ant_hrate_connect_name = "";
  String ant_power_connect_name = "";
  String ant_cadence_connect_name = "";
  String ant_temp_connect_name = "";


  BroadcastReceiver battery_receiver;
  BroadcastReceiver connectivity_receiver;
  BroadcastReceiver screen_state_receiver;

  SharedPreferences prefs;

  String[] sound_uri;

  float battery_level = -1;

  boolean wifi_connected = false;
  boolean mobile_connected = false;
  boolean network_connected = false;

  boolean location_permission = false;

  Timer timer;
  
  Bitmap icon;
  Bitmap gps_icon_off;
  Bitmap gps_icon_on;

//float geoid_correction = 0.0f;
  float geoid_correction = 48.0f; 

  float accuracy_filter = 50.0f;

  float ascent_eps = 1.0f;
  float point_mindist = 5.0f;

  double[] total_ascent_limit = { 1000, 1500, 2000, 100000 };
  int total_ascent_limit_index = 0;

  int total_ascent_notify_limit = 0;
  int total_ascent_notify_interval = 100; // m

  int total_distance_notify_limit = 0;
  int total_distance_notify_interval = 10; // km


  boolean use_srtm3_altitude = true;
  boolean use_barometer_altitude = true;


  boolean indoor_mode = false;
  float   indoor_fixed_speed = 0;

  Sensor barometer = null;

  float baro_std_nn = 1013.25f;

//float baro_pressure_nn = 0;
  float baro_pressure_nn = baro_std_nn;

  float baro_pressure = 0;
  float baro_pressure_start = 0;

  double baro_alt_start = 0;

// barometer averaging (SENSOR_DELAY_NORMAL: 100 msec ?)
//float[] baro_values = new float[50]; // 5 sec
  float[] baro_values = new float[80]; // 8 sec (2.226) 

  float baro_sum = 0;
  int baro_count = 0;

//float[] power_values = new float[5]; 
  float[] power_values = new float[7]; 
  float power_sum = 0;
  int power_count = 0;

  float[] cadence_values = new float[5]; 
  float cadence_sum = 0;
  int cadence_count = 0;

  float[] torque_values = new float[5]; 
  float torque_sum = 0;
  int torque_count = 0;


  PowerManager.WakeLock wake_lock;

  FileWriter gps_writer;
  FileWriter log_writer;
  FileWriter bt_log_writer;


  File  stracks_folder = null;
  File  gps_folder = null;
  File  wp_folder = null;
  File  log_folder = null;
  File  tmp_folder = null;

  File  srtm3_folder = null;

  //String xserver_host_def = "xserver.algobase.com";

  String xserver_host_def = sTracksActivity.xserver_host;
  String xserver_host = xserver_host_def;

  String srtm3_xs_path = sTracksActivity.srtm3_xs_path;

  srtm3Matrix srtm3_matrix = null;

  File  current_gps_file =  null;
  File  current_crs_file =  null;

  File  log_file =  null;
  File  log_file_save =  null;

  File  bt_log_file =  null;
  File  bt_log_file_save =  null;


// have to be restored after re-start !

  String track_name = "";
  String track_name_long = "";

  long track_begin_time = 0;

  int   num_points = 0;
  int   num_sent_points = 0;

  double total_dist;

  int   last_calibration_point = 0;

  TotalAscent totalAsc = new TotalAscent();

  WayPoint[] waypoints = null;

  int tick_count = 0;

  int gps_count = 0;
  int gps_signal_lost_count = 0;

  int srtm3_update_count = 0;


  float client_version;
  Location last_loc;
  Location last_tracking_loc;
  long last_tracking_t;
  float last_dist;

  String lang;

  String user_name = "";
  String tracking_name = "";

  boolean live_tracking = false;
  boolean mock_locations = false;

  int   lap_auto_time = 60;  // min
  int   lap_auto_dist = 10;  // km
  int   lap_auto_mode = 0;

  int    lap_num;
  long   lap_time;
  double lap_dist;

  int acoustic_signals_volume = 85;

  int ant_hrate_connect_count = 0;
  int ant_power_connect_count = 0;
  int ant_cadence_connect_count = 0;
  int ant_temp_connect_count = 0;

  long hrate_last_update_time = 0;
  long power_last_update_time = 0;
  long cadence_last_update_time = 0;
  long temp_last_update_time = 0;

  int user_hrate_limit = 180;
  int hrate_limit_low  = user_hrate_limit - 10;
  int hrate_limit = user_hrate_limit;
  int hrate_max = -1;

  boolean gps_enabled = true;
  boolean gps_available = false;

  int num_satellites = 0;
  int used_satellites = 0;

  boolean connected = false;

  Location calibration_loc = null;
  int calibration_count = 0;
  double calibration_delta = 0;

  double srtm3_calibration_dist = 1.0;
  Location srtm3_closest = null;

  double wp_calibration_dist = 30.0;

  Location home_loc = null;
  Location wp_loc = null;
  String wp_name = null;
  int wp_find_count = 0;

  LocationBuffer location_buffer;

  Handler handler =  new Handler(Looper.getMainLooper());

  Assets assets;

  // course

  Course   course = null;
  Location course_current_loc = null;
  boolean  course_on_track = false;
  double   course_current_dst = DISTANCE_UNDEFINED;
  int      course_current_p = -1;
  int      course_lost_count = 0;

  int course_dist_i = 0;

  int    COURSE_LOST_LIMIT = 5;
  double COURSE_LOST_DIST = 50;
  double COURSE_FOUND_DIST = 20;


  // connection to wearables

/*
  Sender sender;
*/


  double totalAscent()
  { if (use_barometer_altitude)
      return totalAsc.getAscent(2);
    else
      if (use_srtm3_altitude)
        return totalAsc.getAscent(1);
      else
        return totalAsc.getAscent(0);
  }


/*
  @SuppressWarnings("deprecation")
  boolean isServiceRunning(String name)
  {
    ActivityManager manager = 
        (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);

    List<RunningServiceInfo> L = manager.getRunningServices(1000);
   
    boolean service_running = false;
   
    for (RunningServiceInfo rsi : L) { 
      if (name.equals(rsi.service.getClassName())) {
        service_running = true;
        break;
      }
    }

    return service_running;
  }
*/


/*
  @SuppressWarnings("deprecation")
  public boolean isForegroundService() {
    Context context = getBaseContext();
    ActivityManager manager =
         (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    boolean fg = false;
    List<RunningServiceInfo> L = manager.getRunningServices(Integer.MAX_VALUE);
    for (RunningServiceInfo rsi : L) {
        String class_name = rsi.service.getClassName();
        if (class_name.equals(DataService.class.getName())) {  
            fg = rsi.foreground;
        }
    }
    return fg;
 }
*/


  // reflection methods

  boolean addNmeaListenerReflection(GpsStatus.NmeaListener listener)
  { boolean result = false;
    try {
      Method addNmeaListener =
              LocationManager.class.getMethod("addNmeaListener", 
                                               GpsStatus.NmeaListener.class);
      Object obj = addNmeaListener.invoke(locationManager,listener);
      result = (Boolean)obj;
    } catch (Exception ex) {}
    return result;
  }

  void removeNmeaListenerReflection(GpsStatus.NmeaListener listener)
  { try {
      Method removeNmeaListener =
              LocationManager.class.getMethod("removeNmeaListener", 
                                               GpsStatus.NmeaListener.class);
      removeNmeaListener.invoke(locationManager,listener);
    } catch (Exception ex) {}
  }



  private class MySensorEventListener implements SensorEventListener {
  
  @Override
  public void onAccuracyChanged(Sensor sensor, int acc) { }
  
   @Override
   public void onSensorChanged(SensorEvent event) 
   { if (event.sensor.getType() == Sensor.TYPE_PRESSURE) {
       update_pressure(event.values[0],event.accuracy);
     }
   }
  
  }

  private class MyLocationListener implements LocationListener {

       //String provider_status = "unknown";

       @Override
       public void onLocationChanged(Location loc) { 

          if (indoor_mode) {
            // ignore gps locations in indoor mode
            return;
          }

          if (loc.getLongitude() == 0 || loc.getLatitude() == 0) {
            // ignore invalid locations (necessary ?)
            log_title("LOCATION IGNORED (invalid)");
            return;
          }

          update_gps_status(true,"onLocationChanged");

          // ignore locations with bad accuracy

          float acc = loc.getAccuracy();
          if (acc > accuracy_filter) {
            synchronized(this)  {
              log_title(format("LOCATION IGNORED (%.1f)",acc));
              log(format("lon = %.7f",loc.getLongitude()));
              log(format("lat = %.7f",loc.getLatitude()));
              log(format("alt = %.1f",loc.getAltitude()));
            }
            return; 
          }

          //if (!use_barometer_altitude || baro_pressure > 0) 
          gps_count++;

          // set time to system time
          long t = get_current_time_millis();
          loc.setTime(t);

          double alt = loc.getAltitude();

          mock_locations = loc.isFromMockProvider();

          if (mock_locations)
          { // simulate barometer
            double p = baro_std_nn * Math.pow(1.0 - alt/44330.0, 5.255);
            baro_pressure = (float)p;
           }
          else 
          { // geoid correction
            loc.setAltitude(alt-geoid_correction);
           }

          current_loc = loc;
       }


       @Override
       public void onProviderDisabled(String provider) { 
          update_gps_status(false,provider);
       }


       @Override
       public void onProviderEnabled(String provider) {
         log_title(provider + " enabled.");
       }

       @Override
       public void onStatusChanged(String provider, int status, Bundle extras) {
         log_title(provider + " status changed.");
       }


  }



  long get_current_time_millis() { 
    return Calendar.getInstance().getTimeInMillis(); 
  }

  Date get_current_time() { 
    return Calendar.getInstance().getTime(); 
  }


  String current_date_and_time()
  { Date d = get_current_time();
    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(d);
   }


  long total_time()
  { long t = current_time - track_begin_time - total_break;
    if (current_break >= break_limit) t -= current_break;
    if (t < 0) {
/*
      // should not happen
      log("");
      log("t = " + t);
      log("current_time:  " + current_time);
      log("track_begin:   " + track_begin_time);
      log("current_break: " + current_break);
      log("total_break:   " + total_break);
*/
      t = 0; 
    }
    return t;
  }

  long break_time()
  { long t = total_break;
    if (current_break >= break_limit) t += current_break;
    return t;
   }


  String format(String pattern, Object... args) {
     return String.format(Locale.US,pattern,args); 
  }
      

  void bt_log(String msg) {
    writeLog(bt_log_writer,msg);
  }

  void log(String msg)  
  { Log.v("sTrackService",msg); 
    writeLog(log_writer,msg);
   }

  void log_title(String s)  
  { Date d = get_current_time();
    String t = new SimpleDateFormat("HH:mm:ss").format(d);
    log(t + " " + s);
   }

  void log_exception(Exception ex)
  { StringWriter stringWriter = new StringWriter();
    PrintWriter printWriter = new PrintWriter(stringWriter);
    ex.printStackTrace(printWriter);
    printWriter.close();
    writeLog(log_writer,stringWriter.toString());
  }



  void writeLog(FileWriter writer, String msg) 
  { if (writer == null) return;
    try
    { writer.write(msg,0,msg.length());
      writer.write('\n');
      writer.flush();
    } catch(IOException e){ }

  }


  public void copy_file(File source, File target)
  {
    try {
      FileInputStream in  = new FileInputStream(source);
      FileOutputStream out = new FileOutputStream(target);
      byte[] buffer = new byte[1024];
      int bytes;
      while((bytes = in.read(buffer)) != -1){
        out.write(buffer, 0, bytes);
      }
     in.close();
     out.close();
   }
   catch(IOException e) { log("copy_file: " + e.toString()); }
  }




/*
  void play_media(int r_sound, int msec, float vol)
  { 
    MediaPlayer player = MediaPlayer.create(this,r_sound);

    try {
      player.prepare();
    } catch(Exception e) { log("play_media: " + e.toString()); }

    //player.setAudioStreamType(AudioManager.STREAM_NOTIFICATION);
    player.setAudioStreamType(AudioManager.STREAM_MUSIC);

    player.setVolume(vol,vol);
    if (msec > 0)
    { int p = player.getDuration()-(msec + 700);
      if (p < 0) p = 0;
      player.seekTo(p);
    }
    player.start();
  }
*/

  void stop_sound() { 
    if (mp == null) return; 
    if (!mp.isPlaying()) return;
    mp.stop();
    mp.release();
    mp = null;
    sound_playing_count = 0;
  }

  void play_sound(int i) { 
    if (i < 0) return;
    log("");
    log_title("SOUND: " + sTracksActivity.sound_title[i]);
    play_sound(sound_uri[i]);
  }

  void play_sound(String sound_uri_str)
  {
    // scale volume

    int max_vol = am.getStreamMaxVolume(stream_type_media); 
    double f = Math.sqrt(acoustic_signals_volume)/10;
    int vol = (int)(f*max_vol);

    if (sound_uri_str.startsWith("tts:")) log_title("play_sound");
    log("uri = " + sound_uri_str);
    log("vol = " + vol + " / " + max_vol);

    if (vol == 0) return;
    if (sound_uri_str.equals("silent")) return;
    if (sound_uri_str.equals("")) return;

    if (am.getRingerMode() == AudioManager.RINGER_MODE_SILENT)
    { showToast("RingerMode: Silent");
      return;
     }


    if (sound_uri_str.startsWith("tts:") && sound_uri_str.length() > 4)
    { String txt = sound_uri_str.substring(4);
      int p = txt.indexOf(":");
      if (p == -1)
         speak(txt,"",vol);
      else
         speak(txt.substring(0,p), txt.substring(p+1),vol);
      return;
     }

    String pkg_name = getPackageName();

    String sound_file = null;

    if (sound_uri_str.equals("beep")) {
      sound_file = "beep1000.mp3";
      //vol = (int)(0.6f*vol);
      vol = (int)(0.5f*vol);
    }
    else
    if (sound_uri_str.equals("ecg")) {
      sound_file = "ecg3500.mp3";
      vol = (int)(0.7f*vol);
    }
    else
    if (sound_uri_str.equals("theetone")) {
      sound_file = "theetone.mp3";
      vol = (int)(0.7f*vol);
    }
    else
    if (sound_uri_str.equals("soft_bell")) {
      sound_file = "soft_bell.mp3";
      vol = (int)(0.7f*vol);
    }
    else
    if (sound_uri_str.equals("jodel")) sound_file = "jodel.mp3";
    else
    if (sound_uri_str.equals("hawkcall")) sound_file = "hawkcall.mp3";
    else
    if (sound_uri_str.equals("cowbell")) sound_file = "cowbell.mp3";
    else
    if (sound_uri_str.equals("arcturus")) sound_file = "arcturus.ogg";


    Uri uri = null;

    if (sound_file != null)
    { // play assets/audio/soundfile
      File tmp = new File(tmp_folder,sound_file);
      assets.copyFile("audio/" + sound_file, tmp);
      uri = FileProvider.getUriForFile(this,pkg_name,tmp);
     }
    else
      if (sound_uri_str.equals("default"))
        uri = Settings.System.DEFAULT_NOTIFICATION_URI;
      else
        uri = Uri.parse(sound_uri_str);

/*
    MediaPlayer mp = MediaPlayer.create(this,uri);
*/

    mp = new MediaPlayer();

    try { 
      mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
      mp.setDataSource(this,uri);
      mp.prepare();
    } catch (Exception ex) { 
         log("MediaPlayer Exception"); 
         log(ex.toString()); 
         showToast("Cannot play " + sound_uri_str);
         return;
      }

    am.setStreamVolume(stream_type_media,vol,0);

    mp.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
        @Override
        public void onCompletion(MediaPlayer mp) {
            if (--sound_playing_count == 0)
            { //showToast("Reset Volume: " + stream_volume_media);
              am.setStreamVolume(stream_type_media,stream_volume_media,0);
             }
        }
    });

    sound_playing_count++;
    mp.start();

  }


  void vibrate(int msec)
  { Vibrator vib = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE);
    vib.vibrate(msec);
   }


  public void showToast(String msg)
  { final MyToast toast = new MyToast(getBaseContext(),msg);
    toast.setTextColor(Color.WHITE);
    toast.setBackgroundColor(0xff777777);
    toast.setBorderColor(0xffffffff);
    handler.post(new Runnable() {
       public void run() { toast.show(); }
    });
  }


  void baro_calibrate(String method, double alt, Location cali_loc, double dst,
                                                                    float acc)
  { //log("baro_calibrate");

    double current_alt = baro_altitude();

    log("");
    log_title(format("CALIBRATION %d: %s",calibration_count+1,method));
    log(format("current_alt = %.2f",current_alt));

    if (alt == DISTANCE_UNDEFINED) {
      log(format("NO CALIBRATION(%s): alt = undefined",method));
      return;
    }

    if (acc > 100) {
      log(format("NO CALIBRATION(%s): acc = %.0f",method,acc));
      return;
    }

    if (calibration_loc != null)
    { // not first calibration
      if (cali_loc != null && cali_loc.distanceTo(calibration_loc) < 3) {
        // too close to previous calibration location
        return;
      }
    }

/*
    // this should not happen
    if (num_points > 0  && current_alt == DISTANCE_UNDEFINED) {
      showToast("BARO ALTITUDE UNDEFINED");
    }
*/

    double delta = alt - current_alt;

    // limit delta to CALIBRATION_DELTA_MAX (when recording)

    if (num_points > 0 && current_alt != DISTANCE_UNDEFINED 
                       && Math.abs(delta) > CALIBRATION_DELTA_MAX)
    { log("delta = " + delta);
      log("|delta| > DELTA_MAX (" + CALIBRATION_DELTA_MAX + ")");
      if (delta > 0)
        delta = +CALIBRATION_DELTA_MAX;
      else
        delta = -CALIBRATION_DELTA_MAX;
      alt = current_alt + delta;
    }


   // actual calibration

   double p = baro_pressure/Math.pow(1.0 - alt/44330.0, 5.255);
   baro_pressure_nn = (float)p;
   calibration_count++;

   if (cali_loc != null) calibration_loc = cali_loc;

   synchronized(this) 
   { log("method = " + method);
     log("gps_count  = " + gps_count);
     log("num_points = " + num_points);
     if (cali_loc == null)
       log("location  = null");
     else
     { 
       log(format("lngitude: %.6f ",cali_loc.getLongitude()));
       log(format("latitude: %.6f ",cali_loc.getLatitude()));
      }
     log(format("altitude:  %.1f", alt));
     log(format("accuracy:  %.1f" ,acc));
     log(format("distance:  %.1f", dst));
     log(format("pressure:  %.1f",baro_pressure));
     log(format("delta:     %.1f", delta));
     log(format("calibrate: %.1f ---> %.1f", current_alt,alt));
    }

   if (!method.equals("GPS") || gps_count == 1) {
      showToast(format("%s  %.1f m", method, baro_altitude()));
   }

   if (num_points > 0 && current_alt != DISTANCE_UNDEFINED 
                      && !method.equals("GPS"))
   { // srtm3 or wp calibration during ride
     write_gps_line(format("#calibration %.1f",delta));
     calibration_delta += delta;
     recompute_ascent();
    }

  }



  double baro_altitude()
  { 
    if (indoor_mode) {
        return DISTANCE_UNDEFINED;
    }
 
    if (mock_locations) {
      if (current_loc != null)
        return current_loc.getAltitude();
      else
        return DISTANCE_UNDEFINED;
    }

    if (baro_pressure == 0) { 
      //log("baro_altitude: PRESSURE = 0");
      return DISTANCE_UNDEFINED;
    }

  //if (baro_pressure_nn == 0)
    if (calibration_count == 0)  {
      //log_title("baro_altitude: NOT CALIBRATED");
      return DISTANCE_UNDEFINED;
    }

  //return SensorManager.getAltitude(baro_pressure_nn,baro_pressure);

/*
    double k = -100*baro_std_nn/(1.29*9.81);
    double hPa = baro_pressure + (baro_std_nn - baro_pressure_nn);
    return k*Math.log(hPa/baro_std_nn);
*/
    return 44330*(1.0 - Math.pow(baro_pressure/baro_pressure_nn, 1.0/5.255));
  }



  void track_begin(String name)
  { 
    log_title("track_begin");

    track_name_long = name;

    if (name.length() > 16) 
      track_name = name.substring(0,16);
    else
      track_name = name;

    //if (mock_locations) showToast("Mock Locations");

    lap_num = 0;
    lap_time = 0;
    lap_dist = 0;

    num_points = 0;
    num_sent_points = 0;

    // open file for writing (overwrite existing file)

    if (gps_writer != null)  {
      try { 
        gps_writer.close();
      } catch (IOException e) { log("track_begin: " + e.toString()); }
    }

    gps_writer = null;

    try { 
      gps_writer = new FileWriter(current_gps_file);
    } catch (IOException e) { log("track_begin: " + e.toString()); }


    write_gps_line(track_name_long);

    if (gps_enabled)
       write_gps_line("gps:enabled");
    else
       write_gps_line("gps:disabled");

    write_gps_line(
 "# t lon lat alt acc gps_alt srtm3_alt baro_alt dst spd prs hrt pwr cad tmp");

    current_time = get_current_time_millis();
    track_begin_time = current_time;

    current_break = 0;
    total_break= 0;
    total_dist = 0;
    last_loc = null;

    totalAsc.reset();

    total_ascent_limit_index = 0;
    total_ascent_notify_limit = total_ascent_notify_interval;

    total_distance_notify_limit = total_distance_notify_interval;

    send_tracking_cmd("trk_begin " + track_name_long);

  }

  void track_resume()
  { 
    log_title("RESUME: " + track_name);
    log("");

    //showToast("resume " + track_name);

    // send gps status
    String msg = gps_available ? "available" : "unavailable";
    update_device_message("gps", "", msg, "resume");

    // send sensor status

    if (ant != null) ant.updateStatus();
    
    if (!bt_hrt.getDeviceName().equals(""))
      update_device_message("bt_hrate",bt_hrt.getDeviceName(),"available",
                                       bt_hrt.getDeviceAddress());

    if (!bt_pwr.getDeviceName().equals(""))
      update_device_message("bt_power",bt_pwr.getDeviceName(), "available",
                                       bt_pwr.getDeviceAddress());

    if (!bt_cad.getDeviceName().equals(""))
      update_device_message("bt_cadence",bt_cad.getDeviceName(),"available",
                                         bt_cad.getDeviceAddress());

// not necessary if file is still open ?

    //(re-)open file for appending

    if (gps_writer != null) {
      try { 
        gps_writer.close();
      } catch (IOException e) { log("track_resume: " + e.toString()); }
    }

    gps_writer = null;

    try { 
      gps_writer = new FileWriter(current_gps_file,true);
    } catch (IOException e) { log("track_resume: " + e.toString()); }

/*
    if (gps_writer == null) {
      if (current_gps_file.exists())
        showToast("Resume: Not recording."); 
      else
        showToast("Resume: Missing gps file."); 
      try { 
        gps_writer = new FileWriter(current_gps_file,true);
      } catch (IOException e) { log("track_resume: " + e.toString()); }
    }
*/

  }


  void track_start()
  { 
    if (gps_writer == null) 
    { if (!current_gps_file.exists())
        showToast("GPS FILE NOT EXISTING");
      else
        try { 
          gps_writer = new FileWriter(current_gps_file,true);
        } catch (IOException e) { log("ERROR start: " + e.toString()); }
    }

    stop_time = 0;

    location_buffer.clear();
    srtm3_matrix.clearBuffer();
    current_loc = null;
    write_gps_line("#start " + current_time);
    sum_hrate = 0;
    update_notification();
  }

  void track_stop()
  { stop_time = current_time;
    write_gps_line("#stop  " + current_time);
    if (gps_writer != null) {
      try { gps_writer.close(); } catch(Exception ex) {}
    }
    gps_writer = null;
    update_notification();
   }



  void track_finish()
  { 
    long tt = total_time();
    long bt = break_time();

    float baro_pressure_diff = baro_pressure - baro_pressure_start;
    double baro_alt_diff = baro_altitude() - baro_alt_start;

    log("");
    log_title(track_name_long);
    log("");
    log("GPS   Count   = " + gps_count);
    log("Point Number  = " + num_points);
    log("Total Time    = " + time_to_hms(tt));
    log("Break Time    = " + time_to_hms(bt));
    log("Distance      = " + format("%.1f km", total_dist/1000));
    log("");
    log(format("Ascent[GPS]  = %.1f m (%.2f)",totalAsc.getAscent(0),GPS_EPS));
    log(format("Ascent[SRTM3]= %.1f m (%.2f)",totalAsc.getAscent(1),SRTM3_EPS));
    log(format("Ascent[BARO] = %.1f m (%.2f)",totalAsc.getAscent(2),BARO_EPS));

    log("");
    log("Calibration Summary");
    log(format("calibration_count = %d",  calibration_count));
    log(format("calibration_delta = %.1f m",calibration_delta));
    log(format("baro_diff = %.1f hPa / %.1f m",baro_pressure_diff,baro_alt_diff));
    log("");

    if (gps_writer == null) {
      log("track_finsh: gps_writer = null");
      //return;
    }

   if (current_gps_file.exists())
   { 
     String name = "track_save";

     try {
        FileReader freader = new FileReader(current_gps_file);
        BufferedReader breader = new BufferedReader(freader);
        name = breader.readLine();
     } catch (IOException e) { log(e.toString()); }

     name = name.trim();

     // remove/rename current gps file ("current_track.gps")

     File trk_gps = new File(gps_folder, name + ".gps");
     current_gps_file.renameTo(trk_gps); 


     // copy log file
     File trk_log = new File(gps_folder, name + ".log");
     copy_file(log_file,trk_log);


     // keep at most 50 files

     File[] files = gps_folder.listFiles();

     Comparator<File> cmp = new Comparator<File>() {
                               @Override
                               public int compare(File f1, File f2)
                               { String name1 = f1.getName();
                                 String name2 = f2.getName();
                                 return name1.compareTo(name2);
                               }
                             };

     Arrays.sort(files,cmp);

     int num = files.length - 50;

     for(int i=0; i<num; i++) {
       if (files[i].getName().startsWith("current_track")) continue;
       files[i].delete();
      }

   }


   send_tracking_cmd("trk_finish");

   track_name_long = "";
   track_name = "";
   gps_writer = null;

   update_wearable();
  }


  void write_gps_line(String s)
  { if (gps_writer == null) return;
    try { gps_writer.write(s,0,s.length());
          gps_writer.write('\n');
          gps_writer.flush();
    } catch (IOException e) { log("write_gps_line: " + e.toString()); }
  }



  void write_data(long t, double lon, double lat, double alt, float acc,
                  double gps_alt, double srtm3_alt, double baro_alt,
                  double dst, float spd, 
                  float prs, int hrt, int pwr, float cad, float tmp)
  {
    // write data line
    // t lon lat alt acc gps_alt srtm3_alt baro_alt dst spd prs hrt pwr cad tmp

/*
   if (barometer != null && baro_alt == DISTANCE_UNDEFINED)
     log_title("write: baro_alt = " + baro_alt);

   if (use_srtm3_altitude && srtm3_alt == DISTANCE_UNDEFINED)
      log_title("write: srtm3_alt = " + srtm3_alt);
*/

    // lat/lon precision: 0.6f:  11 cm

    try { 
     String line = format(
     "%d %.6f %.6f %.1f %.1f %.1f %.1f %.1f %.2f %.2f %.1f %d %d %.1f %.1f",
     t,lon,lat,alt,acc,gps_alt,srtm3_alt,baro_alt,dst,spd,prs,hrt,pwr,cad,tmp);

      write_gps_line(line);
    } catch (Exception e) { log("write_data: " + e.toString()); }

  }


  void send_tracking_cmd(final String cmd)
  { 
    if (!network_connected) return;
    if (!live_tracking) return;
    if (tracking_name.equals("")) return;

    synchronized(this) {
      log_title("tracking: " + cmd);
    }

    connected = false;

    new MyThread() {
      public void run() {
        LedaSocket sock = new LedaSocket();
        sock.setTimeout(5000);
 
        if (!sock.connect(server_host,tracking_port)) return;
     
        sock.sendString(format("%s %.2f %s",tracking_name,client_version,cmd));
 
        String s = sock.receiveString();
        if (!s.equals("ok") && !s.equals("")) 
        { log("send_tracking_cmd: " + s);
          sock.disconnect();
          return;
         }
 
        sock.disconnect();
        connected = true;
      }
    }.start();

  }


  boolean send_tracking_data(String what, long t, 
                                     double lon, double lat, double alt,
                                     float acc, float spd, double dist, 
                                     double asc, int hrate, float temp)
  {
    if (!network_connected) return false;

    if (tracking_name.equals("")) return false;

    if (!connected)
    { send_tracking_cmd("trk_begin " + track_name_long);
      return false;
     }

    int bat = (int)(0.5 + 100*battery_level);
    long sec = t/1000;

/*
    final String data = 
             format("%s %s %s %d %.6f %.6f %.1f %.2f %.2f %.1f %.1f %d %d",
                    tracking_name, client_version,
                    what,sec,lon,lat,alt,acc,spd,dist,asc,hrate,bat);
*/

    final String data = 
             format("%s %s %s %d %.6f %.6f %.1f %.2f %.2f %.1f %.1f %d %.2f",
             tracking_name, client_version,
             what,sec,lon,lat,alt,acc,spd,dist,asc,hrate,temp);

    new MyThread() {
      public void run() {
        LedaSocket sock = new LedaSocket();
        sock.setTimeout(5000);
        if (!sock.connect(server_host,tracking_port)) return;
        sock.sendString(data);
        String s = sock.receiveString();
        if (!s.equals("ok") && !s.equals("")) log("send_tracking_data: " + s);
        sock.disconnect();
      }
    }.start();

    return true;
  }


  void send_tracking_location(String label, long t, Location loc, 
                              double gps_alt, double srtm3_alt, double baro_alt,
                              float spd, double dst, double asc, int hrt)
  {
    double lon = loc.getLongitude();
    double lat = loc.getLatitude();
    float  acc = loc.getAccuracy();

    double alt = gps_alt;
    if (use_srtm3_altitude && srtm3_alt != DISTANCE_UNDEFINED) alt = srtm3_alt;
    if (use_barometer_altitude && baro_alt!=DISTANCE_UNDEFINED) alt = baro_alt;

    if (alt == DISTANCE_UNDEFINED) return;

    double tracking_d = 0;
    double tracking_t = 0;

    if (last_tracking_loc != null)
    { tracking_d = loc.distanceTo(last_tracking_loc);
      tracking_t = t - last_tracking_t;
     }

    if (last_tracking_loc == null || tracking_d >= 30 || tracking_t >= 5000)
    { // moved 30 m or for 5 sec
      send_tracking_data(label,t,lon,lat,alt,acc,spd,dst,asc,hrt,current_temp);
      last_tracking_loc = loc;
      last_tracking_t = t;
     }
  }



  String time_to_hms(long msec)
  { int s = (int)(msec/1000);
    int h = s/3600;
    s -= 3600*h;
    int m = s/60;
    s -= 60*m;
    return format("%02d:%02d:%02d",h,m,s);
   }


  String time_to_ms(long msec)
  { int s = (int)(msec/1000);
    int h = s/3600;
    s -= 3600*h;
    int m = s/60;
    s -= 60*m;
    if (h == 0)
      return format("%02d:%02d",m,s);
    else
      return format("%d:%02d:%02d",h,m,s);
   }


  void update_wearable()
  {
/*
    if (sender.getConnectedDevice() == null) return;
*/

    long tt = total_time(); // msec
    long bt = break_time();
    double avg_speed = (tt > 0) ? (1000*total_dist)/tt : 0;  // meter/s


    double asc = totalAscent();

    SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd-HH:mm");
    String name = df.format(track_begin_time);

    String state = "";

    if (gps_writer == null)
      state = "stopped";
    else
      if (current_break > break_limit)
        state = "break";
      else
        state = "moving";


    double avg_spd = 0;
    double avg_hrate = 0;

    if (tt > 10 && total_dist > 100) avg_spd = total_dist/tt;

    sum_hrate += current_hrate;
    if (tt > 0) avg_hrate = sum_hrate/tt;

    double alt = baro_altitude();

    if (alt == DISTANCE_UNDEFINED) alt = 0;

    JSONObject json = new JSONObject();

    try {

    json.put("state",state);
    json.put("name",name);
    json.put("time",tt);
    json.put("brk",bt);
    json.put("dst",total_dist);
    json.put("alt",alt);
    json.put("asc",asc);
    json.put("spd",current_speed);
    json.put("avg_spd",avg_speed);
    json.put("hrt",current_hrate);
    json.put("avg_hrt",avg_hrate);
    json.put("max_hrt",hrate_max);
    json.put("pwr",current_power);

    if (course != null && course_current_p != -1)
    { double crs_dst = course.getDistance(course_current_p);
      double crs_asc = course.getAscent(course_current_p);
      json.put("crs_index",course_current_p);
      json.put("crs_lon",course_current_loc.getLongitude());
      json.put("crs_lat",course_current_loc.getLatitude());
      json.put("crs_dst",crs_dst);
      json.put("crs_asc",crs_asc);
      json.put("crs_closest_dst",course_current_dst);
      json.put("crs_on_track",course_on_track);
    }

    } catch(Exception ex) {}

/*
    sender.sendMessage(json.toString());
*/
  }



  void notify(String title, String text1, String text2)
  { 
    Context context = getBaseContext();

    int title_clr = 0xff000080;
    int text_clr = 0xff000080;

    int uimode = getResources().getConfiguration().uiMode;
    int flags = uimode & Configuration.UI_MODE_NIGHT_MASK;

    if (flags == Configuration.UI_MODE_NIGHT_YES)
    { title_clr = 0xffcccccc;
      text_clr = 0xffcccccc;
      title = " " + title;
      text1  = " " + text1;
      text2  = " " + text2;
    }


    String pkg_name = getPackageName();

    Intent intent1 = new Intent(this, sTracksActivity.class);
    intent1.setAction("android.intent.action.MAIN");
    PendingIntent pIntent = PendingIntent.getActivity(this,0,intent1,
                                            PendingIntent.FLAG_UPDATE_CURRENT |
                                            PendingIntent.FLAG_IMMUTABLE);

    RemoteViews rviews;

    Spannable span = new SpannableString(text2);
    int p = text2.indexOf("\u2191");
    if (p != -1) {
      span.setSpan(new RelativeSizeSpan(1.3f),p,p+1,
                                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    }

    if (Build.VERSION.SDK_INT >= 24) 
    { rviews  = new RemoteViews(pkg_name,R.layout.notification);

      notificationBuilder.setStyle(new Notification.DecoratedCustomViewStyle());
      //notificationBuilder.setCustomBigContentView(rviews);
      notificationBuilder.setCustomContentView(rviews);

    //if (gps_writer != null)
      if (gps_available)
        notificationBuilder.setColor(0xff00aa00);
      else
        notificationBuilder.setColor(0xffaa0000);
    }
    else
    { rviews  = new RemoteViews(pkg_name,R.layout.notification_old);

      if (gps_available)
        rviews.setImageViewBitmap(R.id.notification_icon_right,gps_icon_on);
      else
        rviews.setImageViewBitmap(R.id.notification_icon_right,gps_icon_off);
  
      int i_res = R.drawable.crankset512a_transparent;
      if (pkg_name.endsWith("devel")) i_res = R.drawable.crankset512a_yellow;
      if (pkg_name.endsWith("full")) i_res = R.drawable.crankset512a_green;
      rviews.setImageViewResource(R.id.notification_image,i_res);

      notificationBuilder.setContent(rviews);
     }

    rviews.setOnClickPendingIntent(R.id.notification_main,pIntent);

    rviews.setTextViewText(R.id.notification_title, title);
    rviews.setTextColor(R.id.notification_title,title_clr);

    rviews.setTextViewText(R.id.notification_text1, text1);
    rviews.setTextColor(R.id.notification_text1,text_clr);

    rviews.setTextViewText(R.id.notification_text2, span);
    rviews.setTextColor(R.id.notification_text2,text_clr);

    notificationBuilder.setPriority(Notification.PRIORITY_MAX);

    notificationBuilder.setShowWhen(false);
/*
    notificationBuilder.setShowWhen(true);
    notificationBuilder.setWhen(System.currentTimeMillis());
*/

    //notificationBuilder.setContentTitle("sTracks");

    try { // TransactionTooLargeException ?

      notificationManager.notify(NOTIFICATION_ID1,notificationBuilder.build());

    } catch (Exception ex) { 
        showToast(ex.toString()); 
        log("");
        log_title("EXCEPTION");
        log_exception(ex);
        log("");
      }
  }


  void notifyText(String text)
  { log("");
    log_title("NOTIFY TEXT");
    log_title("text = " + text);
    notificationBuilder.setContentTitle("sTracks");
    notificationBuilder.setContentText(text);
    notificationBuilder.setPriority(Notification.PRIORITY_MAX);
    notificationBuilder.setShowWhen(false);
/*
    notificationBuilder.setShowWhen(true);
    notificationBuilder.setWhen(System.currentTimeMillis());
*/
    notificationManager.notify(NOTIFICATION_ID1,notificationBuilder.build());
  }




  void notifyPopup(String title, String text)
  { 
    if (Build.VERSION.SDK_INT < 26) return;

/*
    Bitmap bmp = BitmapFactory.decodeResource(getResources(),
                                              R.drawable.crankset512a_white);
*/

   Notification.Builder builder;
   if (Build.VERSION.SDK_INT >= 26) 
      builder = new Notification.Builder(this,CHANNEL2_ID);
   else
      builder = new Notification.Builder(this);

    builder.setSmallIcon(R.drawable.bike32a);
    builder.setContentTitle(title);
    builder.setContentText(text);
    builder.setColor(0xffa00000);
/*
    builder.setLargeIcon(bmp);
*/
    builder.setStyle(new Notification.BigTextStyle().bigText(text));

    notificationManager.notify(NOTIFICATION_ID2, builder.build());
  }




  void track_notify()
  {
      long tt = total_time();
      long bt = break_time();

      double avg_speed = (tt > 0) ? 3600*total_dist/tt : 0;

      double asc = totalAscent();

      SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd-HH:mm");
      String name = df.format(track_begin_time);


      String tit = name;

      //tit += "  " + time_to_hms(tt);

      //String txt1 = time_to_hms(tt) + "  /  " + time_to_ms(bt);
      String txt1 = time_to_hms(tt) + " / " + time_to_ms(bt);

      String txt2 = "";

      if (total_dist < 1000)
        txt2 += format("%.0f m ", total_dist);
      else
        if (total_dist < 10000)
          txt2 += format("%.2f km", total_dist/1000);
        else
          txt2 += format("%.1f km", total_dist/1000);

      txt2 += format("    %4.0f\u2191m",asc);  // up-arrow
      txt2 += format("     %4.1f km/h",avg_speed);


/*
txt = format("t = %d   tbrk %d   cbrk: %d", 
                                   total_time(), total_break, current_break);
*/
 
      //notify(tit,txt);
      notify(tit,txt1,txt2);
   }


  void notrack_notify()
  { 
    String title = sTracksActivity.APP_NAME;
    String txt = "";

    if (!location_permission) {
      txt = "LOCATION: NO PERMISSION";
    }
    else
    if (user_name.equals(""))
    { if (gps_available)
        txt = string_gps_available;
      else
        txt = string_gps_waiting;
     }
    else
    { title = server_host;
      txt = user_name;
/*
      if (Build.VERSION.SDK_INT >= 26)
      { txt += "\n";
        if (live_tracking) 
          txt += "tracking: on";
        else
          txt += "tracking: off";
       }
*/
     }

/*
    if (current_hrate > 0)
       txt += format("   hr: %d", current_hrate);
*/

/*
    if (current_loc !=null) {
      float h_acc = current_loc.getAccuracy();
      float v_acc = -1;
      if (Build.VERSION.SDK_INT >= 26) {
        v_acc = current_loc.getVerticalAccuracyMeters();
      }
      txt += format("   sat: %d / %d", used_satellites, num_satellites);
      txt += format("   acc: %.1f / %.1f", h_acc,v_acc);
    }
*/

    //notify(title,txt);
    notify(title,txt,"");
  }


  void update_notification()
  { if (!current_gps_file.exists()) 
      notrack_notify();
    else
    { track_notify();
      update_wearable();
     }
   }




  public void update_location()
  { 
    if (indoor_mode && indoor_fixed_speed > 0) {
      float dist = current_wheel_dist + indoor_fixed_speed;
      update_wheel_distance(dist,indoor_fixed_speed);
    }

    Location loc = current_loc;

    if (loc == null) return;

    long t  = get_current_time_millis();

    current_time = t;

    double lon = 0;
    double lat = 0;
    double alt = 0;
    float  spd = 0;
    float  acc = -1; // indicates undefined location
    float  v_acc = -1; 

    double gps_alt = DISTANCE_UNDEFINED;
    double baro_alt = DISTANCE_UNDEFINED;
    double srtm3_alt = DISTANCE_UNDEFINED;
    double srtm3_dist = DISTANCE_UNDEFINED;

    double home_dist = DISTANCE_UNDEFINED;
    double wp_dist = DISTANCE_UNDEFINED;

    Intent intent = new Intent(UPDATE_TRACKPOINT);
    intent.putExtra("time",t);

    srtm3_matrix.set_auto_download(network_connected);

    if (use_srtm3_altitude && loc != null && ++srtm3_update_count <= 10) 
    { // wait for srtm3 values to be available (for the first 10 updates)

      if (srtm3_matrix.getAltitude(loc) == DISTANCE_UNDEFINED) 
      { if (srtm3_update_count == 10)
        { log("");
          log_title(format("SRTM3: timeout (%d)",srtm3_update_count)); 
         }
        return;
      }

      log("");
      log_title(format("SRTM3: ok (%d)",srtm3_update_count)); 
      srtm3_update_count = 10;
    }


    if (loc != null)
    { 
      lon = loc.getLongitude();
      lat = loc.getLatitude();
      alt = loc.getAltitude();
      spd = loc.getSpeed();
      acc = loc.getAccuracy();
    
      if (Build.VERSION.SDK_INT >= 26) {
        v_acc = loc.getVerticalAccuracyMeters();
      }

      // find waypoint near start

      if (loc != last_known_loc)
      { if (wp_loc == null && wp_find_count < 5 && acc < wp_calibration_dist)
        { // find closest waypoint at start (at most 5 times)
          find_waypoint(loc,wp_calibration_dist);
         }
       }

      // home & wp distances

      if (home_loc != null) home_dist = home_loc.distanceTo(loc);
      if (wp_loc != null) wp_dist = wp_loc.distanceTo(loc);


      // heading
      if (last_loc != null && loc != last_loc)
      { float heading  = last_loc.bearingTo(loc);
        float delta = heading - current_heading;
        // keep delta in [-180 .. +180]
        if (delta > +180) delta -=360;
        if (delta < -180) delta +=360;
        if (Math.abs(delta) > 15) current_heading += delta/2;

        // keep heading in beween 0 and 360
        if (current_heading < 0) 
          current_heading += 360;
        else
          if (current_heading > 360) current_heading -= 360;
       }


      // speed

    //current_speed = location_buffer.getCurrentSpeed();
      current_speed = loc.getSpeed();


      // altitude

      if (use_srtm3_altitude)
      { srtm3_alt = srtm3_matrix.updateAltitude(loc);
        //srtm3_alt = srtm3_matrix.getAltitude(loc);
        srtm3_dist = srtm3_matrix.getDistance(loc);
        srtm3_closest = srtm3_matrix.closestPoint(loc);
       }

      if (loc == last_known_loc)
        gps_alt = alt;
      else
      { location_buffer.add(loc);
        gps_alt = location_buffer.getCurrentAltitude();
       }

      // barometer calibration

      if (gps_count > 0 && use_barometer_altitude && baro_pressure > 0)
      { 
        // gps position and barometer pressure defined

        if (wp_loc != null && calibration_loc != wp_loc
                           && wp_dist < wp_calibration_dist)
          baro_calibrate(wp_name, wp_loc.getAltitude(), wp_loc,wp_dist,acc);
        else
          if (srtm3_alt != DISTANCE_UNDEFINED && current_loc != last_known_loc)
          { if (calibration_loc == null)
              baro_calibrate("SRTM3",srtm3_alt,current_loc,0,acc);
            else
              if (srtm3_dist < srtm3_calibration_dist)
                baro_calibrate("SRTM3",srtm3_alt,srtm3_closest,srtm3_dist,acc);
          }
          else 
            //if (calibration_loc == null && gps_count <= 10)
            if (calibration_count == 0)
              baro_calibrate("GPS",gps_alt,null,0,acc);
      }


      // position on course

      if (course != null && !indoor_mode)
      { 
        long t1 = System.currentTimeMillis();
        int p = course.find_next_point(loc,course_current_p,-1,
                                                     COURSE_FOUND_DIST);
/*
        int p = course.find_next_point(loc,course_current_p,100,
                                                     COURSE_FOUND_DIST);
*/
        //course_current_loc = course.getLocation(p);
        course_current_loc = course.find_closest_loc(loc,p);

        double d = loc.distanceTo(course_current_loc);

        //log(format("Course Point: %d  d = %.1f",p,d));

/*
if (p != course_current_p) {
   long t2 = System.currentTimeMillis();
   log(format("Course Point: %d  d = %.1f t = %d ms",p,d,t2-t1));
}
*/
        course_current_p = p;
        course_current_dst = d;

        if (d >= COURSE_LOST_DIST)
        { course_lost_count++;
          if (course_lost_count <= 4)
            log_title(format("Off Course %d  p = %d  d = %.1f",
                              course_lost_count,p,d));
         }

        if (d <= COURSE_FOUND_DIST)
        { if (course_lost_count > 0)
          { log_title(format("On Course %d  p = %d  d = %.1f",
                              course_lost_count,p,d));
            course_lost_count = 0;
           }
         }

        if (course_on_track) 
        { if (course_lost_count >= COURSE_LOST_LIMIT)
          { log(format("COURSE LOST  %.2f km",total_dist/1000));
            course_on_track = false;
            String uri = sound_uri[sTracksActivity.SOUND_COURSE_LOST];
            if (uri.equals("tts"))
            { if (lang.equals("Deutsch"))
                uri = format("tts:Warnung:Abweichung vom Kurs");
              else
                uri = format("tts:Warning:Course lost");
             }
            play_sound(uri);
          }
        }
        else
        { if (course_lost_count == 0)
          { log(format("COURSE FOUND  %.2f km ",total_dist/1000));
            course_on_track = true;
            String uri = sound_uri[sTracksActivity.SOUND_COURSE_FOUND];
            if (uri.equals("tts"))
            { if (lang.equals("Deutsch"))
                uri = format("tts:Hinweis:Kurs gefunden");
              else
                uri = format("tts:Notice:Course found");
             }
            play_sound(uri);
          }
        }
      }


      intent.putExtra("lon",lon);
      intent.putExtra("lat",lat);
      intent.putExtra("alt",alt);
      intent.putExtra("dst",total_dist);
      intent.putExtra("spd",spd);
      intent.putExtra("acc",acc);

      if (gps_writer != null && course_current_p != -1)
      { // recording and current course point defined
        double crs_dst = course.getDistance(course_current_p);
        double crs_asc = course.getAscent(course_current_p);
        intent.putExtra("crs_index",course_current_p);
        intent.putExtra("crs_lon",course_current_loc.getLongitude());
        intent.putExtra("crs_lat",course_current_loc.getLatitude());
        intent.putExtra("crs_dst",crs_dst);
        intent.putExtra("crs_asc",crs_asc);
        intent.putExtra("crs_closest_dst",course_current_dst);
        intent.putExtra("crs_on_track",course_on_track);
       }


      intent.putExtra("gps_alt",gps_alt);
      intent.putExtra("srtm3_alt",srtm3_alt);
      intent.putExtra("srtm3_dist",srtm3_dist);

      intent.putExtra("home_dist",home_dist);


      intent.putExtra("speed",current_speed);
      intent.putExtra("heading",current_heading);

    } // loc defined


    // barometer
    baro_alt = baro_altitude();
    intent.putExtra("baro_alt",baro_alt);
    intent.putExtra("prs",baro_pressure);
    intent.putExtra("prsnn",baro_pressure_nn);
    intent.putExtra("cali",calibration_count);

    // hrate
    if (current_hrate > 0) intent.putExtra("hrt",current_hrate);

    //power 
    if (current_power > 0) intent.putExtra("pwr",current_power);
    if (current_torque > 0) intent.putExtra("trq",current_torque);
    if (current_cadence > 0) intent.putExtra("cad",current_cadence);
    if (current_temp != 0) intent.putExtra("temp",current_temp);
    if (current_wheel_speed > 0) intent.putExtra("wheel_spd",current_wheel_speed);
    if (current_wheel_dist > 0) intent.putExtra("wheel_dst",current_wheel_dist);


    // auto laps

    long tt = 60000 * lap_auto_time; // msec
    int  dd = 1000 * lap_auto_dist;  // m

    if ((lap_auto_mode == 1 && t > lap_time + tt) ||
        (lap_auto_mode == 2 && total_dist > lap_dist + dd))
    { lap_num++;
      lap_time = t;
      lap_dist = total_dist;
      if (gps_writer != null) {
        play_sound(sTracksActivity.SOUND_LAP_START);
        write_gps_line("#lap");
       }
      intent.putExtra("lap",lap_num);
     }

    sendBroadcast(intent);
    //localBroadcastMgr.sendBroadcast(intent);


    if (gps_writer == null) 
    { if (current_gps_file.exists())
      { // recording stopped (update break time)
        if (last_loc != null)
        { //long diff_time = t - last_loc.getTime();
          //current_break = diff_time;
          if (stop_time > 0) {
            total_break += current_time - stop_time;
            stop_time = current_time;
          }
         }
        update_notification();
       }
      else
      { // not recording
        if (live_tracking && loc != null) 
         send_tracking_location("point",t,loc,gps_alt,srtm3_alt,baro_alt,0,0,0,
                                                                current_hrate);
       }
      return;
    }

    // gps_writer != null (recording)

    // update total dist, total ascent and breaks 
    // write to gps file 
    // send data to tracking server
    // play acoustic signals

    if (last_loc == null) 
    { // first point
      baro_pressure_start = baro_pressure;
      baro_alt_start = baro_altitude();
      last_loc = loc;
    }
    else
    { // update total_ascent and total dist (if movement above threshold)

      long diff_time = t - last_loc.getTime();
      double diff_dist = loc.distanceTo(last_loc);

      double dmin = acc;
      if (dmin < point_mindist) dmin = point_mindist;

      if (diff_dist < dmin)
      { // movement below threshold: possibly a break

        if (current_break < break_limit && diff_time >= break_limit) {
          //break detected: current_break exceeds break_limit
          play_sound(sTracksActivity.SOUND_TIMER_STOP);
        }

        current_break = diff_time;
      }
      else
      { // diff_dist >= dmin

        //total_dist += diff_dist;
        //total_dist += 0.996 * diff_dist; // 2.223
        //total_dist += 0.9955 * diff_dist;  // 2.224
        total_dist += 0.996 * diff_dist;  // 2.228

        totalAsc.update(0,alt,total_dist,acc);
        totalAsc.update(1,srtm3_alt,total_dist,acc);
        if (barometer != null) totalAsc.update(2,baro_alt,total_dist,acc);

        if (current_break >= break_limit)
        { // finish break 
          play_sound(sTracksActivity.SOUND_TIMER_START);
          total_break += diff_time;
         }
        current_break = 0;
        last_loc = loc;
      }
    }

    write_data(t,lon,lat,alt,acc,
               gps_alt,srtm3_alt,baro_alt,
               total_dist,spd, baro_pressure,
               current_hrate,current_power,
               current_cadence,current_temp);

    num_points++;

/*
    // testing
    if (num_points % 10 == 0) recompute_ascent();
*/

    double total_asc = totalAscent();

    if (live_tracking && acc > 0) {
      send_tracking_location("trkpoint",t,loc,gps_alt,srtm3_alt,baro_alt,spd,
                                        total_dist,total_asc,current_hrate);
    }


    // acoustic signals 
 
    if (acoustic_signals_volume == 0) return;
 
    if (current_hrate >= hrate_limit) 
    { hrate_limit = 1000;
      play_sound(sTracksActivity.SOUND_HRATE_LIMIT);
    }
 
    if (current_hrate <= hrate_limit_low) hrate_limit = user_hrate_limit;
 
 
    final boolean ascent_notify_sound = total_asc >= total_ascent_notify_limit; 

    if (total_asc >= total_ascent_limit[total_ascent_limit_index])
    { 
      synchronized(this) { 
        log_title("Total Ascent Limit");
        log("ascent = " + total_asc);
        log("index = " + total_ascent_limit_index);
        log ("");
      }
 
      final int i = total_ascent_limit_index++;
 
      new MyThread() {
        public void run() {
           if (ascent_notify_sound) sleep(4000);
           if (i == 0) play_sound(sTracksActivity.SOUND_ASCENT_LIMIT1);
           if (i == 1) play_sound(sTracksActivity.SOUND_ASCENT_LIMIT2);
           if (i == 2) play_sound(sTracksActivity.SOUND_ASCENT_LIMIT3);
         }
      }.start();
 
    }
     
 
    if (ascent_notify_sound)
    {
      String uri = sound_uri[sTracksActivity.SOUND_ASCENT_NOTIFY];
 
      if (uri.equals("tts"))
      { if (lang.equals("Deutsch"))
          uri = format("tts:Aufstieg:%d Meter",total_ascent_notify_limit);
        else
          uri = format("tts:Total Ascent:%d Meters",total_ascent_notify_limit);
       }
 
      play_sound(uri);

      if (lang.equals("Deutsch"))
        notifyText(format("Aufstieg  %.0f m", total_asc));
      else
        notifyText(format("Ascent  %.0f m", total_asc));

      while (total_asc >= total_ascent_notify_limit) {
          total_ascent_notify_limit += total_ascent_notify_interval;
      }
    }


    if (total_dist >= 1000*total_distance_notify_limit)
    {
      int i = sTracksActivity.SOUND_DISTANCE_NOTIFY;
      //String uri = prefs.getString("sound_uri"+i,"silent");
      String uri = sound_uri[i];

      if (uri.equals("tts"))
      { if (lang.equals("Deutsch"))
          uri = format("tts:Distanz:%d Kilometer",total_distance_notify_limit);
        else
          uri = format("tts:Total Distance:%d Kilometers",total_distance_notify_limit);
       }

      play_sound(uri);

      if (lang.equals("Deutsch"))
        notifyText(format("Distanz  %.0f km", total_dist/1000));
      else
        notifyText(format("Distance  %.0f km", total_dist/1000));

      while (total_dist >= 1000*total_distance_notify_limit) 
         total_distance_notify_limit += total_distance_notify_interval;
    }

  }


  void update_gps_status(boolean b, String reason)
  { 
    if (b == gps_available) return;

    String msg = "";

    if (b)
      { //showToast("GPS: Signal ok.");
        msg = string_gps_available;
        update_device_message("gps","","available",reason);
        play_sound(sTracksActivity.SOUND_GPS_ON);
      }
    else
      { //showToast("GPS: Signal lost.");
        //current_loc = null;
        msg = string_gps_waiting;
        update_device_message("gps","","unavailable",reason);
        play_sound(sTracksActivity.SOUND_GPS_OFF);
      }

    gps_available = b;

    update_notification();
  }


  void update_device_message(String dev, String name, String msg, String value)
  { 
    if (value != null && value.equals("USER_CANCELLED")) value = "user";
/*
    if (dev.equals("network"))
    { String s = "none";
      if (wifi_connected && mobile_connected) s = "wifi/mobile";
      else
      { if (wifi_connected) s = "wifi";
        if (mobile_connected) s = "mobile";
       }
      log_title(format("dev: %s %s",dev,s));
     }
*/
    log("");
    log_title("device message");
    if (name == null || name.equals(""))
      log(format("%s %s %s",dev,msg,value));
    else
      log(format("%s %s %s %s",dev,name,msg,value));

    Intent intent = new Intent(UPDATE_DEVICE_MESSAGE);
    intent.putExtra("device",dev);
    intent.putExtra("name",name);
    intent.putExtra("msg",msg);
    intent.putExtra("val",value);
    sendBroadcast(intent);
    //localBroadcastMgr.sendBroadcast(intent);
  }


  void update_bt_device_message(Bluetooth bt, String device, String name,
                                                             String msg,
                                                             String addr,
                                                             int sound)
  { 
    update_device_message(device,name,msg,addr);

    if (msg.equals("available"))
    { int n = bt.getConnectCount();

      if (device.equals("bt_hrate") && !bt_hrate_connect_name.equals("")) 
        name = bt_hrate_connect_name;

      if (n > 0)
        showToast(name + " " + n);
      else
      { showToast(name);
        if (sound != -1 && n == 0) play_sound(sound);
       }
     }
    else
     if (msg.equals("disconnected")) {
       //showToast(name + "\ndisconnected");
     }
  }


  void update_pressure(float pressure, float accuracy)
  { 
    if (mock_locations) return;

    // sliding average

    int i = baro_count % baro_values.length;
    baro_sum -= baro_values[i];
    baro_sum += pressure;
    baro_values[i] = pressure;

    baro_count++;

    int n = baro_count;
    if (baro_count > baro_values.length) n = baro_values.length;

    if (n > baro_values.length/2) baro_pressure = baro_sum / n;

   }


  void update_hrate(int hrate, int hrnum)
  { 
    hrate_last_update_time = get_current_time_millis();
    current_hrate = hrate;
    if (hrate > hrate_max) hrate_max = hrate;
   }


  void update_power(float power)
  { power_last_update_time = get_current_time_millis();

    //current_power = power;

    int i = power_count % power_values.length;
    power_sum += power;
    power_sum -= power_values[i];
    power_values[i] = power;
    power_count++;

    int n = power_values.length;
    if (power_count < power_values.length) n = power_count;

    current_power = (int)(0.5f + power_sum/n);
  }

  void update_cadence(float cadence)
  { cadence_last_update_time = get_current_time_millis();

    //current_cadence = cadence;

    int i = cadence_count % cadence_values.length;
    cadence_sum += cadence;
    cadence_sum -= cadence_values[i];
    cadence_values[i] = cadence;
    cadence_count++;

    int n = cadence_values.length;
    if (cadence_count < cadence_values.length) n = cadence_count;

    current_cadence = (int)(0.5f + cadence_sum/n);
   }


  void update_torque(float torque) 
  { //current_torque = torque;

    int i = torque_count % torque_values.length;
    torque_sum += torque;
    torque_sum -= torque_values[i];
    torque_values[i] = torque;
    torque_count++;

    int n = torque_values.length;
    if (torque_count < torque_values.length) n = torque_count;

    current_torque = torque_sum/n;
  }


  void update_wheel_distance(float dist, float speed) { 

    //log_title("update_wheel_dist: dist =  " + dist);

    if (dist <= current_wheel_dist) return;

    current_wheel_dist  = dist;
    current_wheel_speed = speed;

    if (course == null)
    { // 1 km circle around last known location

      double len = 1000;
      double lon = 0;
      double lat = 0;
      double alt = 0;

      if (last_known_loc != null)
      { lon = last_known_loc.getLongitude();
        lat = last_known_loc.getLatitude();
        alt = last_known_loc.getAltitude();
       }

      double r = len/(2*Math.PI);
      double a = 2*Math.PI*dist/len;
      double dx = Math.cos(a)*r;
      double dy = Math.sin(a)*r;

      lat += 180*dy/(Math.PI * 6371000);
      lon += 180*dx/(Math.PI * 6371000 * Math.cos(lat * Math.PI/180));

      current_loc = new Location("stracks");
      current_loc.setLatitude(lat);
      current_loc.setLongitude(lon);
      current_loc.setAltitude(alt);
      current_loc.setAccuracy(5);
      current_loc.setSpeed(current_wheel_speed);
      current_loc.setTime(get_current_time_millis());
      return;
    }


    // course != null
    // find position on course by distance

    int i = course.find_next_point_by_dist(dist,course_dist_i);

    if (i < course.size()-1)
    { // interpolate by dist between i and i+1
      double d = dist - course.getDistance(i);
      double dd = course.getDistance(i+1) - course.getDistance(i);

      Location loc1 = course.getLocation(i);
      double lon1 = loc1.getLongitude();
      double lat1 = loc1.getLatitude();
      double alt1 = loc1.getAltitude();

      Location loc2 = course.getLocation(i+1);
      double lon2 = loc2.getLongitude();
      double lat2 = loc2.getLatitude();
      double alt2 = loc2.getAltitude();

      double lon = lon1 + d*(lon2-lon1)/dd;
      double lat = lat1 + d*(lat2-lat1)/dd;
      double alt = alt1 + d*(alt2-alt1)/dd;

      current_loc = new Location("stracks");
      current_loc.setLongitude(lon);
      current_loc.setLatitude(lat);
      current_loc.setAltitude(alt);
      current_loc.setAccuracy(5);
      current_loc.setSpeed(current_wheel_speed);
      current_loc.setTime(get_current_time_millis());
/*
      if (alt <= 0) alt = srtm3_matrix.updateAltitude(current_loc);
      current_loc.setAltitude(alt);
*/
    }

    course_current_p = i;
    course_current_loc = current_loc;
    course_on_track = true;
    course_current_dst = 0;
  }



  void update_temperature(float temp)
  { temp_last_update_time = get_current_time_millis();
    log_title(format("TEMPERATURE: %.2f",temp));
    current_temp = temp;
   }


    int count_lines(File file)
    {
      int count = 0;

      try {
        BufferedReader reader = new BufferedReader(new FileReader(file));
        for(;;)
        { String line = reader.readLine();
          if (line == null) break;
          count++;
         }
       } catch (IOException e) { count = 0; }

      return count;
    }


    void reload_gps()
    {
      log_title("reload_gps: " + current_gps_file.getPath());

      track_name_long = "";
      track_name = "";
      num_points = 0;
      total_dist = 0;
      gps_count = 0;
      current_break = 0;
      total_break = 0;
      last_loc = null;
      last_dist = 0;

      cdt_enabled = false;

      totalAsc.reset();

      GpsReader gpsReader = new GpsReader(current_gps_file) {

             @Override
             public void writeLog(String txt) { log(txt); }

             @Override
             public void handle_track_begin(String name, int length, int mode)
             {
               if (mode == 0)
               { gps_enabled = false;
                 locationManager.removeUpdates(locationListener);
                 update_gps_status(false,"gps:disable");
                 indoor_mode = true;
                }

               stop_time = 0;
             }

             @Override
             public void handle_track_point(final int i, Location loc,
                           double gps_alt,double srtm3_alt,double baro_alt,
                           float dst, float spd, float press,
                           int hrt, int pwr, float cad, float tmp)
             {
               gps_count++;

               long t = loc.getTime();
               double acc = loc.getAccuracy();
               double alt = loc.getAltitude();

               if (acc < 0) return;

               //num_points++;

               current_time = t;
               baro_pressure = press;

               if (last_loc == null) 
                 last_loc = loc;
               else
               { // update total_ascent and total dist 
                 // (if movement above threshold)
           
                 long diff_time = t - last_loc.getTime();
      
               //double diff_dist = loc.distanceTo(last_loc);
                 double diff_dist = dst - last_dist;
           
                 double dmin = acc;
                 if (dmin < point_mindist) dmin = point_mindist;
           
                 if (diff_dist < dmin)
                   current_break = diff_time;
                 else
                 { total_dist += diff_dist;
      
                   totalAsc.update(0,alt,total_dist,acc);
                   totalAsc.update(1,srtm3_alt,total_dist,acc);
                   if (barometer != null) 
                      totalAsc.update(2,baro_alt,total_dist,acc);
      
                   if (current_break >= break_limit) total_break += diff_time;
                   current_break = 0;
                   last_loc = loc;
                   last_dist = dst;
                  }
                }
             }


             @Override
             public void handle_track_end() { 
                current_loc = last_loc;
             }

             @Override
             public void handle_lap() { }

             @Override
             public void handle_start(long t) { 
                if (stop_time != 0) total_break += (t - stop_time);
             }

             @Override
             public void handle_stop(long t) { stop_time = t; }
       };


       track_name_long = gpsReader.readTrackName();
       log("name = " + track_name_long);

       track_name = track_name_long;
       if (track_name.length() > 16) track_name = track_name.substring(0,16);

       SimpleDateFormat df;
       if (track_name_long.length() == 16)
         df = new SimpleDateFormat("yyyy-MM-dd-HH-mm");
       else
         df = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");

       Date d = null;
       try { d = df.parse(track_name_long);
       } catch(Exception e) { log("reload_gps: " + e.toString()); }

       if (d != null) track_begin_time = d.getTime();

       log("begin_time = " + df.format(track_begin_time));

       current_time = track_begin_time;

       log("Start GpsReader");
       gpsReader.read();

       cdt_enabled = true;

       if (lap_auto_mode == 1) 
       { long tt = 60000 * lap_auto_time; 
         while (current_time > lap_time + tt) 
         { lap_time += tt;
           lap_num++;
          }
        }

       if (lap_auto_mode == 2)
       { int dd = 1000 * lap_auto_dist;
         while (total_dist > lap_dist + dd)
         { lap_dist += dd;
           lap_num++;
          }
        }
 
       double asc = totalAscent();

       total_distance_notify_limit = 0;
       while (asc >= total_distance_notify_limit) 
         total_distance_notify_limit += total_distance_notify_interval;

       total_ascent_notify_limit = 0;
       while (asc >= total_ascent_notify_limit) 
         total_ascent_notify_limit += total_ascent_notify_interval;

       total_ascent_limit_index = 0;
       while (asc >= total_ascent_limit[total_ascent_limit_index])
         total_ascent_limit_index++; 


       long total_t = current_time - track_begin_time;


       log("gps_count  = " + gps_count);

       log("track_begin_time: " + df.format(track_begin_time));
       log("current_time: " + df.format(current_time));
 
       log("total_time = " + time_to_hms(total_t));
       log(format("total_dist = %.2f km", total_dist/1000.0f));
       log(format("ascent[0] = %.2f m", totalAsc.getAscent(0)));
       log(format("ascent[1] = %.2f m", totalAsc.getAscent(1)));
       log(format("ascent[2] = %.2f m", totalAsc.getAscent(2)));
 
       log("end of reload_gps");
       log("");
    }


    void recompute_ascent()
    {
      double ascent0 = totalAsc.getAscent(0);
      double ascent1 = totalAsc.getAscent(1);
      double ascent2 = totalAsc.getAscent(2);

      cdt_enabled = false;

      long t = System.currentTimeMillis();

      totalAsc.reset();

      GpsReader gpsReader = new GpsReader(current_gps_file) {

             Location last_loc = null;
             float last_dist = 0;
             float total_dist = 0;

/*
             @Override
             public void writeLog(String txt) { log(txt); }
*/

             @Override
             public void handle_track_point(final int i, Location loc,
                           double gps_alt,double srtm3_alt,double baro_alt,
                           float dst, float spd, float press,
                           int hrt, int pwr, float cad, float tmp)
             {
               double acc = loc.getAccuracy();

               if (acc < 0) return;

               if (last_loc == null) 
               { last_loc = loc;
                 last_dist = 0;
                 total_dist = 0;
                 return;
                }

               // update total_ascent and total dist 
               double diff_dist = dst - last_dist;
               double dmin = acc;
               if (dmin < point_mindist) dmin = point_mindist;
         
               if (diff_dist >= dmin)
               { total_dist += diff_dist;
                 totalAsc.update(0,gps_alt,total_dist,acc);
                 totalAsc.update(1,srtm3_alt,total_dist,acc);
                 if (barometer != null) 
                    totalAsc.update(2,baro_alt,total_dist,acc);
                 last_loc = loc;
                 last_dist = dst;
               }
            }
       };

       gpsReader.read();

       cdt_enabled = true;

       log("");
       log_title("RECOMPUTE ASCENT");
       log(format("gps:   %.1f --> %.1f",ascent0,totalAsc.getAscent(0)));
       log(format("srtm3: %.1f --> %.1f",ascent1,totalAsc.getAscent(1)));
       log(format("baro:  %.1f --> %.1f",ascent2,totalAsc.getAscent(2)));
       log(format("Time: %d msec",System.currentTimeMillis() - t));
    }



   void read_config()
   {
     log_title("read config");
     log("");

     lang = prefs.getString("language","English");

     if (lang.equals("Deutsch"))
     { string_gps_available = "GPS Signal verfügbar";
       string_gps_waiting = "Warten auf GPS Signal.";
       string_gps_deactivated =
                      "Standortabfragen sind\n" +
                      "deaktiviert - bitte einschalten."; 
      }

     synchronized(this)
     {
       String host = prefs.getString("server_host",null);
       if (host != null) server_host = host;
       log("server_host = " + server_host);

       xserver_host = prefs.getString("xserver_host",xserver_host_def);
       log("xserver_host = " + xserver_host);
     
       user_name = prefs.getString("user","");
       log("user_name = " + user_name);

       tracking_name = user_name;
       log("tracking_name = " + tracking_name);
     
       live_tracking = prefs.getBoolean("live_tracking",false);
       log("live_tracking = " + live_tracking);
     
       use_srtm3_altitude = prefs.getBoolean("use_srtm3_altitude",true);
       log("use_srtm3_altitude = " + use_srtm3_altitude);
  
       use_barometer_altitude = prefs.getBoolean("use_barometer_altitude",true);
       log("use_barometer_altitude = " + use_barometer_altitude);
     
       lap_auto_mode=prefs.getInt("lap_auto_mode",0);
       log("lap_auto_mode = " + lap_auto_mode);
  
       lap_auto_time=prefs.getInt("lap_auto_time",0);
       log("lap_auto_time = " + lap_auto_time);
  
       lap_auto_dist=prefs.getInt("lap_auto_dist",0);
       log("lap_auto_dist = " + lap_auto_dist);
     
     
       srtm3_calibration_dist = prefs.getFloat("srtm3_calibration_dist",1);
       log("srtm3_calibration_dist = " + srtm3_calibration_dist);

       accuracy_filter = prefs.getFloat("accuracy_filter",50.0f);
       log(format("accuracy_filter = %5.2f",accuracy_filter));
     
       ascent_eps = prefs.getFloat("ascent_eps",5.0f);
       log(format("ascent_eps = %5.2f",ascent_eps));
     
       point_mindist = prefs.getFloat("point_mindist",5.0f);
       log("point_mindist  = " + point_mindist);
     
       break_limit = prefs.getLong("break_limit",10000);
       log("break_limit = " + break_limit);
     
       user_hrate_limit = prefs.getInt("user_hrate_limit",170);
       log("user_hrate_limit  = " + user_hrate_limit);
  
       hrate_limit_low  = user_hrate_limit - 10;
       hrate_limit = user_hrate_limit;
     
       for(int i=0; i<3; i++)
       { int x  = prefs.getInt("ascent_limit"+i, 1000+500*i);
         total_ascent_limit[i] = x;
         log("ascent_limit"+i+"  = " + x);
        }
  

       total_distance_notify_interval = 
                                prefs.getInt("distance_notify_interval",100);
       log("distance_notify_interval  = " + total_distance_notify_interval);
       total_distance_notify_limit = total_distance_notify_interval;
  
     
       total_ascent_notify_interval= prefs.getInt("ascent_notify_interval",100);
       log("ascent_notify_interval  = " + total_ascent_notify_interval);
       total_ascent_notify_limit = total_ascent_notify_interval;
  

       // bluetooth

       bt_hrate_connect_address=prefs.getString("bt_hrate_connect_address","");
       log("bt_hrate_connect_address = " + bt_hrate_connect_address);

       bt_hrate_connect_name=prefs.getString("bt_hrate_connect_name","");
       log("bt_hrate_connect_name = " + bt_hrate_connect_name);

       bt_hrate_auto_connect=prefs.getBoolean("bt_hrate_auto_connect",true);
       log("bt_hrate_auto_connect = " + bt_hrate_auto_connect);


       bt_power_auto_connect=prefs.getBoolean("bt_power_auto_connect",true);
       log("bt_power_auto_connect = " + bt_power_auto_connect);

       bt_power_connect_address=prefs.getString("bt_power_connect_address","");
       log("bt_power_connect_address = " + bt_power_connect_address);

       bt_power_connect_name=prefs.getString("bt_power_connect_name","");
       log("bt_power_connect_name = " + bt_power_connect_name);


       bt_cadence_auto_connect=prefs.getBoolean("bt_cadence_auto_connect",true);
       log("bt_cadence_auto_connect = " + bt_cadence_auto_connect);

       bt_cadence_connect_address=prefs.getString("bt_cadence_connect_address","");
       log("bt_cadence_connect_address = " + bt_cadence_connect_address);


       bt_temp_auto_connect=prefs.getBoolean("bt_temp_auto_connect",true);
       log("bt_temp_auto_connect = " + bt_temp_auto_connect);

       bt_temp_connect_address=prefs.getString("bt_temp_connect_address","");
       log("bt_temp_connect_address = " + bt_temp_connect_address);

       bt_fitness_auto_connect=prefs.getBoolean("bt_fitness_auto_connect",true);
       log("bt_fitness_auto_connect = " + bt_fitness_auto_connect);

       bt_fitness_connect_address=prefs.getString("bt_fitness_connect_address","");
       log("bt_fitness_connect_address = " + bt_fitness_connect_address);

       
       // ant

       ant_hrate_connect_name = prefs.getString("ant_hrate_connect_name","");
       log("ant_hrate_connect_name  = " + ant_hrate_connect_name);
  
       ant_power_connect_name = prefs.getString("ant_power_connect_name","");
       log("ant_power_connect_name  = " + ant_power_connect_name);
  
       ant_cadence_connect_name = prefs.getString("ant_cadence_connect_name","");
       log("ant_cadence_connect_name  = " + ant_cadence_connect_name);

       ant_temp_connect_name = prefs.getString("ant_temp_connect_name","");
       log("ant_temp_connect_name  = " + ant_temp_connect_name);
  
  
       //sound
  
       acoustic_signals_volume = prefs.getInt("acoustic_signals_volume",90);
       if (acoustic_signals_volume > 100) acoustic_signals_volume = 100;
       log("acoustic_signals_volume = " + acoustic_signals_volume);

       sound_uri = new String[sTracksActivity.SOUND_NUM];
  
       for(int i=0; i<sound_uri.length; i++)
       { sound_uri[i] = prefs.getString("sound_uri"+i,"silent");
         log("sound_uri" + i + " = " + sound_uri[i]);
        }

       log("");
  
     } // sync

   }


   void update_config_line(String line)
   {
     // line: key=value

     //log(line);

     String[] params = line.split("=");

     String key = params[0];
     String value = (params.length == 2) ? params[1] : "";

     if (key.equals("user")) {
       user_name = value;
       tracking_name = user_name;
       return;
     }

     if (key.equals("language")) {
       lang = value; 
       return;
     }

     if (key.equals("live_tracking")) {
       live_tracking = value.equals("true"); 
       return;
     }

     if (key.equals("acoustic_signals_volume")) {
       acoustic_signals_volume = Integer.parseInt(value);
       if (acoustic_signals_volume > 100) acoustic_signals_volume = 100;
       return;
     }

     if (key.equals("server_host")) {
       server_host = value;
       return;
     }

     if (key.equals("xserver_host")) {
       xserver_host = value;
       return;
     }

     if (key.equals("use_srtm3_altitude")) {
       use_srtm3_altitude = value.equals("true");
       return;
     }

     if (key.equals("use_barometer_altitude")) {
       use_barometer_altitude = value.equals("true");
       return;
     }

     if (key.equals("accuracy_filter")) {
       accuracy_filter = Float.parseFloat(value);
       return;
     }

     if (key.equals("ascent_eps")) {
       ascent_eps = Float.parseFloat(value);
       totalAsc.setAscentEps(ascent_eps);
       return;
     }
   
     if (key.equals("srtm3_calibration_dist")) {
       srtm3_calibration_dist = Float.parseFloat(value);
       return;
     }
   
     if (key.equals("point_mindist")) {
       point_mindist = Float.parseFloat(value);
       totalAsc.setMinDist(point_mindist);
       return;
     }

     if (key.equals("break_limit")) {
       break_limit = Long.parseLong(value);
       return;
     }

     if (key.startsWith("sound_uri")) 
     { int i = -1;
       try {
         i  = Integer.parseInt(key.replace("sound_uri",""));
       } catch (NumberFormatException ex) {}

       if (i < 0 || i >=  sound_uri.length) 
         showToast("sound: illegal index = " + i);
       else
         sound_uri[i] = value;
       return;
     }

     if (key.equals("user_hrate_limit")) { 
       user_hrate_limit = Integer.parseInt(value);
       hrate_limit_low  = user_hrate_limit - 10;
       hrate_limit = user_hrate_limit;
     }

     if (key.equals("ascent_limits")) { 
       String[] buf = value.split(" ");
       total_ascent_limit[0] = Integer.parseInt(buf[0].trim());
       total_ascent_limit[1] = Integer.parseInt(buf[1].trim());
       total_ascent_limit[2] = Integer.parseInt(buf[2].trim());
       return;
     }

     if (key.equals("ascent_notify_interval")) { 
       total_ascent_notify_interval = Integer.parseInt(value);
       return;
     }

     if (key.equals("distance_notify_interval")) { 
       total_distance_notify_interval = Integer.parseInt(value);
       return;
     }

     if (key.equals("bt_hrate_auto_connect")) {
       bt_hrate_auto_connect = value.equals("true");
       bt_hrt.setAutoConnect(bt_hrate_auto_connect);
       return;
     }

     if (key.equals("bt_power_auto_connect")) {
       bt_power_auto_connect = value.equals("true");
       bt_pwr.setAutoConnect(bt_power_auto_connect);
       return;
     }

     if (key.equals("bt_cadence_auto_connect")) {
       bt_cadence_auto_connect = value.equals("true");
       bt_cad.setAutoConnect(bt_cadence_auto_connect);
       return;
     }

     if (key.equals("bt_temp_auto_connect")) {
       bt_temp_auto_connect = value.equals("true");
       bt_tmp.setAutoConnect(bt_temp_auto_connect);
       return;
     }

     if (key.equals("bt_fitness_auto_connect")) {
       bt_fitness_auto_connect = value.equals("true");
       bt_fit.setAutoConnect(bt_fitness_auto_connect);
       return;
     }

   }


   void update_config(String txt)
   { String[] lines = txt.split(";");
     for(int i = 0; i<lines.length; i++) update_config_line(lines[i]);
     log("");
    }
   


   void ant_restart_scanning(final String reason)
   { 
     ant_restart_count++;

     hrate_last_update_time = 0;
     power_last_update_time = 0;
     cadence_last_update_time = 0;

   //update_device_message("ant","0","restart","" + ant_restart_count);

     log_title("ANT:Restart " + ant_restart_count + " " + reason);

/*
     showToast("ANT Restart: " + reason);
*/
     ant.stopScanning("all");

     MyThread thr = new MyThread() {
          public void run() {
             handler.post(new Runnable() {
                public void run() { 
                  //sleep(2000);
                  sleep(1000);
                  startAnt();
                }
             });
          }
     };

     thr.start();

   }
   

   void startAnt()
   {
     boolean ant_plugin = false;
     boolean ant_radio = false;
     boolean ant_hal = false;
     boolean ant_usb = false;

     PackageManager pm = getPackageManager();

     try {
       ant_plugin = pm.getPackageInfo("com.dsi.ant.plugins.antplus",0) != null;
     } catch (Exception ex) {}

     try {
       ant_radio = pm.getPackageInfo("com.dsi.ant.service.socket",0) != null;
     } catch (Exception ex) {}

     try {
        ant_hal = pm.getPackageInfo("com.dsi.ant.server",0) != null;
      } catch (Exception ex) {}

     try {
        ant_usb = pm.getPackageInfo("com.dsi.ant.usbservice",0) != null;
     } catch (Exception ex) {}

     log("ant_plugins = " + ant_plugin);
     log("ant_radio   = " + ant_radio);
     log("ant_hal     = " + ant_hal);
     log("ant_usb     = " + ant_usb);

      boolean ant_stick = false;

      if (ant_plugin && ant_radio && !ant_hal && ant_usb)
      { // find usb stick
        UsbManager usbManager = 
                     (UsbManager)getSystemService(Context.USB_SERVICE);

        HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList(); 
        Iterator<UsbDevice> deviceIterator = deviceList.values().iterator();
 
        log("USB Devices: " + deviceList.size());
 
        while(deviceIterator.hasNext()) 
        { UsbDevice device = deviceIterator.next();  
          log(device.getDeviceName());
          log("Product Name:  " + device.getProductName());
          log("Manufacturer:  " + device.getManufacturerName());
          log("Vendor Id:     " + device.getVendorId());
          log("Product Id:    " + device.getProductId());
          log("");
          if (device.getProductName().indexOf("ANT") != -1) 
          { ant_stick = true;
            break;
           }
         }
      }

      if (ant_plugin && ant_radio && (ant_hal || ant_stick))
      { log("ANT+ AVAILABLE.");
        update_device_message("ant","","available","INSTALLED");
       }
      else
      { log("ANT+ NOT AVAILABLE.");
        update_device_message("ant","0","unavailable","NOT INSTALLED");
        return;
       }


      ant = new Ant(this) {

       @Override
       public void writeLog(String txt) { 
/*
            DataService.this.writeLog(log_writer,"ANT LOG: " + txt); 
*/
        }

        @Override
        public void handleDeviceMessage(String dev, int id, String msg, 
                                                             String value) 
        {  
          String dev_name = format("%d",id);

          if (msg.equals("stopped"))
          { 
/*
            if (value.equals("ADAPTER_NOT_DETECTED")) {
              showToast(dev + "  " + id + " " + msg + " " + value);
            }
*/
           if (value.equals("OTHER_FAILURE") || 
                value.equals("CHANNEL_NOT_AVAILABLE"))
            { 
              log_title(format("ANT: %-7s %s",dev,value));
              ant_restart_scanning(value);
              return; // do not call update_device_message
             }
          }


          if (msg.equals("connect_failed"))
          { 
            log_title(format("ANT: %-7s %s",dev,msg));

            if (value.equals("SEARCH_TIMEOUT"))
            { ant_restart_scanning(value);
              return; // do not call update_device_message
             }
          }


          if (msg.equals("device"))
          { // new device detected (connect if dev_name = connect_name)

            //showToast("ANT+ device: " + dev_name);

            if (dev_name.equals(ant_hrate_connect_name))
              ant.connectToHeartRateDevice(dev_name);

            if (dev_name.equals(ant_power_connect_name))
              ant.connectToPowerDevice(dev_name);

            if (dev_name.equals(ant_cadence_connect_name))
              ant.connectToCadenceDevice(dev_name);

            if (dev_name.equals(ant_temp_connect_name))
              ant.connectToTempDevice(dev_name);
          }


          if (msg.equals("connected"))
          { 
            if (dev.equals("ant_hrate")) 
            { showToast("ANT+ HR-Sensor: " + dev_name);
              if (ant_hrate_connect_count == 0)
                 play_sound(sTracksActivity.SOUND_HRATE_SENSOR);
              ant_hrate_connect_name = dev_name;
              ant_hrate_connect_count++;
             }

            if (dev.equals("ant_power"))
            { showToast("ANT+ PWR-Sensor: " + dev_name);
              if (ant_power_connect_count == 0)
                   play_sound(sTracksActivity.SOUND_POWER_SENSOR);
              ant_power_connect_name = dev_name;
              ant_power_connect_count++;
             }

            if (dev.equals("ant_cadence"))
            { showToast("ANT+ CAD-Sensor: " + dev_name);
              ant_cadence_connect_name = dev_name;
              ant_cadence_connect_count++;
             }

            if (dev.equals("ant_temperature"))
            { showToast("ANT+ Temp-Sensor: " + dev_name);
              ant_temp_connect_name = dev_name;
              ant_temp_connect_count++;
             }
          }


/*
          if (msg.equals("disconnected"))
          { 
            if (dev.equals("ant_hrate")) 
            { showToast("ANT+ HR-Sensor: disconnected");
              //ant_hrate_connect_count = 0;
             }

            if (dev.equals("ant_power"))
            { showToast("ANT+ PWR-Sensor: disconnected");
              //ant_power_connect_count = 0;
             }

            if (dev.equals("ant_cadence"))
            { showToast("ANT+ CAD-Sensor: disconnected");
              //ant_cadence_connect_count = 0;
             }

            if (dev.equals("ant_temperature"))
            { showToast("ANT+ TMP-Sensor: disconnected");
              //ant_temp_connect_count = 0;
             }
           }
*/

          if (msg.equals("state"))  
          { 
/*
            if (value.equals("DEAD"))
              showToast(dev + "  " + id + " " + msg + " " + value);
*/
            if (dev.equals("ant_hrate")) 
              showToast(format("ANT+ HR %s : %s",dev_name,value));

            if (dev.equals("ant_power")) 
              showToast(format("ANT+ PWR %s : %s",dev_name,value));

            if (dev.equals("ant_cadence")) 
              showToast(format("ANT+ CAD %s : %s",ant_cadence_connect_name,value));
            if (dev.equals("ant_temperature")) 
              showToast(format("ANT+ TEMP %s : %s",ant_temp_connect_name,value));
/*
            if (value.equals("DEAD"))
            { log_title("ANT: Scanning DEAD");
              log("dev = " + dev);
              log("msg = " + msg);
              log("val = " + value);
              ant_restart_scanning(value);
              return;
             }
*/
          }

          update_device_message(dev,dev_name,msg,value);
        }


        @Override
        public void handleHeartRateEvent(int id, long t, int hrate, long hrnum)
        { update_hrate(hrate,(int)hrnum); }
  
        @Override
        public void handlePowerEvent(int id, long t, float power)
        { update_power(power); }

        @Override
        public void handlePowerTorqueEvent(int id, long t, float torque)
        { update_torque(torque); }

        @Override
        public void handlePowerWheelSpeedEvent(int id, long t, float speed)
        { if (indoor_fixed_speed == 0) {
             update_wheel_distance(current_wheel_dist, speed); 
          }
        }

        @Override
        public void handlePowerWheelDistanceEvent(int id, long t, float dist)
        { if (indoor_fixed_speed == 0) {
          update_wheel_distance(dist,current_wheel_speed); 
          }
        }

        @Override
        public void handlePowerCadenceEvent(int id, long t, float cadence)
        { update_cadence(cadence); }


        @Override
        public void handleCadenceEvent(int id, long t, float cadence)
        { update_cadence(cadence); }

        @Override
        public void handleTempEvent(int id, long t, float temp)
        { update_temperature(temp); }

      };
       
     ant.startScanning("all");
   }


   void check_ant_timeout(int timeout)
   {
      // reconnect ant devices after timeout sec without updates

     if (timeout <= 0) return;

     int count = 0;

     long d = 1000 * timeout;
     long t = get_current_time_millis();

     if (hrate_last_update_time > 0 && t > hrate_last_update_time+d)
     { log_title("ANT+ HR TIMEOUT " + timeout);
       count++;
      }

     if (power_last_update_time > 0 && t > power_last_update_time+d)
     { log_title("ANT+ PWR TIMEOUT " + timeout);
       count++;
      }

     if (cadence_last_update_time > 0 && t>cadence_last_update_time+d)
     { log_title("ANT+ CAD TIMEOUT " + timeout);
       count++;
      }

     if (count > 0) 
       ant_restart_scanning("TIMEOUT " + timeout + " sec");
   }


   void handleNmeaMessage(String msg, long t)
   {
     if (msg == null || msg.equals("")) return;

     //msg = msg.trim();


     String[] buf = msg.split(",");
/*
     if (buf.length > 2 && (buf[0].endsWith("RMC") || buf[0].endsWith("GGA"))) 
     { log_title("NmeaMessage");
       for(int i=0; i<buf.length; i++) {
         log(format("%d: %s",i,buf[i])); 
       }
       log("");
     }
*/

     if (buf.length > 2 && buf[0].endsWith("RMC")) 
     { 
      //log(msg);

      if (buf[2].equals("A")) { // ACTIVE
        gps_signal_lost_count = 0;
        update_gps_status(true,"NMEA:RMC"); 
       }
       else
       if (buf[2].equals("V")) { // VOID
         if (++gps_signal_lost_count == 10) update_gps_status(false,"NMEA:RMC");
       }
      return;
     }

     if (buf.length < 12 || !buf[0].endsWith("GGA")) return;

     //log(msg);

     // GGA  message
     // GPGGA,191410,4735.5634,N,00739.3538,E,1,04,4.4,351.5,M,48.0,M,,*45
     //  0: sentence type (gga)
     //  1: current time 
     //  2: latitude (DDMM.MMMM)
     //  3: N/S
     //  4: longitude (DDMM.MMMM)
     //  5: E/W
     //  6: Fix Type/Quality (0: no fix 1: gps  2: dgps)
     //  7: number of used satellites (4)
     //  8: horizontal dilutaion of precision (4.4)
     //  9: Altitude (351.5)
     // 10: unit (M: meter)
     // 11  geoid correction (48.0)
     // 12  unit (M: meter)


     if (buf[6].equals("1")) { 
       // gps fix
       gps_signal_lost_count = 0;
       update_gps_status(true,"NMEA:GGA"); 
     }

/*
     if (buf[6].equals("0")) 
       // fix not valid
       if (++gps_signal_lost_count == 10) update_gps_status(false,"NMEA:GGA");
     }
*/

     // get geoid correction

     float gc = 0;

     try {
       gc = Float.parseFloat(buf[11]);
     } catch(Exception ex) {}

     if (gc != 0 && gc != geoid_correction) {
       log_title("GEOID CORRECTION: " + gc);
       geoid_correction = gc;
     }

   }


   void speak(final String txt1, final String txt2, final int vol)
   {
/*
     log("");
     log_title("TextToSpeech");
     log("txt1 = " + txt1);
     log("txt2 = " + txt2);
     log("vol  = " + vol);
*/

     tts = new TextToSpeech(this, new TextToSpeech.OnInitListener() {

      @Override 
      public void onInit(int status) {

        if (status != TextToSpeech.SUCCESS) {
          log("TextToSpeech: Initialization failed.");
          return;
        }


        Locale locale;

        if (lang.equals("Deutsch")) {
          //locale = new Locale("de","DE");
          locale = Locale.GERMAN;
        }
        else
        { //locale = new Locale("en","US");
          locale = Locale.ENGLISH;
          tts.setSpeechRate(0.6f);
         }

        int result = tts.setLanguage(locale);

        //showToast("result = " + result);

        if (result == TextToSpeech.LANG_MISSING_DATA ||
            result == TextToSpeech.LANG_NOT_SUPPORTED) {
          log("tts: Language not supported");
          return;
        }

/*
          Set<String> features = new HashSet<String>();
          features.add("male");
          Voice voice = new Voice("en-us-x-sfg-local",
                                  new Locale("en","US"),
                                  400,200,false,features);
          tts.setVoice(voice);
*/


        tts.setOnUtteranceProgressListener( 
           new UtteranceProgressListener() {


               @Override
               public void onStart(String id) {
                 if (id.equals("tts_text1_id"))
                    am.setStreamVolume(stream_type_media,vol,0);
               }

               @Override
               public void onDone(String id) {
                 if (id.equals("tts_text2_id"))
                 { am.setStreamVolume(stream_type_media,stream_volume_media,0);
                   tts.shutdown();
                   tts = null;
                  }
               }

               @Override
               public void onError(String id) {
                  log("TEXTOSPEECH ERROR: id = " + id);
               }

               @Override
               public void onError(String id, int code) {
                  log("");
                  log("TEXTOSPEECH ERROR: id = " + id + " code = " + code);
                  log_title("Available Voices");
                  for(Voice v : tts.getVoices()) {
                     String name = v.getName();
                     //if (name.startsWith("en-us")) 
                     log(name);
                  }
                  log("");
               }

        });

        if (txt1 == null) return;

        HashMap<String,String> params1 = new HashMap<String,String>();
        params1.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,"tts_text1_id");
        tts.speak(txt1,TextToSpeech.QUEUE_ADD,params1);

        HashMap<String,String> params2 = new HashMap<String,String>();
        params2.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,"tts_text2_id");

        if (txt2 != null) 
          tts.speak(txt2,TextToSpeech.QUEUE_ADD,params2);
        else
          tts.playSilence(100,TextToSpeech.QUEUE_ADD,params2);

/*
        Bundle params1 = new Bundle();
        tts.speak(txt1,TextToSpeech.QUEUE_ADD,params1,null);
        if (txt2 != null)
          tts.playSilentUtterance(200,TextToSpeech.QUEUE_ADD,"tts_text1_id");
 
        Bundle params2 = new Bundle();
        if (txt2 != null) 
          tts.speak(txt2,TextToSpeech.QUEUE_ADD,params2,null);
        else
          tts.playSilentUtterance(100,TextToSpeech.QUEUE_ADD,"tts_text2_id");
*/
       }

     });

   }

   void read_waypoints()
   { 
     long t = System.currentTimeMillis();

     File[] wp_files = wp_folder.listFiles(new FileFilter() {
                               public boolean accept(File f) 
                               { return f.getName().endsWith(".wpt"); }
                       });

     int num = (wp_files == null) ? 0: wp_files.length;

     if (num > 0) 
     { waypoints = new WayPoint[num];
       for(int i=0; i<num; i++) 
       { WayPoint wp = new WayPoint();
         wp.read(wp_files[i]);
         waypoints[i] = wp;
         if (wp.getName().equals("Home")) home_loc = wp.getLocation();
        }
      }

     synchronized(this) { 
       log("");
       log_title("read_waypoints");
       log(format("%d  Waypoints   %d msec", num, 
                                             System.currentTimeMillis() - t));
     }

   }


/*
   void find_waypoint_files(Location loc, double dmax)
   {
     // find wp with distance < dmax

     wp_find_count++;

     log("");
     log_title(format("find_waypoint %d: %.1f m",wp_find_count,dmax));

     long t = System.currentTimeMillis();

     File[] wp_files = wp_folder.listFiles(new FileFilter() {
                               public boolean accept(File f) 
                               { return f.getName().endsWith(".wpt"); }
                       });

     if (wp_files == null || wp_files.length == 0)
     { wp_loc = null;
       wp_name = "null";
       return;
      }

     double dist = Double.MAX_VALUE;

     WayPoint wp = new WayPoint();

     // first check for Home waypoint
     File home_wpt = new File(wp_folder,"Home.wpt");
     if (home_wpt.exists()) {
        wp.read(home_wpt);
        dist = loc.distanceTo(wp.getLocation());
     }

     if (dist > dmax)  {
       int i = 0;
       while(i < wp_files.length) 
       { wp.read(wp_files[i]);
         double d = loc.distanceTo(wp.getLocation());
         if (d < dist) dist = d;
         if (d < dmax) break;
         i++;
        }
     }

     if (dist <= dmax)
     { wp_loc = wp.getLocation();
       wp_name = wp.getName();
      }
     else
     { wp_loc = null;
       wp_name = "null";
      }

     log(format("%s  %.2f m  %d msec",wp_name, dist,
                                               System.currentTimeMillis() - t));
     log("");
   }
*/


   void find_waypoint(Location loc, double dmax)
   { 
     // find wp with distance < dmax

     wp_find_count++;

     if (waypoints == null) return;

     long t = System.currentTimeMillis();

     double dist = Double.MAX_VALUE;
     WayPoint closest_wp = null;

     for(int i = 0; i<waypoints.length; i++)
     { WayPoint wp = waypoints[i];
       double d = loc.distanceTo(wp.getLocation());
       if (d < dist) {
         closest_wp = wp;
         dist = d;
       }
     }

     if (dist <= dmax)
     { wp_loc = closest_wp.getLocation();
       wp_name = closest_wp.getName();
      }
     else
     { wp_loc = null;
       wp_name = "null";
      }

     String status = (wp_loc != null) ? "ok" : "null";

     synchronized(this) {
       log("");
       log_title("find_waypoint " + wp_find_count);
       log(format("%s (%s) %.2f m  %d msec",closest_wp.getName(),status,dist,
                                            System.currentTimeMillis() - t));
     }

   }


   void load_current_course(String param1)
   {
     course = new Course() {
          @Override
            public void writeLog(String s) { log(s); }
     };

     course.load(current_crs_file);

//showToast("Course: sz = " + course.size());

     if (param1.equals("reverse")) {
       course.reverse();
       log("course reversed");
     }

     //pre-load srtm3 data 
     Location loc = course.getLocation(0);
     if(loc !=null) srtm3_matrix.getAltitude(loc);

     log("");
     log("Course " + course.getName());
     log("size = " + course.size());
     log("dist = " + course.getTotalDistance());
     log("asc0 = " + course.getTotalAscent());

     course.recompute_ascent();
     log("asc1 = " + course.getTotalAscent());

     course_current_p = -1;
     course_dist_i = 0;
   }




    @Override
    public void onCreate() 
    {
      super.onCreate();

      Context context = getBaseContext();

/*
      sender = new Sender(context) {
                @Override
                public void writeLog(String msg) { log(msg); }
      };
*/

      assets = new Assets(context);

      String svn_revision = assets.getString("revision.txt");
      long build_time = 1000*assets.getLong("buildtime.txt",0);
      String build_string = 
            new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(build_time);

      location_permission =
        checkSelfPermission(sTracksActivity.ACCESS_FINE_LOCATION) == 
                                           PackageManager.PERMISSION_GRANTED;

      client_version = 0.001f*PackageCompat.getVersionCode(this);

      if (sTracksActivity.USE_LEGACY_STORAGE)
      { File sdroot = Environment.getExternalStorageDirectory();
        stracks_folder = new File(sdroot,"sTracks");
       }
      else
        stracks_folder = getFilesDir();

      // folders should exist already
      gps_folder = new File(stracks_folder,"gps");
      log_folder = new File(stracks_folder,"log");
      tmp_folder = new File(stracks_folder,"tmp");
      srtm3_folder = new File(stracks_folder,"srtm3");
      wp_folder = new File(stracks_folder,"WayPoints");


      log_file = new File(log_folder,"service_log.txt");
      log_file_save = new File(log_folder,"service_log_save.txt");
      if (log_file.exists()) log_file.renameTo(log_file_save);

      bt_log_file = new File(log_folder,"bluetooth_log.txt");
      bt_log_file_save = new File(log_folder,"bluetooth_log_save.txt");
      if (bt_log_file.exists()) bt_log_file.renameTo(bt_log_file_save);


      current_gps_file = new File(gps_folder,"current_track.gps");
      current_crs_file = new File(stracks_folder,"current_course.trk");

      boolean restart = current_gps_file.exists();

      gps_writer = null;
      log_writer = null;
      bt_log_writer = null;

      try {
        log_writer = new FileWriter(log_file);
        bt_log_writer = new FileWriter(bt_log_file);
      } catch (IOException e) { showToast(e.toString()); }

      log("");
      log("-------------------------------");
      log(format("sTracks-%.3f DataService ",client_version));
      log(format("revision: %s",svn_revision));
      log(format("build:    %s",build_string));

      String tstr = restart ? "RESTART: " : "START: ";
      tstr += current_date_and_time();

      log("-------------------------------");
      log("");
      log(tstr);
      log("");


      log_title("start notification");

      icon = BitmapFactory.decodeResource(getResources(),
                                          R.drawable.crankset512a_white);

      gps_icon_off = MyBitmap.bitmap_from_image(getBaseContext(),
                                                R.drawable.location64_red,
                                                0xffa00000);

      gps_icon_on = MyBitmap.bitmap_from_image(getBaseContext(),
                                               R.drawable.location64_green,

                                               0xff00a000);


      notificationManager = getSystemService(NotificationManager.class);

      if (Build.VERSION.SDK_INT >= 26)
      {
        NotificationChannel channel = 
            new NotificationChannel(CHANNEL1_ID, CHANNEL1_NAME,
                                    NotificationManager.IMPORTANCE_LOW);
        channel.setSound(null,null);
        channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);

        notificationManager.createNotificationChannel(channel);

        // popup: channel2  (high importance)

        NotificationChannel channel2 = 
             new NotificationChannel(CHANNEL2_ID,CHANNEL2_NAME,
                                     NotificationManager.IMPORTANCE_HIGH);
        notificationManager.createNotificationChannel(channel2);
       }


       if (Build.VERSION.SDK_INT >= 26) 
         notificationBuilder = new Notification.Builder(this,CHANNEL1_ID);
       else
         notificationBuilder = new Notification.Builder(this);


       // continuing notifications if title not set  ?
       notificationBuilder.setContentTitle("sTracks");
       notificationBuilder.setContentText("Service started.");

       notificationBuilder.setVisibility(Notification.VISIBILITY_PUBLIC);

       notificationBuilder.setSmallIcon(R.drawable.bike32a);
     //notificationBuilder.setLargeIcon(icon);

       notificationBuilder.setOngoing(true);

       notrack_notify();

       // start foreground service (attach notification)

       if (Build.VERSION.SDK_INT >= 29) 
         startForeground(NOTIFICATION_ID1,
                         notificationBuilder.build(),
                         ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION);
/*
                         ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST);
*/
       else
         startForeground(NOTIFICATION_ID1, notificationBuilder.build());


      String prefs_file = sTracksActivity.PREFS_NAME;
      prefs = getSharedPreferences(prefs_file,0);

      read_config();

      totalAsc.setAscentEps(ascent_eps);
      totalAsc.setMinDist(point_mindist);


      new MyThread() {
          public void run() { read_waypoints(); }
      }.start();
     

      am = (AudioManager)getSystemService(Context.AUDIO_SERVICE);

      stream_type_notify = AudioManager.STREAM_NOTIFICATION;
      stream_volume_notify = am.getStreamVolume(stream_type_notify);

      stream_type_media = AudioManager.STREAM_MUSIC;
      stream_volume_media = am.getStreamVolume(stream_type_media);

      String url = xserver_host + srtm3_xs_path;

      log_title("SRTM3 Matrix");
      log("url = " + url);

      srtm3_matrix = new srtm3Matrix(srtm3_folder,null,url) {
              @Override
              public void write_log(String s) { 
               synchronized(this) { log(s); }
              }
      };

      location_buffer = new LocationBuffer();

      track_name_long = "";
      track_name = "";

      current_loc = null;


      log("");
      log_title("wake_lock");

      PowerManager pow = (PowerManager)getSystemService(Context.POWER_SERVICE);
      wake_lock =  pow.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                                                         "MyPowerManagerTag1");
      wake_lock.acquire();


       // barometer

       log_title("Barometer Listener");

       SensorManager sm = 
               (SensorManager)getSystemService(Context.SENSOR_SERVICE);

       barometer = sm.getDefaultSensor(Sensor.TYPE_PRESSURE);

       if (barometer != null)
       { SensorEventListener listener = new MySensorEventListener();
         sm.registerListener(listener, barometer, 
                                       SensorManager.SENSOR_DELAY_NORMAL);
/*
         sm.registerListener(listener, barometer, 
                                       1000000,
                                       1000000); // 1 sec
*/
        }
       else
       { log("NO BAROMETER");
         use_barometer_altitude = false;
        }


       bt_any = new Bluetooth(this,"any") {

         @Override
         public void writeLog(String txt) { log(txt); bt_log(txt); }

         @Override
         public void handle_message(String txt) {
           String name = getDeviceName();
           update_device_message("bt_any",name,"message",txt);
           //showToast(txt);
         }

         @Override
         public void update_device(String name,String addr,String status,
                                                           String data)
         { bt_log("");
           bt_log("update_device");
           bt_log("name = " + name);
           bt_log("addr = " + addr);
           bt_log("stat = " + status);
           bt_log("data = " + data);
           bt_any.disconnect();
           update_device_message("bt_log",bt_log_file.getPath(),"","");
          }

       };

       bt_any.setAutoConnect(false);
       bt_any.registerReceiver();



       bt_hrt = new Bluetooth(this,"hrt") {

         @Override
         public void writeLog(String txt) { 
            //if (!txt.equals("")) showToast(txt);
            log(txt); 
            bt_log(txt); 
         }


         @Override
         public void handle_message(String txt) { 
           String name = getDeviceName();
           update_device_message("bt_hrate",name,"message",txt);
           //showToast(txt);
         }
   
         @Override
         public void update_device(String name,String addr,String msg,String d) 
         { if (!bt_hrate_connect_name.equals("")) name = bt_hrate_connect_name;
           update_bt_device_message(this,"bt_hrate",name,msg,addr,
                                          sTracksActivity.SOUND_HRATE_SENSOR);
         }

   
         @Override
         public void update_data(String[] A)
         {
           if (A == null) {
              update_hrate(0,0);
              return;
           }

           if (A[0].equals("bat")) 
           { update_device_message("bt_hrate",getDeviceName(),"battery",A[1]);
             return;
           }

           if (!A[0].equals("hrt")) return;

           int hr = Integer.parseInt(A[1]);
           update_hrate(hr,0);

           return;
         }
       };

       bt_hrt.setAutoConnect(bt_hrate_auto_connect);
       bt_hrt.registerReceiver();


       bt_pwr = new Bluetooth(this,"pwr") {

         @Override
         public void writeLog(String txt) { log(txt); bt_log(txt); }

         @Override
         public void handle_message(String txt) { 
            String name = getDeviceName();
            update_device_message("bt_power",name,"message",txt);
            //showToast(txt);
         }
 
         @Override
         public void update_device(String name,String addr,String msg,String d)
         { if (!bt_power_connect_name.equals("")) name = bt_power_connect_name;
           update_bt_device_message(this,"bt_power",name,msg,addr,
                                          sTracksActivity.SOUND_POWER_SENSOR);
           start_crank_revolutions = -1;
           start_wheel_revolutions = -1;
           last_accumulated_torque = -1;
         }

 
         @Override
         public void update_data(String[] A) 
         { String name = getDeviceName();

           if (A == null) {
             update_power(0);
             return;
           }

           if (A[0].equals("bat")) 
           { update_device_message("bt_power",name,"battery",A[1]);
             return;
           }


           if (A[0].equals("ctrl")) {
             String value = A[1];
             update_device_message("bt_power",name,"zero offset",value);
             return;
           }


           if (!A[0].equals("pwr")) return;

           // time in msec


           int instant_power = Integer.parseInt(A[1]);
           int pedal_balance = Integer.parseInt(A[2]);
           int accumulated_torque = Integer.parseInt(A[3]);
           long wheel_revol_num = Long.parseLong(A[4]);
           int wheel_revol_time = Integer.parseInt(A[5]);
           int crank_revol_num = Integer.parseInt(A[6]);
           int crank_revol_time = Integer.parseInt(A[7]);

           // power
           update_power(instant_power);


          // torque
          if (accumulated_torque != -1)
          {
            if (last_accumulated_torque == -1) {
              last_accumulated_torque = accumulated_torque;
            }

            int torque = accumulated_torque - last_accumulated_torque;
            if (torque < 0) torque += 2048;
            last_accumulated_torque = accumulated_torque;
            update_torque(torque);
          }



          // crank revolutions
          if (crank_revol_num != -1)
          {
            if (start_crank_revolutions == -1) {
              log("CRANK REVOLUTIONS: " + crank_revol_num);
              start_crank_revolutions = crank_revol_num;
            }

            crank_revol_num -= start_crank_revolutions;
            if (crank_revol_num < 0) crank_revol_num += (1L << 16);

            int crank_dnum = crank_revol_num - current_crank_revolutions;

            int dtime = crank_revol_time - current_crank_revolution_t;
            if (dtime < 0) dtime += 64000;

            current_crank_revolutions = crank_revol_num;
            current_crank_revolution_t = crank_revol_time;

            if (crank_dnum == 0) {
              if (dtime == 0) update_cadence(0);
            }
            else
            if (dtime > 0) {
              float secs = 0.001f * dtime;
              update_cadence(60*crank_dnum/secs);
            }
          }


          // wheel revolutions
          if (wheel_revol_num != -1)
          {
            float wheel_circumference = 2.11f;

            if (start_wheel_revolutions == -1) {
              log("WHEEL REVOLUTIONS: " + wheel_revol_num);
              start_wheel_revolutions = wheel_revol_num;
            }

            wheel_revol_num -= start_wheel_revolutions;
            if (wheel_revol_num < 0) wheel_revol_num += (1L << 32);

            long wheel_dnum = wheel_revol_num - current_wheel_revolutions;

            int dtime = wheel_revol_time - current_wheel_revolution_t;
            if (dtime < 0) dtime += 32000;

            current_wheel_revolutions = wheel_revol_num;
            current_wheel_revolution_t = wheel_revol_time;

            float dist = wheel_circumference*current_wheel_revolutions;
            float speed = 0;
            if (wheel_dnum > 0 && dtime > 0) {
              float secs = 0.001f * dtime;
              speed = wheel_circumference*wheel_dnum/secs;  // meters per sec
            }

            if (indoor_fixed_speed == 0) update_wheel_distance(dist,speed);
          }

         }
       };

       bt_pwr.setAutoConnect(bt_power_auto_connect);
     //bt_pwr.setAutoControl(true);
       bt_pwr.setAutoControl(false);
       bt_pwr.registerReceiver();


       bt_cad = new Bluetooth(this,"cad") {

         @Override
         public void writeLog(String txt) { log(txt); bt_log(txt); }

         @Override
         public void handle_message(String txt) {
           String name = getDeviceName();
           update_device_message("bt_cadence",name,"message",txt);
           //showToast(txt);
         }
 
         @Override
         public void update_device(String name,String addr,String msg,String d)
         { update_bt_device_message(this,"bt_cadence",name,msg,addr,
                                          sTracksActivity.SOUND_POWER_SENSOR);
         }


         @Override
         public void update_data(String[] A) 
         {
           if (A == null) {
             update_cadence(0);
             return;
           }

           if (A[0].equals("bat")) 
           { update_device_message("bt_cadence",getDeviceName(),"battery",A[1]);
             return;
           }

           if (!A[0].equals("cad")) return;

           // time in msec

           int revol_num = Integer.parseInt(A[3]);
           int revol_time = Integer.parseInt(A[4]);

           int dnum = revol_num - current_crank_revolutions;

           int dtime = revol_time - current_crank_revolution_t;
           if (dtime < 0) dtime += 64000;

           current_crank_revolutions = revol_num;
           current_crank_revolution_t = revol_time;

           if (dnum == 0) 
             update_cadence(0);
           else
           if (dtime > 0) {
             float secs = 0.001f * dtime;
             update_cadence(60*dnum/secs);
           }

         }
 
      };

      bt_cad.setAutoConnect(bt_cadence_auto_connect);
      bt_cad.registerReceiver();


      bt_tmp = new Bluetooth(this,"tmp") {

         @Override
         public void writeLog(String txt) { log(txt); bt_log(txt); }

         @Override
         public void handle_message(String txt) { 
           String name = getDeviceName();
           update_device_message("bt_temperature",name,"message",txt);
           //showToast(txt);
         }
 
         @Override
         public void update_device(String name,String addr,String msg,String d)
         { update_bt_device_message(this,"bt_temperature",name,msg,addr,-1); 
          }

         @Override
         public void update_data(String[] A) 
         { 
           if (A == null) {
             update_temperature(0);
             return;
           }


           if (A[0].equals("bat")) 
           { update_device_message("bt_temperature",getDeviceName(),"battery",A[1]);
             return;
           }

           if (A[0].equals("tmp")) {
             int t = Integer.parseInt(A[1]);
             update_temperature(0.01f*t);
             return;
           }
         }
 
      };

      bt_tmp.setAutoConnect(bt_temp_auto_connect);
      bt_tmp.registerReceiver();



      bt_fit = new Bluetooth(this,"fit") {

         @Override
         public void writeLog(String txt) { log(txt); bt_log(txt); }

         @Override
         public void handle_message(String txt) { 
           String name = getDeviceName();
           update_device_message("bt_fitness",name,"message",txt);
           //showToast(txt);
         }
 
         @Override
         public void update_device(String name,String addr,String msg,String d)
         { update_bt_device_message(this,"bt_fitness",name,msg,addr,-1);
          }

         @Override
         public void update_data(String[] A) 
         { if (A == null) return;

           if (A[0].equals("bat")) 
           { update_device_message("bt_fitness",getDeviceName(),"battery",A[1]);
             return;
           }

           for(int i=0; i<A.length; i++) {
           }
         }
 
      };

      bt_fit.setAutoConnect(true);
      bt_fit.registerReceiver();




       // location listener

       log_title("Location Manager");
       locationManager = 
                (LocationManager) getSystemService(Context.LOCATION_SERVICE);


       // test if gps service is running
   
       //if (!isServiceRunning(GpsService.class.getName())) 
       if (!GpsService.isRunning)
       { try {
            //showToast("Remove Test Provider");
            locationManager.removeTestProvider(LocationManager.GPS_PROVIDER);
          } catch (Exception e) {}
       }


       boolean provider_enabled = 
          locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);

       log_title("GPS PROVIDER: " + (provider_enabled ? "enabled":"disabled"));

       if (!provider_enabled)
       {
         showToast(string_gps_deactivated);

         final Intent intent = 
                        new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

         handler.postDelayed(new Runnable() {
                public void run() { startActivity(intent); }
             },2000);
       }


       log_title("Add NMEA Listener");

       boolean nmea_ok = false;

       if (Build.VERSION.SDK_INT >= 24) 
       {  nmea_msg_listener = new OnNmeaMessageListener() {
                     @Override
                     public void onNmeaMessage(String msg, long t) {
                         handleNmeaMessage(msg,t);
                     }
          };

         nmea_ok = locationManager.addNmeaListener(nmea_msg_listener);
       }
       else {
        nmea_listener = new GpsStatus.NmeaListener() {
                  @Override
                  public void onNmeaReceived(long t, String msg) {
                       handleNmeaMessage(msg,t);
                  }
         };

        //nmea_ok = locationManager.addNmeaListener(nmea_listener);
        nmea_ok = addNmeaListenerReflection(nmea_listener); 
       }

       if (!nmea_ok) log("Adding NMEA-Listener failed");

       gps_available = false;

       log_title("Location Listener");

       locationListener = new MyLocationListener();


       try {
       locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 
                                              locationMinTime,
                                              locationMinDist,
                                              locationListener);
       } catch(Exception e) { 
            log("requestLocationUpdates: " + e.toString()); 
         }


      // set current location to last known location (if available)

      log("");
      log("Last Known Locations");

      Location gps_loc = 
          locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);

      if (gps_loc != null)
      { // geoid correction
        gps_loc.setAltitude(gps_loc.getAltitude() - geoid_correction);

        Date d = new Date(gps_loc.getTime());
        String s = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(d);
        log("GPS " + s);
        log(format("%.6f / %.6f / %3.0f acc: %.0f", gps_loc.getLatitude(),
                                                    gps_loc.getLongitude(),
                                                    gps_loc.getAltitude(),
                                                    gps_loc.getAccuracy()));
        log("");
       }

      Location net_loc = 
         locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);

      if (net_loc != null)
      { Date d = new Date(net_loc.getTime());
        String s = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(d);
        log("NET " + s);
        log(format("%.6f / %.6f / %3.0f acc: %.0f", net_loc.getLatitude(),
                                                    net_loc.getLongitude(),
                                                    net_loc.getAltitude(),
                                                    net_loc.getAccuracy()));
        log("");
       }

      long net_time = (net_loc == null) ? 0 : net_loc.getTime();
      long gps_time = (gps_loc == null) ? 0 : gps_loc.getTime();

      last_known_loc =  (net_time > gps_time)? net_loc : gps_loc;

      if (last_known_loc == null) {
        if (home_loc != null) 
          last_known_loc = home_loc;
        else
          last_known_loc = new Location("gps");
/*
          last_known_loc.setLatitude(49.78763);
          last_known_loc.setLongitude(6.71185);
          last_known_loc.setAltitude(165);
*/
          last_known_loc.setLatitude(49.78458);
          last_known_loc.setLongitude(6.70925);
          last_known_loc.setAltitude(130);
      }

      current_loc = last_known_loc;

      if (use_srtm3_altitude && last_known_loc != null) 
      { //pre-load srtm3 data for last known position
        srtm3_matrix.getAltitude(last_known_loc);
       }



      // battery broadcast receiver

       battery_receiver = new BroadcastReceiver() {
         public void onReceive(Context context, Intent bstatus) {
           float level = bstatus.getIntExtra(BatteryManager.EXTRA_LEVEL,-1);
           float scale = bstatus.getIntExtra(BatteryManager.EXTRA_SCALE,-1);

         //int status = bstatus.getIntExtra(BatteryManager.EXTRA_STATUS,-1);
         //battery_full     = status == BatteryManager.BATTERY_STATUS_FULL;
         //battery_charging = status == BatteryManager.BATTERY_STATUS_CHARGING;

           if (level >= 0 && scale > 0)
              battery_level = level/scale;
           else
              battery_level = -1;

           }
       };


       registerReceiver(battery_receiver, 
                        new IntentFilter(Intent.ACTION_BATTERY_CHANGED));


        connectivity_receiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {

          boolean wifi_c = false;
          boolean mobile_c = false;

          ConnectivityManager 
           cm = (ConnectivityManager)context.getSystemService(
                                                Context.CONNECTIVITY_SERVICE);

/*
          if (Build.VERSION.SDK_INT >= 29) 
          if (Build.VERSION.SDK_INT >= 23) 
*/
          {
            Network active_net = cm.getActiveNetwork();
      
            if (active_net != null) 
            { NetworkCapabilities ncap = cm.getNetworkCapabilities(active_net);
              if (ncap != null)  {
                if (ncap.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
                 wifi_c = true;
                if (ncap.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR))
                 mobile_c = true;

            //if (ncap.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) 
            //if (ncap.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH))
               }
             }
           }
/*
         else 
         { NetworkInfo active_network = cm.getActiveNetworkInfo();
           if (active_network != null)
           { if (active_network.getType() == ConnectivityManager.TYPE_WIFI)
               wifi_c = true;
             if (active_network.getType() == ConnectivityManager.TYPE_MOBILE)
               mobile_c = true;
            }
         }
*/
  
         if (wifi_c != wifi_connected)
         { String msg = wifi_c  ? "available" : "unavailable";
           wifi_connected = wifi_c;
           update_device_message("network", "", msg, "wifi");
          }

         if (mobile_c != mobile_connected)
         { String msg = mobile_c  ? "available" : "unavailable";
           mobile_connected = mobile_c;
           update_device_message("network", "", msg,"mobile");
          }

         network_connected = wifi_connected || mobile_connected;
       }

     };


       registerReceiver(connectivity_receiver,
                    new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));


       if (restart)
       { log_title("Service Restart");
         reload_gps();
         track_resume();
         send_tracking_cmd("service_restart " + current_date_and_time());
       }


       tick_count = 0;

       cdt = new CountDownTimer(100000000, 1000) {
             @Override
             public void onTick(long ms) 
             { if (!cdt_enabled) return;
               update_location(); 
               update_notification();
               if (tick_count % 60 == 0) {
                if (bt_tmp.getDeviceAddress() != null) bt_tmp.read();
                //check_ant_timeout(ant_restart_timeout);
               }
               tick_count++;
              }

             @Override
             public void onFinish() {}
        };

       cdt_enabled = true;
       cdt.start();

       update_wearable();


       log("");
       log_title("Start ANT+ Scanning.");

       new MyThread() {
          public void run() {
             handler.post(new Runnable() {
                public void run() { startAnt(); }
             });
          }
       }.start();


       log("");
       log_title("Connect BT devices.");

       new MyThread() {
          public void run() {
            //sleep(500);

            if (!bt_hrate_connect_address.equals("")) {
              //showToast("HR " + bt_hrate_connect_address);
              bt_hrt.connect(bt_hrate_connect_address);
            }

            if (!bt_power_connect_address.equals("")) {
              bt_pwr.connect(bt_power_connect_address);
            }

            if (!bt_cadence_connect_address.equals("")) {
              bt_cad.connect(bt_cadence_connect_address);
            }

            if (!bt_temp_connect_address.equals("")) {
              bt_tmp.connect(bt_temp_connect_address);
            }

            if (!bt_fitness_connect_address.equals("")) {
              bt_fit.connect(bt_fitness_connect_address);
            }
          }
       }.start();


      // do some checks

      if (!gps_folder.exists()) 
         notifyPopup("sTracks", "Folder not existing.");


/*

    // register broadcast receiver for wearable Listener

     BroadcastReceiver receiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent)
         { String action = intent.getAction();
           String msg = intent.getStringExtra("message");
           //showToast("Wearable: " + msg);
          }
      };

     IntentFilter intentFilter = new IntentFilter();
     intentFilter.addAction(ListenerService.WEARABLE_MESSAGE);

     if (Build.VERSION.SDK_INT >= 26)
       context.registerReceiver(receiver,intentFilter,
                                         context.RECEIVER_NOT_EXPORTED);
     else
       context.registerReceiver(receiver,intentFilter);
*/

     log_title("onCreate finished");

   }



   @Override
   public int onStartCommand(Intent intent, int flags, int startId) 
   { 
     super.onStartCommand(intent,START_STICKY,startId);

/*
     boolean fg = isForegroundService();
     log_title("isForegroundService = " + fg);
*/

     // update sTracks account

     String cmd = "";
     String param1 = "";
     String param2 = "";

     if (intent != null) // can it be null ? 
     { if (intent.hasExtra("cmd")) cmd = intent.getStringExtra("cmd");
       if (intent.hasExtra("param1")) param1 = intent.getStringExtra("param1");
       if (intent.hasExtra("param2")) param2 = intent.getStringExtra("param2");
      }

     //showToast(cmd);

     log("");
     if (cmd.equals("update_config"))
       log_title("cmd: " + cmd);
     else
       log_title("cmd: " + cmd + " " + param1 + " " + param2);

     if (cmd.equals("crash")) 
     { MyThread thread = new MyThread() {
          public void run() { sleep(1000); play_sound(null); }
       };
       thread.start();
      }
     else
     if (cmd.equals("load_current_course"))
       load_current_course(param1);
     else
     if (cmd.equals("update_config"))
       update_config(param1);
     else
     if (cmd.equals("login")) {
       user_name = param1;
       tracking_name = param1;
      }
     else
     if (cmd.equals("logout")) {
       user_name = "";
       tracking_name = "";
      }
     else
     if (cmd.equals("gps")) {

       if (param1.equals("enable")) 
       { gps_enabled = true;
         gps_available = false;
         locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 
                                                locationMinTime,
                                                locationMinDist,
                                                locationListener);
        }

       if (param1.equals("disable")) 
       { gps_enabled = false;
         if (ant != null) ant.resetWheelDistance();
         update_gps_status(false,"cmd");
        }
     }
     else
     if (cmd.equals("baro_calibrate")) {
       float alt = Float.parseFloat(param1);
       baro_calibrate("User",alt,null,0,0);
     }
     else
     if (cmd.equals("sound")) {
         stop_sound();
         if (param1.equals("")) param1 = "default";
         int save = acoustic_signals_volume;
         if (!param2.equals("")) 
           acoustic_signals_volume = Integer.parseInt(param2);
         play_sound(param1);
         acoustic_signals_volume = save;
     }
     else
     if (cmd.equals("speak")) {
        //speak(param1,param2);
        play_sound("tts:" + param1 + ":" + param2);
     }
     else
     if (cmd.equals("bt_connect")) {
       if (param1.equals("any")) bt_any.connect(param2);
       if (param1.equals("hrt")) bt_hrt.connect(param2);
       if (param1.equals("pwr")) bt_pwr.connect(param2);
       if (param1.equals("cad")) bt_cad.connect(param2);
       if (param1.equals("tmp")) bt_tmp.connect(param2);
       if (param1.equals("fit")) bt_fit.connect(param2);
     }
     else
     if (cmd.equals("bt_status")) {
       if (param1.equals("any")) bt_any.status(param2);
       if (param1.equals("hrt")) bt_hrt.status(param2);
       if (param1.equals("pwr")) bt_pwr.status(param2);
       if (param1.equals("cad")) bt_cad.status(param2);
       if (param1.equals("tmp")) bt_tmp.status(param2);
       if (param1.equals("fit")) bt_fit.status(param2);
     }
     if (cmd.equals("bt_control")) {
       //assert: param2.startsWith("0x")
       if (param1.equals("pwr")) bt_pwr.writeControl("Zero Offset",param2);
/*
       if (param1.equals("hrt")) bt_hrt.writeControl("",param2);
       if (param1.equals("cad")) bt_cad.writeControl("",param2);
       if (param1.equals("tmp")) bt_tmp.writeControl("",param2);
       if (param1.equals("fit")) bt_fit.writeControl("",param2);
*/
     }
     else
     if (cmd.equals("bt_disconnect")) {
       if (param1.equals("any") || param1.equals("all")) bt_any.disconnect();
       if (param1.equals("hrt") || param1.equals("all")) bt_hrt.disconnect();
       if (param1.equals("pwr") || param1.equals("all")) bt_pwr.disconnect();
       if (param1.equals("cad") || param1.equals("all")) bt_cad.disconnect();
       if (param1.equals("tmp") || param1.equals("all")) bt_tmp.disconnect();
       if (param1.equals("fit") || param1.equals("all")) bt_fit.disconnect();
     }
     else
     if (cmd.equals("ant_update")) {
           ant.updateStatus();
     }
     else
     if (cmd.equals("ant_start")) {

       current_hrate = 0;

       if (param1.equals("all"))
       { //showToast("ANT: Scanning for Devices.");
         ant.startScanning("all");
         hrate_last_update_time = 0;
         power_last_update_time = 0;
         cadence_last_update_time = 0;
        }
       else
       if (param1.equals("hrate"))
       { ant.startScanning("hrate");
         hrate_last_update_time = 0;
        }
       else
       if (param1.equals("power"))
       { ant.startScanning("power");
         power_last_update_time = 0;
        }
       else
       if (param1.equals("cadence"))
       { ant.startScanning("cadence");
         cadence_last_update_time = 0;
        }
     }
     else
     if (cmd.equals("ant_stop"))  {
       if (param1.equals("all")) ant.stopScanning("all");
       else
       if (param1.equals("hrate")) ant.stopHeartRateScanning();
       else
       if (param1.equals("power")) ant.stopPowerScanning();
       else
       if (param1.equals("cadence")) ant.stopCadenceScanning();
      }
     else
     if (cmd.equals("ant_restart"))  {
        if (param1.equals("disconnect"))
        { ant_hrate_connect_name = "";
          ant_hrate_connect_count = 0;
          ant_power_connect_name = "";
          ant_power_connect_count = 0;
          ant_cadence_connect_name = "";
          ant_cadence_connect_count = 0;
          ant_temp_connect_name = "";
          ant_temp_connect_count = 0;
        }

        ant_restart_scanning("user reset");
      }
     else
     if (cmd.equals("ant_connect"))  {
         if (param1.equals("hrate"))   ant.connectToHeartRateDevice(param2);
         if (param1.equals("power"))   ant.connectToPowerDevice(param2);
         if (param1.equals("cadence")) ant.connectToCadenceDevice(param2);
         if (param1.equals("temp"))    ant.connectToTempDevice(param2);
      }
     else
     if (cmd.equals("ant_disconnect"))  {
         if (param1.equals("hrate")) {
            ant_hrate_connect_name = "";
            hrate_last_update_time = 0;
            ant.disconnectHeartRateDevice(param2);
         }
         else
         if (param1.equals("power")) { 
            ant_power_connect_name = "";
            power_last_update_time = 0;
            ant.disconnectPowerDevice(param2);
         }
         else
         if (param1.equals("cadence")) {
            ant_cadence_connect_name = "";
            cadence_last_update_time = 0;
            ant.disconnectCadenceDevice(param2);
         }
         else
         if (param1.equals("temp")) {
            ant_temp_connect_name = "";
            temp_last_update_time = 0;
            ant.disconnectTempDevice(param2);
         }
      }
     else
     if (cmd.equals("ant_pwr_calibrate"))
       ant.pwrManualCalibration();
     else
     if (cmd.equals("ant_pwr_auto_zero")) 
         ant.pwrSetAutoZero(true);
     else
     if (cmd.equals("exit") || (cmd.equals("disconnect") && gps_writer == null))
     { update_wearable();
       send_tracking_cmd("service_stop"); 
       stopSelf();
     }
     else
     if (cmd.equals("stop_self"))
       stopSelf();
     else
     if (cmd.equals("connect"))
     { 
       // update gps and network status

       MyThread thread = new MyThread() {
          public void run() {

             // give client time to be ready to receive messages
             //sleep(2000);
             sleep(1000);

             String msg = gps_available ? "available" : "unavailable";
             update_device_message("gps", "", msg, "start");

             msg  = wifi_connected ? "available" : "unavailable";
             update_device_message("network", "", msg, "wifi");

             msg  = mobile_connected ? "available" : "unavailable";
             update_device_message("network", "", msg, "mobile");
          }
        };

        thread.start();

        if (gps_writer != null) write_gps_line("#resuming");
      }
      else
      if (cmd.equals("begin"))
        track_begin(param1);
      else
      if (cmd.equals("resume"))
        track_resume();
/*
      if (cmd.equals("begin") || cmd.equals("resume"))
      { track_name_long = param1;
        track_name = param1;
        if (track_name.length() > 16) track_name = track_name.substring(0,16);

        if (cmd.equals("begin"))
           track_begin();
        else
           track_resume();
       }
*/
      else
      if (cmd.equals("finish"))
        track_finish();
      else
      if (cmd.equals("stop"))
        track_stop();
      else
      if (cmd.equals("start"))
      { if (param1.equals("indoor"))
        { indoor_mode = true;
          course_current_p = -1;
          float spd = Float.parseFloat(param2);
          indoor_fixed_speed = spd;
          current_wheel_speed = spd;
          log(format("indoor_fixed_speed = %.2f m/s",indoor_fixed_speed));
          srtm3_matrix.set_auto_download(false);
        }
        track_start();
       }
      else
      if (cmd.equals("lap"))
      { play_sound(sTracksActivity.SOUND_LAP_START);
        write_gps_line("#lap");
       }

      update_notification();

      return START_STICKY;
    }



   @Override
   public void onDestroy() 
   {
     log_title("onDestroy");

/*
     if (sender.getConnectedDevice() != null) {
        log("WEARABLE: FINISH");
        sender.sendMessage("FINISH");
     }
*/

     cdt.cancel();

     notificationManager.cancel(NOTIFICATION_ID1);
     notificationManager.cancel(NOTIFICATION_ID2);

     if (Build.VERSION.SDK_INT >= 24)
     { if (gnssStatusCallback != null)
         locationManager.unregisterGnssStatusCallback(gnssStatusCallback);
       if (nmea_msg_listener != null)
         locationManager.removeNmeaListener(nmea_msg_listener);
     }
     else {
       if (gpsStatusListener != null)
         locationManager.removeGpsStatusListener(gpsStatusListener);
       if (nmea_listener != null) {
         //locationManager.removeNmeaListener(nmea_listener);
         removeNmeaListenerReflection(nmea_listener);
       }
     }

     locationManager.removeUpdates(locationListener);

     log("Disconnect BT-Devices");

     bt_hrt.disconnect();
     bt_hrt.unregisterReceiver();

     bt_pwr.disconnect();
     bt_pwr.unregisterReceiver();
     bt_pwr.close();

     bt_cad.disconnect();
     bt_cad.unregisterReceiver();
     bt_cad.close();

     bt_tmp.disconnect();
     bt_tmp.unregisterReceiver();
     bt_tmp.close();

     bt_fit.disconnect();
     bt_fit.unregisterReceiver();
     bt_fit.close();

     bt_any.disconnect();
     bt_any.unregisterReceiver();
     bt_any.close();
   

     log("Unregister Receivers");
     unregisterReceiver(battery_receiver);
     unregisterReceiver(connectivity_receiver);

     log("Release Wake Lock");
     wake_lock.release();

/*
     // stop provider service
     Intent intent = new Intent(this,ProviderService.class);
     stopService(intent);
*/

     super.onDestroy();

     // kill process (in a daemon thread)

     log("Kill Process");

     MyThread thread = new MyThread() {
          public void run() {
             sleep(500); 
             Process.killProcess(Process.myPid());
          }
     };

    thread.setDaemon(true);
    thread.start();

   }


   @Override
   public void onConfigurationChanged(Configuration config) 
   { log_title("onConfigurationChanged");
     //log(config.toString());
     super.onConfigurationChanged(config);
    }

   @Override
   public void onTaskRemoved(Intent intent) 
   { log_title("onTaskRemoved");
     super.onTaskRemoved(intent);
    }


   @Override
   public void onLowMemory() 
   { log_title("onLowMemory");
     super.onLowMemory();
    }


   @Override
   public void onTrimMemory(int level) 
   { if (level != trim_memory_level)
       log_title("onTrimMemory  level = " + level);
     trim_memory_level = level;
     super.onTrimMemory(level);
    }


  @Override
  public IBinder onBind(Intent intent) { 
     log_title("onBind");
     return null;
  }

}
