diff --git a/src/androidTest/java/de/dennisguse/opentracks/util/EspressoUtils.java b/src/androidTest/java/de/dennisguse/opentracks/util/EspressoUtils.java index 459c55148..c10651480 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/util/EspressoUtils.java +++ b/src/androidTest/java/de/dennisguse/opentracks/util/EspressoUtils.java @@ -1,8 +1,8 @@ package de.dennisguse.opentracks.util; -import static org.hamcrest.Matchers.allOf; -import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static org.hamcrest.Matchers.allOf; import android.app.Activity; import android.os.SystemClock; @@ -14,11 +14,11 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.test.espresso.PerformException; import androidx.test.espresso.UiController; import androidx.test.espresso.ViewAction; +import androidx.test.espresso.util.HumanReadables; +import androidx.test.espresso.util.TreeIterables; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; import androidx.test.runner.lifecycle.Stage; -import androidx.test.espresso.util.HumanReadables; -import androidx.test.espresso.util.TreeIterables; import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.tabs.TabLayout; @@ -33,8 +33,6 @@ import java.util.function.Predicate; public class EspressoUtils { - private static final long VIEW_POLLING_INTERVAL_MS = 50; - public static Matcher withRecyclerViewItemCount(final int count) { return new TypeSafeMatcher<>() { @Override diff --git a/src/main/java/de/dennisguse/opentracks/TrackRecordingActivity.java b/src/main/java/de/dennisguse/opentracks/TrackRecordingActivity.java index d0e56ff4d..7bf360409 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackRecordingActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackRecordingActivity.java @@ -67,6 +67,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key"; private Snackbar snackbar; + private Snackbar sensorWarningSnackbar; // The following are setFrequency in onCreate private ContentProviderUtils contentProviderUtils; @@ -86,6 +87,9 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc service.getGpsStatusObservable() .observe(TrackRecordingActivity.this, this::onGpsStatusChanged); + service.getSensorDisconnectedWarningObservable() + .observe(TrackRecordingActivity.this, this::onSensorDisconnectedWarningChanged); + if (!service.isRecording()) { finish(); return; @@ -408,4 +412,27 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc }); snackbar.show(); } + + private void onSensorDisconnectedWarningChanged(boolean show) { + if (show) { + if (sensorWarningSnackbar == null) { + sensorWarningSnackbar = Snackbar + .make(viewBinding.trackRecordingCoordinatorLayout, + getString(R.string.sensor_disconnected_warning), + Snackbar.LENGTH_INDEFINITE) + .setAction(getString(R.string.sensor_disconnected_switch_action), v -> { + if (trackRecordingServiceConnection != null && trackRecordingServiceConnection.getTrackRecordingService() != null) { + trackRecordingServiceConnection.getTrackRecordingService().setOverrideSensorWithGPS(true); + } + }); + } + if (!sensorWarningSnackbar.isShown()) { + sensorWarningSnackbar.show(); + } + } else { + if (sensorWarningSnackbar != null && sensorWarningSnackbar.isShown()) { + sensorWarningSnackbar.dismiss(); + } + } + } } \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/data/statistics/ExtremityMonitor.java b/src/main/java/de/dennisguse/opentracks/data/statistics/ExtremityMonitor.java index ac7147034..ade7a2785 100644 --- a/src/main/java/de/dennisguse/opentracks/data/statistics/ExtremityMonitor.java +++ b/src/main/java/de/dennisguse/opentracks/data/statistics/ExtremityMonitor.java @@ -50,15 +50,6 @@ public class ExtremityMonitor { return min; } - /** - * Sets the minimum value. - * - * @param min the minimum value - */ - public void setMin(double min) { - this.min = min; - } - /** * Gets the maximum value seen. */ @@ -66,15 +57,6 @@ public class ExtremityMonitor { return max; } - /** - * Sets the maximum value. - * - * @param max the maximum value - */ - public void setMax(double max) { - this.max = max; - } - /** * Updates the min and the max with a new value. * diff --git a/src/main/java/de/dennisguse/opentracks/data/statistics/TrackStatisticsUpdater.java b/src/main/java/de/dennisguse/opentracks/data/statistics/TrackStatisticsUpdater.java index 46ad43431..855b104fa 100644 --- a/src/main/java/de/dennisguse/opentracks/data/statistics/TrackStatisticsUpdater.java +++ b/src/main/java/de/dennisguse/opentracks/data/statistics/TrackStatisticsUpdater.java @@ -54,6 +54,7 @@ public class TrackStatisticsUpdater { private TrackPoint lastTrackPoint; private boolean isIdle; + private boolean forceZeroDistance; public TrackStatisticsUpdater(@NonNull TrackPoint trackPoint) { addTrackPoint(trackPoint); @@ -78,6 +79,7 @@ public class TrackStatisticsUpdater { this.lastTrackPoint = toCopy.lastTrackPoint; this.isIdle = toCopy.isIdle; + this.forceZeroDistance = toCopy.forceZeroDistance; resetAverageHeartRate(); addTrackPoint(tmp); @@ -94,6 +96,10 @@ public class TrackStatisticsUpdater { return isIdle; } + public void setForceZeroDistance(boolean forceZeroDistance) { + this.forceZeroDistance = forceZeroDistance; + } + public Statistics getCurrentSegment() { return currentSegment.getStatistics(); } @@ -147,6 +153,8 @@ public class TrackStatisticsUpdater { Distance movingDistance = null; if (trackPoint.sensorDistance() != null) { movingDistance = trackPoint.sensorDistance(); + } else if (forceZeroDistance) { + movingDistance = Distance.ZERO; } else if (lastTrackPoint != null && lastTrackPoint.position().hasLocation() && trackPoint.position().hasLocation()) { diff --git a/src/main/java/de/dennisguse/opentracks/fragments/ChooseActivityTypeDialogFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/ChooseActivityTypeDialogFragment.java index e6967d633..84858768a 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/ChooseActivityTypeDialogFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/ChooseActivityTypeDialogFragment.java @@ -56,7 +56,7 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements ActivityType.WORKOUT ); - private static int getPosition(Context context, ActivityType activityType) { + private static int getPosition(ActivityType activityType) { if (activityType == null) { return -1; } @@ -92,7 +92,7 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements } final ChooseActivityTypeImageAdapter imageAdapter = new ChooseActivityTypeImageAdapter(iconDrawableIds); - int position = getPosition(getContext(), preselectedActivityType); + int position = getPosition(preselectedActivityType); if (position != -1) { imageAdapter.setSelected(position); } diff --git a/src/main/java/de/dennisguse/opentracks/io/healthconnect/exporter/HealthConnectUtils.java b/src/main/java/de/dennisguse/opentracks/io/healthconnect/exporter/HealthConnectUtils.java index 591a0070c..cc3998bf5 100644 --- a/src/main/java/de/dennisguse/opentracks/io/healthconnect/exporter/HealthConnectUtils.java +++ b/src/main/java/de/dennisguse/opentracks/io/healthconnect/exporter/HealthConnectUtils.java @@ -27,8 +27,6 @@ import kotlinx.coroutines.BuildersKt; */ public class HealthConnectUtils { - private static final String TAG = HealthConnectUtils.class.getSimpleName(); - static final String WRITE_EXERCISE_PERMISSION = HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(ExerciseSessionRecord.class)); static final String WRITE_EXERCISE_ROUTE_PERMISSION = HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE; diff --git a/src/main/java/de/dennisguse/opentracks/sensors/GpsStatusManager.java b/src/main/java/de/dennisguse/opentracks/sensors/GpsStatusManager.java index 5a5154d95..d82dbac81 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/GpsStatusManager.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/GpsStatusManager.java @@ -20,8 +20,6 @@ import de.dennisguse.opentracks.settings.PreferencesUtils; */ 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); diff --git a/src/main/java/de/dennisguse/opentracks/sensors/SensorHandler.java b/src/main/java/de/dennisguse/opentracks/sensors/SensorHandler.java index 31433932d..0a7c6de8c 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/SensorHandler.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/SensorHandler.java @@ -115,7 +115,8 @@ public abstract class SensorHandler implement return; } - SensorHandler.this.aggregator.resetAggregated(); + SensorHandler.this.aggregator = null; + sensorManager.onChange(); } @Override @@ -130,5 +131,6 @@ public abstract class SensorHandler implement @Override public void onDisconnected() { SensorHandler.this.aggregator = null; + sensorManager.onChange(); } } diff --git a/src/main/java/de/dennisguse/opentracks/sensors/SensorManager.java b/src/main/java/de/dennisguse/opentracks/sensors/SensorManager.java index b2a9ba1bc..7429b048a 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/SensorManager.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/SensorManager.java @@ -239,6 +239,9 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange } public SensorDataSet getSensorDataSet(Instant now) { + if (!isStarted()) { + return new SensorDataSet(new SensorData<>(Position.of(now), ""), null, null, null, null, null, null, null); + } //We always need a Position with now SensorData gpsSensorData = gpsHandler.getSensorData(now); if (gpsSensorData != null && gpsSensorData.data() != null) { diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorCyclingCadence.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorCyclingCadence.java index f10fcace9..42ead636b 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorCyclingCadence.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorCyclingCadence.java @@ -5,6 +5,7 @@ import android.util.Log; import androidx.annotation.NonNull; import java.time.Duration; +import java.time.Instant; import de.dennisguse.opentracks.data.models.Cadence; import de.dennisguse.opentracks.sensors.UintUtils; @@ -18,17 +19,20 @@ public class AggregatorCyclingCadence extends Aggregator current) { if (current.value() instanceof CyclingCadenceBluetooth.DirectCadenceData directCadenceData) { output = directCadenceData.cadence(); + lastValidUpdate = current.time(); return; } - computeRawCadence((CyclingCadenceBluetooth.CrankData) current.value()); + computeRawCadence((CyclingCadenceBluetooth.CrankData) current.value(), current.time()); } - private void computeRawCadence(@NonNull CyclingCadenceBluetooth.CrankData currentRaw) { + private void computeRawCadence(@NonNull CyclingCadenceBluetooth.CrankData currentRaw, Instant now) { CyclingCadenceBluetooth.CrankData previousRaw = getPreviousRaw(); if (previousRaw == null) return; @@ -36,6 +40,9 @@ public class AggregatorCyclingCadence extends Aggregator 0) { + resetOutdated(); + } return; } if (timeDiff.isNegative()) { @@ -52,9 +59,15 @@ public class AggregatorCyclingCadence extends Aggregator current) { if (previous == null) { + lastValidUpdate = current.time(); return; } @@ -31,6 +35,9 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator 0) { + resetOutdated(); + } return; } if (timeDiff.isNegative()) { @@ -52,6 +59,7 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator { + if (trackId != null) { + onIdle(); + } + }); + String address = de.dennisguse.opentracks.settings.PreferencesUtils.getString(de.dennisguse.opentracks.R.string.settings_sensor_bluetooth_cycling_speed_key, de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue()); + if (!de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue().equals(address)) { + handler.postDelayed(() -> { + if (trackId != null && !hasSensorBeenConnected && !isSensorDisconnectedWarningActive) { + isSensorDisconnectedWarningActive = true; + if (sensorDisconnectObserver != null) { + sensorDisconnectObserver.onSensorDisconnectedWarning(true); + } + } + }, 10000); + } + return trackId; } @@ -116,6 +149,23 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen reset(); + handler.post(() -> { + if (this.trackId != null) { + onIdle(); + } + }); + String address = de.dennisguse.opentracks.settings.PreferencesUtils.getString(de.dennisguse.opentracks.R.string.settings_sensor_bluetooth_cycling_speed_key, de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue()); + if (!de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue().equals(address)) { + handler.postDelayed(() -> { + if (trackId != null && !hasSensorBeenConnected && !isSensorDisconnectedWarningActive) { + isSensorDisconnectedWarningActive = true; + if (sensorDisconnectObserver != null) { + sensorDisconnectObserver.onSensorDisconnectedWarning(true); + } + } + }, 10000); + } + return true; } @@ -125,6 +175,10 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen trackId = null; trackStatisticsUpdater = null; + + idleTimeoutScheduledAt = null; + walkingStartedAt = null; + idleStartedAt = null; reset(); } @@ -144,14 +198,48 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen TrackBuilder trackBuilder = new TrackBuilder(track); trackBuilder.setStatistics(tmpTrackStatisticsUpdater.getTrackStatistics()); - - return new RecordingData(trackBuilder.getTrack(), TrackPointWithEGM2008, current.second, tmpTrackStatisticsUpdater.getCurrentSegment()); + + java.time.Instant now = java.time.Instant.now(); + String idleTimer = ""; + if (idleTimeoutScheduledAt != null && !"IDLE".equals(autoPauseStatus) && !"WALKING".equals(autoPauseStatus)) { + java.time.Duration d = java.time.Duration.between(now, idleTimeoutScheduledAt); + if (!d.isNegative()) idleTimer = de.dennisguse.opentracks.util.StringUtils.formatElapsedTime(d); + } + + String walkingTimer = ""; + if (walkingStartedAt != null && "WALKING".equals(autoPauseStatus)) { + java.time.Duration d = java.time.Duration.between(walkingStartedAt, now); + if (!d.isNegative()) walkingTimer = de.dennisguse.opentracks.util.StringUtils.formatElapsedTime(d); + } + + String markerTimer = ""; + if (idleStartedAt != null && "IDLE".equals(autoPauseStatus)) { + java.time.Duration d = java.time.Duration.between(idleStartedAt, now); + if (!d.isNegative()) markerTimer = de.dennisguse.opentracks.util.StringUtils.formatElapsedTime(d); + } + + return new RecordingData(trackBuilder.getTrack(), TrackPointWithEGM2008, current.second, tmpTrackStatisticsUpdater.getCurrentSegment(), autoPauseStatus, idleTimer, walkingTimer, markerTimer); } @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) public synchronized void onIdle() { Log.d(TAG, "Becoming idle"); - onNewTrackPoint(trackPointCreator.createIdle()); + TrackPoint idlePoint = trackPointCreator.createIdle(); + + boolean preventIdle = de.dennisguse.opentracks.settings.PreferencesUtils.shouldPreventIdleIfCadence(); + if (preventIdle && hasCadenceSensorBeenConnected) { + if (idlePoint.cadence() != null && idlePoint.cadence().value_rpm() > 0) { + scheduleNewIdleTimeout(); + return; + } + } + + if (!"IDLE".equals(autoPauseStatus)) { + idleStartedAt = java.time.Instant.now(); + } + autoPauseStatus = "IDLE"; + + onNewTrackPoint(idlePoint); idleObserver.onIdle(); } @@ -167,6 +255,27 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen lastTrackPointUIWithAltitude = trackPoint; } + boolean sensorHasDistance = trackPoint.sensorDistance() != null; + if (sensorHasDistance) { + hasSensorBeenConnected = true; + if (isSensorDisconnectedWarningActive) { + isSensorDisconnectedWarningActive = false; + sensorDisconnectObserver.onSensorDisconnectedWarning(false); + sensorDisconnectObserver.onSensorConnected(); + } + } else if (hasSensorBeenConnected) { + if (!isSensorDisconnectedWarningActive) { + isSensorDisconnectedWarningActive = true; + sensorDisconnectObserver.onSensorDisconnectedWarning(true); + } + } + + if (trackStatisticsUpdater != null) { + String address = de.dennisguse.opentracks.settings.PreferencesUtils.getString(de.dennisguse.opentracks.R.string.settings_sensor_bluetooth_cycling_speed_key, de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue()); + boolean isSensorExpected = !de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue().equals(address); + trackStatisticsUpdater.setForceZeroDistance(hasSensorBeenConnected || (isSensorExpected && !gpsOverriden)); + } + if (trackPoint.type() == TrackPoint.Type.IDLE) { insertTrackPoint(trackPoint, true); handler.removeCallbacks(ON_IDLE); @@ -200,18 +309,57 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen return true; } + if (trackPoint.cadence() != null) { + hasCadenceSensorBeenConnected = true; + } + + boolean isWalkingWithBike = false; + if (autoPauseOnCadence0 && hasCadenceSensorBeenConnected && autoPauseCadence0Speed != null) { + if (trackPoint.cadence() == null || trackPoint.cadence().getRPM() == 0) { + if (trackPoint.position().hasSpeed() && trackPoint.position().speed().lessThan(autoPauseCadence0Speed)) { + isWalkingWithBike = true; + } + } + } + Distance distanceToLastStoredTrackPoint; - if (trackPoint.position().hasLocation() && !lastStoredTrackPoint.position().hasLocation()) { - distanceToLastStoredTrackPoint = trackPoint.distanceToPreviousFromLocation(lastStoredTrackPointWithLocation); + if (isWalkingWithBike) { + distanceToLastStoredTrackPoint = Distance.ZERO; + autoPauseStatus = "WALKING"; } else { - distanceToLastStoredTrackPoint = trackPoint.distanceToPrevious(lastStoredTrackPoint); + if (trackPoint.position().hasLocation() && !lastStoredTrackPoint.position().hasLocation()) { + distanceToLastStoredTrackPoint = trackPoint.distanceToPreviousFromLocation(lastStoredTrackPointWithLocation); + } else { + distanceToLastStoredTrackPoint = trackPoint.distanceToPrevious(lastStoredTrackPoint); + } } if (distanceToLastStoredTrackPoint.greaterThan(maxRecordingDistance)) { - trackPoint = trackPoint.with(TrackPoint.Type.SEGMENT_START_AUTOMATIC); + // Check idle duration before creating SEGMENT_START_AUTOMATIC + boolean shouldCreateSegment = true; + if (idleStartedAt != null) { + java.time.Duration idleDuration = java.time.Duration.between(idleStartedAt, java.time.Instant.now()); + java.time.Duration markerDuration = de.dennisguse.opentracks.settings.PreferencesUtils.getMarkerIdleDuration(); + if (idleDuration.compareTo(markerDuration) < 0) { + shouldCreateSegment = false; + } + } + + if (shouldCreateSegment) { + trackPoint = trackPoint.with(TrackPoint.Type.SEGMENT_START_AUTOMATIC); + idleObserver.onLongBreak(); + } insertTrackPoint(trackPoint, true); scheduleNewIdleTimeout(); + if (!isWalkingWithBike) { + autoPauseStatus = "REC"; + } else { + if (!"WALKING".equals(autoPauseStatus)) { + walkingStartedAt = java.time.Instant.now(); + } + autoPauseStatus = "WALKING"; + } return true; } @@ -219,6 +367,14 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen insertTrackPoint(trackPoint, false); scheduleNewIdleTimeout(); + if (!isWalkingWithBike) { + autoPauseStatus = "REC"; + } else { + if (!"WALKING".equals(autoPauseStatus)) { + walkingStartedAt = java.time.Instant.now(); + } + autoPauseStatus = "WALKING"; + } return true; } @@ -235,6 +391,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen } handler.removeCallbacks(ON_IDLE); handler.postDelayed(ON_IDLE, idleDuration.toMillis()); + idleTimeoutScheduledAt = java.time.Instant.now().plus(idleDuration); } Statistics getTrackStatistics() { @@ -284,6 +441,13 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen lastStoredTrackPoint = null; lastStoredTrackPointWithLocation = null; + + hasSensorBeenConnected = false; + isSensorDisconnectedWarningActive = false; + hasCadenceSensorBeenConnected = false; + if (sensorDisconnectObserver != null) { + sensorDisconnectObserver.onSensorDisconnectedWarning(false); + } } @Override @@ -297,13 +461,39 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen if (PreferencesUtils.isKey(R.string.idle_duration_key, key)) { idleDuration = PreferencesUtils.getIdleDurationTimeout(); } + if (PreferencesUtils.isKey(R.string.settings_recording_auto_pause_on_cadence_0_key, key)) { + autoPauseOnCadence0 = PreferencesUtils.shouldAutoPauseOnCadence0(); + } + if (PreferencesUtils.isKey(R.string.settings_recording_auto_pause_on_cadence_0_speed_key, key)) { + autoPauseCadence0Speed = PreferencesUtils.getAutoPauseCadence0Speed(); + } } public TrackPoint getLastStoredTrackPointWithLocation() { return lastStoredTrackPointWithLocation; } + public void setOverrideSensorWithGPS(boolean override) { + if (override) { + hasSensorBeenConnected = false; + gpsOverriden = true; + if (isSensorDisconnectedWarningActive) { + isSensorDisconnectedWarningActive = false; + sensorDisconnectObserver.onSensorDisconnectedWarning(false); + } + if (trackStatisticsUpdater != null) { + trackStatisticsUpdater.setForceZeroDistance(false); + } + } + } + public interface IdleObserver { void onIdle(); + void onLongBreak(); + } + + public interface SensorDisconnectObserver { + void onSensorDisconnectedWarning(boolean show); + void onSensorConnected(); } } diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java index 8ca4c5a4a..c28f3b224 100644 --- a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java +++ b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java @@ -44,7 +44,7 @@ import de.dennisguse.opentracks.services.announcement.VoiceAnnouncementManager; import de.dennisguse.opentracks.services.handlers.TrackPointCreator; import de.dennisguse.opentracks.settings.PreferencesUtils; -public class TrackRecordingService extends Service implements TrackPointCreator.Callback, SharedPreferences.OnSharedPreferenceChangeListener, TrackRecordingManager.IdleObserver { +public class TrackRecordingService extends Service implements TrackPointCreator.Callback, SharedPreferences.OnSharedPreferenceChangeListener, TrackRecordingManager.IdleObserver, TrackRecordingManager.SensorDisconnectObserver { private static final String TAG = TrackRecordingService.class.getSimpleName(); @@ -84,6 +84,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator. private RecordingStatus recordingStatus; private MutableLiveData recordingStatusObservable; private MutableLiveData gpsStatusObservable; + private MutableLiveData sensorDisconnectedWarningObservable; private MutableLiveData recordingDataObservable; private Handler handler; @@ -104,10 +105,11 @@ public class TrackRecordingService extends Service implements TrackPointCreator. recordingStatusObservable = new MutableLiveData<>(); updateRecordingStatus(STATUS_DEFAULT); gpsStatusObservable = new MutableLiveData<>(STATUS_GPS_DEFAULT); + sensorDisconnectedWarningObservable = new MutableLiveData<>(false); recordingDataObservable = new MutableLiveData<>(RecordingData.NOT_RECORDING); trackPointCreator = new TrackPointCreator(this); - trackRecordingManager = new TrackRecordingManager(this, trackPointCreator, this, handler); + trackRecordingManager = new TrackRecordingManager(this, trackPointCreator, this, this, handler); voiceAnnouncementManager = new VoiceAnnouncementManager(this); notificationManager = new TrackRecordingServiceNotificationManager(this); @@ -138,6 +140,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator. recordingStatusObservable = null; gpsStatusObservable = null; + sensorDisconnectedWarningObservable = null; recordingDataObservable = null; Log.d(TAG, "Destroyed"); @@ -315,6 +318,10 @@ public class TrackRecordingService extends Service implements TrackPointCreator. return gpsStatusObservable; } + public LiveData getSensorDisconnectedWarningObservable() { + return sensorDisconnectedWarningObservable; + } + public LiveData getRecordingDataObservable() { return recordingDataObservable; } @@ -337,6 +344,32 @@ public class TrackRecordingService extends Service implements TrackPointCreator. voiceAnnouncementManager.announceIdle(); } + @Override + public void onLongBreak() { + voiceAnnouncementManager.announceLongBreak(); + } + + @Override + public void onSensorDisconnectedWarning(boolean show) { + if (show) { + voiceAnnouncementManager.announceSensorDisconnected(); + } + if (sensorDisconnectedWarningObservable != null) { + sensorDisconnectedWarningObservable.postValue(show); + } + } + + @Override + public void onSensorConnected() { + voiceAnnouncementManager.announceSensorConnected(); + } + + public void setOverrideSensorWithGPS(boolean override) { + if (trackRecordingManager != null) { + trackRecordingManager.setOverrideSensorWithGPS(override); + } + } + @VisibleForTesting public void stopUpdateRecordingData() { handler.removeCallbacks(updateRecordingData); diff --git a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementManager.java b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementManager.java index af2e5b651..1c3988884 100644 --- a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementManager.java +++ b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementManager.java @@ -119,6 +119,30 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe voiceAnnouncement.speak(VoiceAnnouncementUtils.createIdle(context)); } + public void announceLongBreak() { + if (shouldNotAnnounce() || !PreferencesUtils.shouldVoiceAnnouncementLongBreak()) { + return; + } + + voiceAnnouncement.speak(VoiceAnnouncementUtils.createLongBreak(context)); + } + + public void announceSensorDisconnected() { + if (shouldNotAnnounce() || !PreferencesUtils.shouldVoiceAnnouncementSensorDisconnected()) { + return; + } + + voiceAnnouncement.speak(VoiceAnnouncementUtils.createSensorDisconnected(context)); + } + + public void announceSensorConnected() { + if (shouldNotAnnounce() || !PreferencesUtils.shouldVoiceAnnouncementSensorConnected()) { + return; + } + + voiceAnnouncement.speak(VoiceAnnouncementUtils.createSensorConnected(context)); + } + public void announceStatisticsIfNeeded(@NonNull RecordingData data) { if (shouldNotAnnounce()) { return; diff --git a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java index bbce637e9..3fe16ce73 100644 --- a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java +++ b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java @@ -50,6 +50,21 @@ class VoiceAnnouncementUtils { .append(context.getString(R.string.voiceIdle)); } + static Spannable createLongBreak(Context context) { + return new SpannableStringBuilder() + .append(context.getString(R.string.voiceLongBreak)); + } + + static Spannable createSensorDisconnected(Context context) { + return new SpannableStringBuilder() + .append(context.getString(R.string.voiceSensorDisconnected)); + } + + static Spannable createSensorConnected(Context context) { + return new SpannableStringBuilder() + .append(context.getString(R.string.voiceSensorConnected)); + } + static Spannable createStatistics(Context context, Track track, SensorDataSet sensorDataSet, UnitSystem unitSystem, boolean isReportSpeed, @Nullable Statistics currentInterval, @Nullable SensorStatistics sensorStatistics) { Statistics trackStatistics = track.statistics(); diff --git a/src/main/java/de/dennisguse/opentracks/services/handlers/TrackPointCreator.java b/src/main/java/de/dennisguse/opentracks/services/handlers/TrackPointCreator.java index 696379cf0..8e449c65b 100644 --- a/src/main/java/de/dennisguse/opentracks/services/handlers/TrackPointCreator.java +++ b/src/main/java/de/dennisguse/opentracks/services/handlers/TrackPointCreator.java @@ -101,8 +101,11 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh position = position.withCoordinates(lastStoredTrackPointWithLocation.position()); } - if (lastTrackPointUISpeed != null) + if (sensorDataSet.speed() != null) { + position = position.with(sensorDataSet.speed().data()); + } else if (lastTrackPointUISpeed != null) { position = position.with(lastTrackPointUISpeed.position().speed()); + } if (lastTrackPointUIAltitude != null) position = position.with(lastTrackPointUIAltitude.position().altitude()); diff --git a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java index 2ef449f90..2d346cd9f 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java +++ b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java @@ -38,11 +38,13 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Optional; +import java.util.stream.Collectors; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.data.models.ActivityType; import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.DistanceFormatter; +import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.data.models.HeartRate; import de.dennisguse.opentracks.data.models.HeartRateZones; import de.dennisguse.opentracks.data.models.Track; @@ -249,6 +251,10 @@ public class PreferencesUtils { setString(R.string.stats_units_key, unitSystem.getPreferenceId()); } + public static Duration getMarkerIdleDuration() { + return Duration.ofMinutes(getInt(R.string.marker_idle_duration_key, 3)); + } + //TODO Check if actually needed or can be superseeded by a flexible default in getUnit() public static void applyDefaultUnit() { if (getString(R.string.stats_units_key, "").isEmpty()) { @@ -342,6 +348,18 @@ public class PreferencesUtils { return getBoolean(R.string.voice_announce_idle_key, true); } + public static boolean shouldVoiceAnnouncementLongBreak() { + return getBoolean(R.string.voice_announce_long_break_key, true); + } + + public static boolean shouldVoiceAnnouncementSensorDisconnected() { + return getBoolean(R.string.voice_announce_sensor_disconnected_key, true); + } + + public static boolean shouldVoiceAnnouncementSensorConnected() { + return getBoolean(R.string.voice_announce_sensor_connected_key, true); + } + public static boolean shouldVoiceAnnounceTime() { return getBoolean(R.string.voice_announce_time_key, true); } @@ -682,6 +700,51 @@ public class PreferencesUtils { return entries; } + static String[] getMarkerIdleDurationEntries() { + String[] entryValues = resources.getStringArray(R.array.marker_idle_duration_values); + String[] entries = new String[entryValues.length]; + + final int defaultDuration = Integer.parseInt(resources.getString(R.string.marker_idle_duration_default)); + + for (int i = 0; i < entryValues.length; i++) { + int value = Integer.parseInt(entryValues[i]); + + if (value == defaultDuration) { + entries[i] = resources.getString(R.string.value_integer_minute_recommended, value); + } else { + entries[i] = resources.getString(R.string.value_integer_minute, value); + } + } + + return entries; + } + + public static boolean shouldAutoPauseOnCadence0() { + return getBoolean(R.string.settings_recording_auto_pause_on_cadence_0_key, false); + } + + public static boolean shouldPreventIdleIfCadence() { + return getBoolean(R.string.settings_recording_prevent_idle_if_cadence_key, true); + } + + public static Speed getAutoPauseCadence0Speed() { + final int DEFAULT = Integer.parseInt(resources.getString(R.string.auto_pause_cadence_0_speed_default)); + int value = getInt(R.string.settings_recording_auto_pause_on_cadence_0_speed_key, DEFAULT); + return Speed.ofKMH(value); + } + + static String[] getAutoPauseCadence0SpeedEntries() { + String[] entryValues = resources.getStringArray(R.array.auto_pause_cadence_0_speed_values); + String[] entries = new String[entryValues.length]; + + for (int i = 0; i < entryValues.length; i++) { + int value = Integer.parseInt(entryValues[i]); + entries[i] = value + " km/h"; + } + + return entries; + } + public static boolean shouldInstantExportToFile() { final boolean INSTANT_POST_WORKOUT_EXPORT_DEFAULT = resources.getBoolean(R.bool.post_workout_export_enabled_default); return getBoolean(R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT) && isDefaultExportDirectoryUri(); @@ -839,7 +902,14 @@ public class PreferencesUtils { new DataField(resources.getString(R.string.stats_custom_layout_cadence_key), false, false, false), new DataField(resources.getString(R.string.stats_custom_layout_power_key), false, false, false), new DataField(resources.getString(R.string.stats_custom_layout_clock_key), false, false, false), - new DataField(resources.getString(R.string.stats_custom_layout_temperature_key), false, false, false) + new DataField(resources.getString(R.string.stats_custom_layout_temperature_key), false, false, false), + new DataField(resources.getString(R.string.stats_custom_layout_auto_pause_status_key), false, false, false), + new DataField(resources.getString(R.string.stats_custom_layout_sensor_status_hr_key), false, false, false), + new DataField(resources.getString(R.string.stats_custom_layout_sensor_status_cadence_key), false, false, false), + new DataField(resources.getString(R.string.stats_custom_layout_sensor_status_speed_key), false, false, false), + new DataField(resources.getString(R.string.stats_custom_layout_debug_idle_timer_key), false, false, false), + new DataField(resources.getString(R.string.stats_custom_layout_debug_walking_timer_key), false, false, false), + new DataField(resources.getString(R.string.stats_custom_layout_debug_marker_timer_key), false, false, false) ); RecordingLayout defaultLayout = new RecordingLayout(resources.getString(R.string.stats_custom_layout_default_layout), 2); @@ -855,7 +925,7 @@ public class PreferencesUtils { String selectedLayout = getSelectedLayoutName(); String csvCustomLayouts = getString(R.string.stats_custom_layouts_key, buildDefaultLayout()); - List recordingLayouts = RecordingLayoutIO.fromCsv(csvCustomLayouts, resources); + List recordingLayouts = getAllCustomLayouts(); RecordingLayout fallback = !recordingLayouts.isEmpty() ? recordingLayouts.get(0) : getDefaultLayout(); @@ -901,7 +971,18 @@ public class PreferencesUtils { public static List getAllCustomLayouts() { String csvCustomLayouts = getString(R.string.stats_custom_layouts_key, buildDefaultLayout()); - return RecordingLayoutIO.fromCsv(csvCustomLayouts, resources); + List layouts = RecordingLayoutIO.fromCsv(csvCustomLayouts, resources); + List defaultFields = getDefaultLayout().getFields(); + + for (RecordingLayout layout : layouts) { + for (DataField df : defaultFields) { + boolean hasField = layout.getFields().stream().anyMatch(f -> f.getKey().equals(df.getKey())); + if (!hasField) { + layout.addField(new DataField(df.getKey(), false, false, df.isWide())); + } + } + } + return layouts; } public static void resetCustomLayoutPreferences() { diff --git a/src/main/java/de/dennisguse/opentracks/settings/RecordingSettingsFragment.java b/src/main/java/de/dennisguse/opentracks/settings/RecordingSettingsFragment.java index 7408de78f..71ac834de 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/RecordingSettingsFragment.java +++ b/src/main/java/de/dennisguse/opentracks/settings/RecordingSettingsFragment.java @@ -57,5 +57,15 @@ public class RecordingSettingsFragment extends PreferenceFragmentCompat { ListPreference idleDuration = findPreference(getString(R.string.idle_duration_key)); idleDuration.setEntries(PreferencesUtils.getIdleDurationEntries()); + + ListPreference autoPauseCadence0Speed = findPreference(getString(R.string.settings_recording_auto_pause_on_cadence_0_speed_key)); + if (autoPauseCadence0Speed != null) { + autoPauseCadence0Speed.setEntries(PreferencesUtils.getAutoPauseCadence0SpeedEntries()); + } + + ListPreference markerIdleDuration = findPreference(getString(R.string.marker_idle_duration_key)); + if (markerIdleDuration != null) { + markerIdleDuration.setEntries(PreferencesUtils.getMarkerIdleDurationEntries()); + } } } diff --git a/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/FilterDialogFragment.java b/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/FilterDialogFragment.java index 5241e7c97..19d78072a 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/FilterDialogFragment.java +++ b/src/main/java/de/dennisguse/opentracks/ui/aggregatedStatistics/FilterDialogFragment.java @@ -136,12 +136,6 @@ public class FilterDialogFragment extends DialogFragment { public final String value; public boolean isChecked; - public FilterItem(String id, String value) { - this.id = id; - this.value = value; - this.isChecked = true; - } - public FilterItem(String id, String value, boolean isChecked) { this.id = id; this.value = value; diff --git a/src/main/java/de/dennisguse/opentracks/ui/customRecordingLayout/RecordingLayoutIO.java b/src/main/java/de/dennisguse/opentracks/ui/customRecordingLayout/RecordingLayoutIO.java index 0ebb5f0f8..7b8c31958 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/customRecordingLayout/RecordingLayoutIO.java +++ b/src/main/java/de/dennisguse/opentracks/ui/customRecordingLayout/RecordingLayoutIO.java @@ -40,6 +40,13 @@ public class RecordingLayoutIO { Log.e(TAG, "Invalid CSV layout. It shouldn't happen: " + csvLine); return recordingLayout; } + + String key = fieldParts[0]; + if (key.equals(resources.getString(R.string.stats_custom_layout_sensor_status_key))) { + // Ignore deprecated sensor_status key to safely remove it from user's custom layout + continue; + } + recordingLayout.addField(fromCSV(fieldParts, resources)); } return recordingLayout; diff --git a/src/main/java/de/dennisguse/opentracks/ui/customRecordingLayout/SettingsCustomLayoutEditAdapter.java b/src/main/java/de/dennisguse/opentracks/ui/customRecordingLayout/SettingsCustomLayoutEditAdapter.java index 3d62ca203..841324b84 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/customRecordingLayout/SettingsCustomLayoutEditAdapter.java +++ b/src/main/java/de/dennisguse/opentracks/ui/customRecordingLayout/SettingsCustomLayoutEditAdapter.java @@ -77,7 +77,7 @@ public class SettingsCustomLayoutEditAdapter extends RecyclerView.AdapterNowy segment jest tworzony, jeśli odległość między dwiema lokalizacjami jest większa niż %1$s Maksymalna odległość Rejestrowanie odcinka + Minimalny czas długiej przerwy Rejestrowanie interwału Dokładność GPS-u Data (ISO 8601) @@ -281,6 +282,7 @@ limitations under the License. %1$d m (zalecane) %1$d mil/h %1$d min + %1$d min (zalecane) %1$d s Brak Wyłącz @@ -443,6 +445,8 @@ limitations under the License. Przedział czasu Tętno Zegar + Status auto-pauzy + Status czujników Długość okrążenia Zresetować twoje układy\? Kolarstwo @@ -523,6 +527,10 @@ limitations under the License. Tempo/prędkość okrążenia %1$d ścieżki Przerwa + Długa przerwa (filiżanka) + Czujniki + Odłączono czujnik + Podłączono czujnik Nagrywanie Zmiana motywu może wymagać ręcznego restartu. Użyj ręcznie zdefiniowanych Dynamicznych Kolorów (tylko Android 12+; wymaga ręcznego restartu) @@ -585,4 +593,15 @@ limitations under the License. API panelu OpenTracks %1$d z %2$d: %3$s Użyj wbudowanego głośnika + Czujnik rozłączony. Kontynuować z GPS? + Przełącz + Zatrzymaj przy prowadzeniu roweru + Uruchom autopauzę jeżeli kadencja wynosi 0 a prędkość wskazuje na chód + Maksymalna prędkość prowadzenia + Prędkość poniżej której wykrywane jest prowadzenie roweru (gdy kadencja to 0) + Zapobiegaj pauzie podczas pedałowania + Nie włączaj trybu postoju jeżeli pedałujesz, nawet przy braku ruchu + Rozpoczęto długą przerwę. + Odłączono czujnik. + Podłączono czujnik. diff --git a/src/main/res/values/settings.xml b/src/main/res/values/settings.xml index 5d9c2038e..3c1323a95 100644 --- a/src/main/res/values/settings.xml +++ b/src/main/res/values/settings.xml @@ -128,6 +128,14 @@ idleSpeedDuration 10 0 + + 4 + 6 + 8 + 10 + 12 + + @string/idle_duration_off 5 @@ -139,6 +147,17 @@ 120 + 3 + + 1 + 2 + @string/marker_idle_duration_default + 5 + 10 + 15 + 30 + + statsRate @string/stats_rate_speed_or_pace_default @@ -243,6 +262,12 @@ true voice_announce_idle_key false + voice_announce_long_break_key + true + voice_announce_sensor_disconnected_key + true + voice_announce_sensor_connected_key + true voiceAnnounceTotalDistance true @@ -379,6 +404,14 @@ average_cadence power time + auto_pause_status + sensor_status + sensor_status_hr + sensor_status_cadence + sensor_status_speed + debug_idle_timer + debug_walking_timer + debug_marker_timer 0 diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index c36234e09..dc51c72ef 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -331,6 +331,10 @@ limitations under the License. Cycling Sensors, Wheel Circumference, Running Sensors Idle Announcements Idle + Long break + Sensors + Sensor disconnected + Sensor connected Statistics Announcements Time, Distance, Voice Speed Import and Export @@ -369,7 +373,9 @@ limitations under the License. A new segment is created if the distance between two locations is greater than %1$s Max distance Recording distance interval - Recording time interval + marker_idle_duration + Marker idle duration + recording_min_recording_interval_key Location is ignored if its accuracy is greater than %1$s GPS accuracy Date (ISO 8601) @@ -478,6 +484,14 @@ limitations under the License. Cadence Power Clock + Auto-Pause Status + Sensor HR + Sensor Cadence + Sensor Speed + Idle Timer + Walking Timer + Marker Timer + Track %1$d Location accuracy: %1$s @@ -536,6 +550,7 @@ limitations under the License. %1$.1f mi/h (recommended) %1$.1f knots (recommended) %1$d min + %1$d min (recommended) %1$d s None Off @@ -543,6 +558,9 @@ limitations under the License. Smallest (recommended) %1$d s (recommended) Becoming idle. + Long break recorded. + Sensor disconnected. + Sensor connected. {n, plural, =1 {1 hour} @@ -705,4 +723,16 @@ limitations under the License. Lap power Power {x, plural, =1 {1 watt} other {# watts} } Power + preventIdleIfCadence + Prevent auto-pause if pedaling + Do not enter idle mode while pedaling even if not moving + Sensor disconnected. Continue with GPS? + Switch + autoPauseCadence0 + Auto-pause when carrying bike + Pause recording if cadence is 0 and speed is low + autoPauseCadence0Speed + Auto-pause max speed + Speed below which walking with bike is detected (if cadence is 0) + 8 diff --git a/src/main/res/xml/settings_announcements.xml b/src/main/res/xml/settings_announcements.xml index 3adf5fa29..919270f62 100644 --- a/src/main/res/xml/settings_announcements.xml +++ b/src/main/res/xml/settings_announcements.xml @@ -22,6 +22,21 @@ android:defaultValue="@bool/voice_announce_idle_default" android:key="@string/voice_announce_idle_key" android:title="@string/settings_announcements_idle_title" /> + + + + + + diff --git a/src/main/res/xml/settings_recording.xml b/src/main/res/xml/settings_recording.xml index 2a16d3041..d4dc66d0d 100644 --- a/src/main/res/xml/settings_recording.xml +++ b/src/main/res/xml/settings_recording.xml @@ -4,6 +4,25 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:title="@string/settings_recording_title"> + + + + + + + + \ No newline at end of file