package de.dennisguse.opentracks.services.handlers; import android.content.Context; import android.location.LocationManager; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import java.time.Duration; import java.time.Instant; import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.settings.PreferencesUtils; /** * This class handle GPS status according to received locations` and some thresholds. */ class GpsStatusManager { private static final String TAG = GpsStatusManager.class.getSimpleName(); // The duration that GpsStatus waits from minimal interval to consider GPS lost. @VisibleForTesting public static final Duration SIGNAL_LOST_THRESHOLD = Duration.ofSeconds(30); private Distance horizontalAccuracyThreshold; // Threshold for time without points. private Duration signalLostThreshold; private GpsStatusValue gpsStatus = GpsStatusValue.GPS_NONE; private GpsStatusListener client; private final Context context; @Nullable private TrackPoint lastTrackPoint = null; // Flag to prevent GpsStatus checks two or more locations at the same time. private boolean checking = false; private Handler handler; public final Runnable gpsStatusTimer = () -> { determineGpsStatusByTime(Instant.now()); //TODO Get now via TrackPointCreator? }; public GpsStatusManager(Context context, GpsStatusListener client, Handler handler) { this.client = client; this.context = context; this.handler = handler; onRecordingDistanceChanged(PreferencesUtils.getRecordingDistanceInterval()); onMinRecordingIntervalChanged(PreferencesUtils.getMinRecordingInterval()); } public void start() { client.onGpsStatusChanged(GpsStatusValue.GPS_ENABLED); } /** * The client that uses GpsStatus has to call this method to stop the Runnable if needed. */ public void stop() { client.onGpsStatusChanged(GpsStatusValue.GPS_NONE); client = null; handler = null; } /** * Method to change the bad threshold from outside. * * @param value New preference value to signalBadThreshold. */ public void onRecordingDistanceChanged(@NonNull Distance value) { horizontalAccuracyThreshold = value; } public void onMinRecordingIntervalChanged(Duration value) { signalLostThreshold = SIGNAL_LOST_THRESHOLD.plus(value); //TODO Reschedule gpsStatusTimer? } /** * This method must be called from the client every time a new trackPoint is received. * Receive new trackPoint and calculate the new status if needed. * It look for GPS changes in lastLocation if it's not null. If it's null then look for in lastValidLocation if any. */ //TODO Remove checking; should be synchronized if this is a problem. public void onNewTrackPoint(@NonNull final TrackPoint trackPoint) { if (checking) { return; } checking = true; lastTrackPoint = trackPoint; determineGpsStatusOnTrackpoint(trackPoint); checking = false; } /** * Checks if lastLocation has new GPS status looking up time and accuracy. * It depends of signalLostThreshold and signalBadThreshold. * If there is any change then it does the change. * Also, it'll run the runnable if signal is bad or stop it if the signal is lost. */ //TODO use MonotonicClock instead of Instant.now() @VisibleForTesting void determineGpsStatusOnTrackpoint(@NonNull TrackPoint lastTrackPoint) { if (lastTrackPoint.fulfillsAccuracy(horizontalAccuracyThreshold)) { if (gpsStatus != GpsStatusValue.GPS_SIGNAL_FIX) { setGpsStatus(GpsStatusValue.GPS_SIGNAL_FIX); scheduleTimer(); //TODO } } else { // GPS signal is to weak; TODO we might need a time-based threshold here as well (i.e., warn after Duration) if (gpsStatus != GpsStatusValue.GPS_SIGNAL_BAD) { setGpsStatus(GpsStatusValue.GPS_SIGNAL_BAD); scheduleTimer(); } } } void determineGpsStatusByTime(Instant now) { if (lastTrackPoint == null) { return; } if (signalLostThreshold.minus(Duration.between(lastTrackPoint.getTime(), now)).isNegative()) { // Too much time without receiving signal -> signal lost. if (gpsStatus != GpsStatusValue.GPS_SIGNAL_LOST) { setGpsStatus(GpsStatusValue.GPS_SIGNAL_LOST); } return; } scheduleTimer(); } /** * This method must be called from the client every time the GPS sensor is enabled. * Anyway, it checks that GPS is enabled because the client assumes that if it's on then GPS is enabled but user can disable GPS by hand. */ public void onGpsEnabled() { if (gpsStatus == GpsStatusValue.GPS_ENABLED) { return; } LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); if (locationManager != null && locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { setGpsStatus(GpsStatusValue.GPS_ENABLED); scheduleTimer(); } else { onGpsDisabled(); } } /** * This method must be called from service every time the GPS sensor is disabled. */ public void onGpsDisabled() { if (gpsStatus == GpsStatusValue.GPS_DISABLED) { return; } setGpsStatus(GpsStatusValue.GPS_DISABLED); lastTrackPoint = null; stopTimer(); } private void setGpsStatus(GpsStatusValue current) { gpsStatus = current; if (client != null) { client.onGpsStatusChanged(current); } } private void scheduleTimer() { handler.removeCallbacks(gpsStatusTimer); handler.postDelayed(gpsStatusTimer, signalLostThreshold.toMillis()); } private void stopTimer() { handler.removeCallbacks(gpsStatusTimer); } public interface GpsStatusListener { void onGpsStatusChanged(GpsStatusValue currentStatus); } }