Cleanup: move GpsManager into sensors package.

This commit is contained in:
Dennis Guse
2025-02-13 22:29:08 +01:00
parent aadceede2c
commit a6d318d970
10 changed files with 25 additions and 25 deletions
@@ -41,9 +41,9 @@ import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.GpsStatusValue;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.announcement.VoiceAnnouncementManager;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
@@ -1,184 +0,0 @@
package de.dennisguse.opentracks.services.handlers;
import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.location.LocationListenerCompat;
import androidx.core.location.LocationManagerCompat;
import androidx.core.location.LocationRequestCompat;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.SensorConnector;
import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorGPS;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class GPSManager implements SensorConnector, LocationListenerCompat, GpsStatusManager.GpsStatusListener, SharedPreferences.OnSharedPreferenceChangeListener {
private final String TAG = GPSManager.class.getSimpleName();
private static final String LOCATION_PROVIDER = LocationManager.GPS_PROVIDER;
private TrackPointCreator trackPointCreator;
private SensorManager.SensorDataChangedObserver observer;
private Context context;
private Handler handler;
private LocationManager locationManager;
private GpsStatusManager gpsStatusManager;
private Duration gpsInterval;
private Distance thresholdHorizontalAccuracy;
public GPSManager(TrackPointCreator trackPointCreator, SensorManager.SensorDataChangedObserver observer) {
this.trackPointCreator = trackPointCreator;
this.observer = observer;
}
public void start(@NonNull Context context, @NonNull Handler handler) {
this.context = context;
this.handler = handler;
onSharedPreferenceChanged(null, null);
gpsStatusManager = new GpsStatusManager(context, this, handler);
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
observer.onConnect(new AggregatorGPS("internal"));
registerLocationListener();
gpsStatusManager.start();
}
private boolean isStarted() {
return locationManager != null;
}
@SuppressWarnings({"MissingPermission"})
public void stop(Context context) {
if (isStarted()) {
LocationManagerCompat.removeUpdates(locationManager, this);
}
locationManager = null;
this.context = null;
handler = null;
gpsStatusManager.stop();
gpsStatusManager = null;
observer.onDisconnect(new AggregatorGPS("internal"));
observer = null;
trackPointCreator = null;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
boolean registerListener = false;
if (PreferencesUtils.isKey(R.string.min_sampling_interval_key, key)) {
registerListener = true;
gpsInterval = PreferencesUtils.getMinSamplingInterval();
if (gpsStatusManager != null) {
gpsStatusManager.onMinSamplingIntervalChanged(gpsInterval);
}
}
if (PreferencesUtils.isKey(R.string.recording_gps_accuracy_key, key)) {
thresholdHorizontalAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy();
}
if (PreferencesUtils.isKey(R.string.recording_distance_interval_key, key)) {
registerListener = true;
if (gpsStatusManager != null) {
Distance gpsMinDistance = PreferencesUtils.getRecordingDistanceInterval();
gpsStatusManager.onRecordingDistanceChanged(gpsMinDistance);
}
}
if (registerListener && isStarted()) {
registerLocationListener();
}
}
/**
* Checks if location is valid and builds a track point that will be send through TrackPointCreator.
*
* @param location {@link Location} object.
*/
@Override
public void onLocationChanged(@NonNull Location location) {
// Send each update to the status; please note that this TrackPoint is not stored.
TrackPoint trackPoint = new TrackPoint(location, trackPointCreator.createNow());
gpsStatusManager.onNewTrackPoint(trackPoint);
if (!LocationUtils.isValidLocation(location)) {
Log.w(TAG, "Ignore newTrackPoint. location is invalid.");
return;
}
if (!LocationUtils.fulfillsAccuracy(location, thresholdHorizontalAccuracy)) {
Log.d(TAG, "Ignore newTrackPoint. Poor accuracy.");
return;
}
observer.onChange(new Raw<>(observer.getNow(), Position.of(location)));
}
@Override
public void onStatusChanged(@NonNull String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(@NonNull String provider) {
gpsStatusManager.onGpsEnabled();
}
@Override
public void onProviderDisabled(@NonNull String provider) {
gpsStatusManager.onGpsDisabled();
}
private void registerLocationListener() {
if (!LocationManagerCompat.hasProvider(locationManager, LOCATION_PROVIDER)) {
Log.e(TAG, "Device doesn't have GPS.");
return;
}
LocationRequestCompat locationRequest = new LocationRequestCompat.Builder(gpsInterval.toMillis())
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
.setMaxUpdateDelayMillis(0)
.build();
if (PermissionRequester.GPS.hasPermission(context)) {
try {
Log.i(TAG, "Register for location updates " + context);
LocationManagerCompat.requestLocationUpdates(locationManager, LOCATION_PROVIDER, locationRequest, handler::post, this);
} catch (SecurityException e) {
Log.e(TAG, "Could not register location listener; permissions not granted.", e);
}
}
}
@Override
public void onGpsStatusChanged(GpsStatusValue currentStatus) {
trackPointCreator.sendGpsStatus(currentStatus);
}
}
@@ -1,181 +0,0 @@
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;
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());
onMinSamplingIntervalChanged(PreferencesUtils.getMinSamplingInterval());
}
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() {
stopTimer();
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 onMinSamplingIntervalChanged(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.
*/
public void onNewTrackPoint(@NonNull final TrackPoint trackPoint) {
lastTrackPoint = trackPoint;
determineGpsStatusOnTrackpoint(trackPoint);
}
/**
* 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);
}
}
@@ -1,27 +0,0 @@
package de.dennisguse.opentracks.services.handlers;
import de.dennisguse.opentracks.R;
/**
* For each status value set an icon and id message.
*/
public enum GpsStatusValue {
GPS_NONE(R.drawable.ic_gps_off_24dp, R.string.gps_disabled_msg),
GPS_ENABLED(R.drawable.ic_gps_not_fixed_24dp, R.string.gps_wait_for_signal),
GPS_DISABLED(R.drawable.ic_gps_off_24dp, R.string.gps_disabled_msg),
GPS_SIGNAL_FIX(R.drawable.ic_gps_fixed_24dp, R.string.gps_fixed_and_ready),
GPS_SIGNAL_BAD(R.drawable.ic_gps_ready_animation, R.string.gps_wait_for_better_signal),
GPS_SIGNAL_LOST(R.drawable.ic_gps_not_fixed_24dp, R.string.gps_wait_for_signal);
public final int icon;
public final int message;
GpsStatusValue(int icon, int message) {
this.icon = icon;
this.message = message;
}
public boolean isGpsStarted() {
return this != GpsStatusValue.GPS_NONE && this != GpsStatusValue.GPS_DISABLED;
}
}
@@ -16,6 +16,7 @@ import java.time.ZoneId;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.GpsStatusValue;
import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@@ -138,7 +139,8 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
this.clock = Clock.fixed(Instant.parse(time), ZoneId.of("CET"));
}
void sendGpsStatus(GpsStatusValue gpsStatusValue) {
@Deprecated //TODO This should be refactored. Can we use a SensorDataSet for this?
public void sendGpsStatus(GpsStatusValue gpsStatusValue) {
service.newGpsStatus(gpsStatusValue);
}