GPS is now optional: data source (internal, none) be selected in settings.

Migrated to GPSHandler.

Fixes #1279.
This commit is contained in:
Dennis Guse
2026-02-04 22:37:19 +01:00
parent d75472ef11
commit b651a8ca6a
15 changed files with 203 additions and 171 deletions
@@ -1,9 +1,7 @@
package de.dennisguse.opentracks.sensors;
import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -12,95 +10,25 @@ import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.sensors.driver.DriverObserver;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.driver.GpsInternal;
import de.dennisguse.opentracks.sensors.driver.SensorType;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorGPS;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class GpsManager implements DriverObserver<Location>, GpsStatusManager.GpsStatusListener, SharedPreferences.OnSharedPreferenceChangeListener {
public class GpsHandler extends SensorHandler<Location, Position> {
private final String TAG = GpsManager.class.getSimpleName();
private final String TAG = GpsHandler.class.getSimpleName();
//TODO Refactor to just pass information via SensorManager.
private TrackPointCreator trackPointCreator;
private SensorManager observer;
private Context context;
private Handler handler;
@VisibleForTesting
public GpsInternal driver;
private GpsStatusManager gpsStatusManager;
private Distance thresholdHorizontalAccuracy;
private AggregatorGPS aggregatorGPS;
public GpsManager(TrackPointCreator trackPointCreator, SensorManager 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);
this.aggregatorGPS = new AggregatorGPS("internal");
this.driver = new GpsInternal(this);
this.driver.connect(context, handler, "internal");
gpsStatusManager.start();
}
@SuppressWarnings({"MissingPermission"})
public void stop() {
if (driver.isConnected()) {
driver.disconnect();
}
driver = null;
this.context = null;
handler = null;
gpsStatusManager.stop();
gpsStatusManager = null;
this.aggregatorGPS = null;
observer = null;
trackPointCreator = null;
}
//TODO Exposed for refactoring. Is this necessary?
@Deprecated
AggregatorGPS getAggregatorGPS() {
return aggregatorGPS;
}
//TODO Exposed for refactoring. This is very ugly.
@Deprecated
void setAggregatorGPS() {
this.aggregatorGPS = null;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
boolean registerListener = false;
if (PreferencesUtils.isKey(R.string.min_sampling_interval_key, key)) {
registerListener = true;
if (gpsStatusManager != null) {
gpsStatusManager.onMinSamplingIntervalChanged(PreferencesUtils.getMinSamplingInterval());
}
}
if (PreferencesUtils.isKey(R.string.recording_gps_accuracy_key, key)) {
thresholdHorizontalAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy();
}
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(R.string.recording_distance_interval_key, key)) {
if (gpsStatusManager != null) {
Distance gpsMinDistance = PreferencesUtils.getRecordingDistanceInterval();
@@ -108,49 +36,85 @@ public class GpsManager implements DriverObserver<Location>, GpsStatusManager.Gp
}
}
if (registerListener && driver != null && driver.isConnected()) {
driver.disconnect();
driver.connect(context, handler, "internal");
if (PreferencesUtils.isKey(R.string.recording_gps_accuracy_key, key)) {
thresholdHorizontalAccuracy = PreferencesUtils.getThresholdHorizontalAccuracy();
}
}
@Override
public void onGpsStatusChanged(GpsStatusValue currentStatus) {
trackPointCreator.sendGpsStatus(currentStatus);
if (PreferencesUtils.isKey(R.string.min_sampling_interval_key, key)) {
if (gpsStatusManager != null) {
gpsStatusManager.onMinSamplingIntervalChanged(PreferencesUtils.getMinSamplingInterval());
}
//TODO
connect();
}
};
public GpsHandler(SensorManager sensorManager, TrackPointCreator trackPointCreator) {
super(sensorManager);
this.trackPointCreator = trackPointCreator;
gpsStatusManager = new GpsStatusManager(sensorManager.getContext(), trackPointCreator::sendGpsStatus, sensorManager.getHandler());
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
}
@Override
public void onConnected(String sensorAddress, String sensorName) {
super.onConnected(sensorAddress, sensorName);
gpsStatusManager.onGpsEnabled();
}
@Override
public void onConnectionLost() {
gpsStatusManager.onGpsDisabled();
gpsStatusManager.start();
}
@Override
public void onDisconnected() {
super.onDisconnected();
gpsStatusManager.stop();
}
@Override
public void onSensorDeactivated() {
super.onSensorDeactivated();
gpsStatusManager.onGpsDisabled();
}
@Override
public void onDataReceived(Location location) {
// Send each update to the status; please note that this TrackPoint is not stored.
Position position = Position.of(location, observer.getNow());
Position position = Position.of(location, trackPointCreator.getNow());
gpsStatusManager.onNewTrackPoint(position);
//TODO We could move the following check into the AggregatorGPS
if (!position.hasValidLocation()) {
Log.w(TAG, "Ignore newTrackPoint. Location is invalid.");
return;
}
if (!position.fulfillsAccuracy(thresholdHorizontalAccuracy)) {
Log.d(TAG, "Ignore newTrackPoint. Poor accuracy.");
return;
}
aggregatorGPS.add(observer.getNow(), position);
observer.onChange();
super.onDataReceived(location);
}
@NonNull
@Override
protected Driver createDriverInternal() {
return new GpsInternal(this);
}
@NonNull
@Override
protected AggregatorGPS createAggregator(String sensorAddress, String sensorName) {
return new AggregatorGPS(sensorAddress, sensorName);
}
@Override
protected int getSensorPreferenceKey() {
return R.string.settings_sensor_bluetooth_gps_key;
}
@Override
protected String getSensorPreferenceDefaultValue() {
return SensorType.INTERNAL.getPreferenceValue();
}
}
@@ -31,7 +31,7 @@ class GpsStatusManager {
private Duration signalLostThreshold;
private GpsStatusValue gpsStatus = GpsStatusValue.GPS_NONE;
private GpsStatusListener client;
private GpsStatusListener observer;
private final Context context;
@Nullable
@@ -44,8 +44,8 @@ class GpsStatusManager {
};
public GpsStatusManager(Context context, GpsStatusListener client, Handler handler) {
this.client = client;
public GpsStatusManager(Context context, GpsStatusListener observer, Handler handler) {
this.observer = observer;
this.context = context;
this.handler = handler;
@@ -54,7 +54,7 @@ class GpsStatusManager {
}
public void start() {
client.onGpsStatusChanged(GpsStatusValue.GPS_ENABLED);
observer.onGpsStatusChanged(GpsStatusValue.GPS_ENABLED);
}
/**
@@ -62,9 +62,7 @@ class GpsStatusManager {
*/
public void stop() {
stopTimer();
client.onGpsStatusChanged(GpsStatusValue.GPS_NONE);
client = null;
handler = null;
observer.onGpsStatusChanged(GpsStatusValue.GPS_NONE);
}
/**
@@ -159,8 +157,8 @@ class GpsStatusManager {
private void setGpsStatus(GpsStatusValue current) {
gpsStatus = current;
if (client != null) {
client.onGpsStatusChanged(current);
if (observer != null) {
observer.onGpsStatusChanged(current);
}
}
@@ -10,6 +10,7 @@ import java.time.Instant;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.driver.DriverObserver;
import de.dennisguse.opentracks.sensors.driver.SensorType;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@@ -19,7 +20,8 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
private final SensorManager sensorManager;
private Driver driver;
@VisibleForTesting
public Driver driver;
protected Aggregator<AggregatorInput, AggregatorOutput> aggregator;
@@ -30,7 +32,7 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
void connect() {
disconnect();
String address = PreferencesUtils.getString(getSensorPreferenceKey(), PreferencesUtils.getBluetoothSensorAddressNone());
String address = PreferencesUtils.getString(getSensorPreferenceKey(), getSensorPreferenceDefaultValue());
switch (PreferencesUtils.getSensorType(address)) {
case NONE -> {
@@ -71,6 +73,10 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
protected abstract int getSensorPreferenceKey();
protected String getSensorPreferenceDefaultValue() {
return SensorType.NONE.getPreferenceValue();
}
protected Context getContext() {
return sensorManager.getContext();
}
@@ -106,6 +112,9 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
@Override
public void onDataReceived(AggregatorInput value) {
if (aggregator == null) {
throw new RuntimeException("Received data while being disconnected.");
}
SensorHandler.this.aggregator.add(sensorManager.getNow(), value);
sensorManager.onChange();
}
@@ -20,7 +20,6 @@ import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorGPS;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorTemperature;
@@ -51,14 +50,14 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
private AltitudeChangeHandler altitudeChangeHandler;
private GpsManager gpsManager;
private GpsHandler gpsHandler;
public SensorManager(TrackPointCreator observer) {
this.observer = observer;
}
public void start(Context context, Handler handler) {
if (gpsManager != null) {
if (isStarted()) {
throw new RuntimeException("SensorManager cannot be started twice; stop first.");
}
@@ -66,7 +65,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
this.context = context;
this.handler = handler;
gpsManager = new GpsManager(observer, this);
gpsHandler = new GpsHandler(this, observer);
altitudeChangeHandler = new AltitudeChangeHandler(this);
heartRateHandler = new HeartRateHandler(this);
temperatureHandler = new TemperatureHandler(this);
@@ -76,8 +75,6 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
runningSpeedCadenceDistanceHandler = new RunningSpeedCadenceDistanceHandler(this);
onSharedPreferenceChanged(null, null);
gpsManager.start(context, handler);
}
public void stop() {
@@ -104,15 +101,15 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
altitudeChangeHandler.disconnect();
altitudeChangeHandler = null;
gpsManager.stop();
gpsManager = null;
gpsHandler.disconnect();
gpsHandler = null;
context = null;
handler = null;
}
public void reset() {
if (gpsManager == null || altitudeChangeHandler == null) {
if (gpsHandler == null || altitudeChangeHandler == null) {
Log.d(TAG, "No recording running and no reset necessary.");
return;
}
@@ -126,11 +123,11 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
cyclingPowerHandler.resetAggregated();
runningSpeedCadenceDistanceHandler.resetAggregated();
altitudeChangeHandler.resetAggregated();
if (gpsManager.getAggregatorGPS() != null) gpsManager.getAggregatorGPS().resetAggregated();
gpsHandler.resetAggregated();
}
public GpsManager getGpsManager() {
return gpsManager;
public GpsHandler getGpsHandler() {
return gpsHandler;
}
@Deprecated
@@ -141,36 +138,38 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, @Nullable String key) {
if (gpsManager != null) {
gpsManager.onSharedPreferenceChanged(sharedPreferences, key);
if (!isStarted()) return;
if (PreferencesUtils.isKey(altitudeChangeHandler.getSensorPreferenceKey(), key)) {
altitudeChangeHandler.connect();
}
if (PreferencesUtils.isKey(gpsHandler.getSensorPreferenceKey(), key)) {
gpsHandler.connect();
}
if (PreferencesUtils.isKey(heartRateHandler.getSensorPreferenceKey(), key)) {
heartRateHandler.connect();
}
if (PreferencesUtils.isKey(altitudeChangeHandler.getSensorPreferenceKey(), key)) {
altitudeChangeHandler.connect();
}
if (PreferencesUtils.isKey(temperatureHandler.getSensorPreferenceKey(), key)) {
temperatureHandler.connect();
}
if (PreferencesUtils.isKey(heartRateHandler.getSensorPreferenceKey(), key)) {
heartRateHandler.connect();
}
if (PreferencesUtils.isKey(cyclingCadenceHandler.getSensorPreferenceKey(), key)) {
cyclingCadenceHandler.connect();
}
if (PreferencesUtils.isKey(temperatureHandler.getSensorPreferenceKey(), key)) {
temperatureHandler.connect();
}
if (PreferencesUtils.isKey(cyclingDistanceSpeedHandler.getSensorPreferenceKey(), key)) {
cyclingDistanceSpeedHandler.connect();
}
if (PreferencesUtils.isKey(cyclingCadenceHandler.getSensorPreferenceKey(), key)) {
cyclingCadenceHandler.connect();
}
if (PreferencesUtils.isKey(cyclingCadenceHandler.getSensorPreferenceKey(), key)) {
cyclingPowerHandler.connect();
}
if (PreferencesUtils.isKey(cyclingDistanceSpeedHandler.getSensorPreferenceKey(), key)) {
cyclingDistanceSpeedHandler.connect();
}
if (PreferencesUtils.isKey(runningSpeedCadenceDistanceHandler.getSensorPreferenceKey(), key)) {
runningSpeedCadenceDistanceHandler.connect();
}
if (PreferencesUtils.isKey(cyclingCadenceHandler.getSensorPreferenceKey(), key)) {
cyclingPowerHandler.connect();
}
if (PreferencesUtils.isKey(runningSpeedCadenceDistanceHandler.getSensorPreferenceKey(), key)) {
runningSpeedCadenceDistanceHandler.connect();
}
}
@@ -251,10 +250,13 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
}
public SensorDataSet getSensorDataSet(Instant now) {
AggregatorGPS gps = gpsManager.getAggregatorGPS();
SensorData<Position> gpsSensorData = gpsHandler.getSensorData(now);
var a = temperatureHandler.getSensorData(now);
var b = altitudeChangeHandler.getSensorData(now);
return new SensorDataSet(
gps != null && gps.hasReceivedData() ? gps.getAggregatedValueWithSensorName(now) : new SensorData<>(Position.of(now), ""),
gpsSensorData != null ? gpsSensorData : new SensorData<>(Position.of(now), ""), //We always need a Position(now)
getSpeed(now),
getDistance(now),
getHeartRate(now),
@@ -291,11 +293,15 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
if (runningSpeedCadenceDistanceHandler != null) {
runningSpeedCadenceDistanceHandler.setAggregator(null);
}
if (this.gpsManager != null) {
this.gpsManager.setAggregatorGPS();
if (this.gpsHandler != null) {
this.gpsHandler.setAggregator(null);
}
}
private boolean isStarted() {
return wakeLock != null;
}
@VisibleForTesting
public void setAggregator(AggregatorHeartRate data) {
heartRateHandler.setAggregator(data);
@@ -41,6 +41,7 @@ import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
/**
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
*/
//TODO Re-act to Bluetooth disabled/enabled
@SuppressLint("MissingPermission")
public class BluetoothDriver<T> implements Driver {
@@ -22,4 +22,10 @@ public interface DriverObserver<T> {
*/
//TODO check if really need this, because the driver was told to disconnect and then just signalizes that it finished this task.
void onDisconnected();
/**
* The Driver detected that the sensor got deactivated.
* For example, the user deactivated the internal GPS.
*/
default void onSensorDeactivated() {}
}
@@ -7,6 +7,7 @@ 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;
@@ -18,9 +19,10 @@ public class GpsInternal implements Driver {
private static final String TAG = GpsInternal.class.getSimpleName();
private static final String LOCATION_PROVIDER = LocationManager.GPS_PROVIDER;
public static final String LOCATION_PROVIDER = LocationManager.GPS_PROVIDER;
private final LocationListenerCompat locationListenerCompat = new LocationListenerCompat() {
@VisibleForTesting
public final LocationListenerCompat locationListenerCompat = new LocationListenerCompat() {
@Override
public void onLocationChanged(@NonNull Location location) {
driverObserver.onDataReceived(location);
@@ -33,13 +35,14 @@ public class GpsInternal implements Driver {
@Override
public void onProviderDisabled(@NonNull String provider) {
driverObserver.onConnectionLost();
driverObserver.onSensorDeactivated();
}
};
private final DriverObserver<Location> driverObserver;
private LocationManager locationManager;
@VisibleForTesting
public LocationManager locationManager;
public GpsInternal(DriverObserver<Location> driverObserver) {
this.driverObserver = driverObserver;
@@ -62,8 +65,8 @@ public class GpsInternal implements Driver {
if (PermissionRequester.GPS.hasPermission(context)) {
try {
Log.i(TAG, "Register for location updates " + context);
LocationManagerCompat.requestLocationUpdates(locationManager, LOCATION_PROVIDER, locationRequest, handler::post, locationListenerCompat);
driverObserver.onConnected(null, null);
LocationManagerCompat.requestLocationUpdates(locationManager, LOCATION_PROVIDER, locationRequest, handler::post, locationListenerCompat);
return;
} catch (SecurityException e) {
Log.e(TAG, "Could not register location listener; permissions not granted.", e);
@@ -16,10 +16,6 @@ public abstract class Aggregator<Input, Output> {
private final String sensorAddress;
private final String sensorName;
Aggregator(String sensorAddress) {
this(sensorAddress, null);
}
Aggregator(String sensorAddress, String sensorName) {
this.sensorAddress = sensorAddress;
this.sensorName = sensorName;
@@ -1,21 +1,23 @@
package de.dennisguse.opentracks.sensors.sensorData;
import android.location.Location;
import androidx.annotation.NonNull;
import java.time.Instant;
import de.dennisguse.opentracks.data.models.Position;
public class AggregatorGPS extends Aggregator<Position, Position> {
public class AggregatorGPS extends Aggregator<Location, Position> {
public AggregatorGPS(String sensorAddress) {
super(sensorAddress);
public AggregatorGPS(String sensorAddress, String sensorName) {
super(sensorAddress, sensorName);
}
@Override
protected void computeValue(Raw<Position> current) {
aggregatedValue = current.value();
protected void computeValue(Raw<Location> current) {
aggregatedValue = Position.of(current.value(), current.time());
}
@Override
@@ -253,10 +253,6 @@ public class PreferencesUtils {
return isReportSpeed(track.activityTypeLocalized());
}
public static String getBluetoothSensorAddressNone() {
return SensorType.NONE.getPreferenceValue();
}
public static SensorType getSensorType(String address) {
return Arrays.stream(SensorType.values())
.filter(it -> it.getPreferenceValue().equals(address))
@@ -0,0 +1,41 @@
package de.dennisguse.opentracks.settings.bluetooth;
import android.content.Context;
import android.location.LocationManager;
import android.util.AttributeSet;
import androidx.core.location.LocationManagerCompat;
import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.List;
import de.dennisguse.opentracks.sensors.driver.GpsInternal;
public class BluetoothLeGpsPreference extends BluetoothLeSensorPreference {
public BluetoothLeGpsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public BluetoothLeGpsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public BluetoothLeGpsPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BluetoothLeGpsPreference(Context context) {
super(context);
}
@Override
public PreferenceDialogFragmentCompat createInstance() {
boolean hasInternalSensor = LocationManagerCompat.hasProvider((LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE), GpsInternal.LOCATION_PROVIDER);
return BluetoothLeSensorPreferenceDialog
.newInstance(getKey(), List.of(), hasInternalSensor);
}
}