forked from upstream-mirrors/OpenTracks
feat: prioritize Bluetooth sensor data over GPS
- Restructured sensor logic to give absolute priority to Bluetooth sensors, largely ignoring GPS data for distance and speed calculations. - Implemented downstream UI expansions, custom recording layouts, and new settings to reflect and control the new sensor-first flow. - Added MediaNotificationListenerService and integrated media playback tracking within statistics. - Modified VoiceAnnouncementUtils to harmonize announcements with the updated architecture. - Added 'About This Fork' section to the README. Note: These modifications were co-created with Antigravity AI.
This commit is contained in:
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AndroidProjectSystem">
|
||||||
|
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+23
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetSelector">
|
||||||
|
<selectionStates>
|
||||||
|
<SelectionState runConfigName="OpenTracks">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DialogSelection />
|
||||||
|
</SelectionState>
|
||||||
|
<SelectionState runConfigName="androidTest">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DialogSelection />
|
||||||
|
</SelectionState>
|
||||||
|
<SelectionState runConfigName="unitTest">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DialogSelection />
|
||||||
|
</SelectionState>
|
||||||
|
<SelectionState runConfigName="main">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DialogSelection />
|
||||||
|
</SelectionState>
|
||||||
|
</selectionStates>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MarkdownSettings">
|
||||||
|
<option name="previewPanelProviderInfo">
|
||||||
|
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectMigrations">
|
||||||
|
<option name="MigrateToGradleLocalJavaHome">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+17
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RunConfigurationProducerService">
|
||||||
|
<option name="ignoredProducers">
|
||||||
|
<set>
|
||||||
|
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -45,6 +45,18 @@ Translations are hosted on <a href="https://translate.codeberg.org/engage/opentr
|
|||||||
<img src="https://translate.codeberg.org/widget/opentracks/opentracks-src-main-res-values-strings-xml-main/horizontal-auto.svg" alt="Translation status" />
|
<img src="https://translate.codeberg.org/widget/opentracks/opentracks-src-main-res-values-strings-xml-main/horizontal-auto.svg" alt="Translation status" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
## ⚠️ About This Fork
|
||||||
|
|
||||||
|
This repository is a customized fork of the official OpenTracks application.
|
||||||
|
|
||||||
|
**Core Purpose:**
|
||||||
|
The primary goal of this fork is to fundamentally change how Bluetooth sensors are prioritized over GPS data. In this modified version, Bluetooth sensor data is given absolute priority and actively ignores/overrides GPS signals in most scenarios. This ensures that cadence, speed, and distance measurements strictly rely on hardware sensors rather than falling back to or being averaged with GPS data.
|
||||||
|
|
||||||
|
**Consequential Changes:**
|
||||||
|
As a direct result of these sensor priority changes, several downstream modifications were implemented to support the new data flow:
|
||||||
|
* **UI & Settings:** Expanded settings menus and customized recording layouts to better expose the raw sensor data and give the user finer control.
|
||||||
|
* **Media & Voice Announcements:** Integrated media playback controls directly into the statistics view and overhauled voice announcements to handle audio focus and integrate with the new sensor-first approach.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#This file is generated by updateDaemonJvm
|
||||||
|
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||||
|
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||||
|
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||||
|
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||||
|
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect
|
||||||
|
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect
|
||||||
|
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
|
||||||
|
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
|
||||||
|
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect
|
||||||
|
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect
|
||||||
|
toolchainVendor=JETBRAINS
|
||||||
|
toolchainVersion=21
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
|||||||
|
cat << 'EOC' > src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java.patch
|
||||||
|
--- src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java
|
||||||
|
+++ src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java
|
||||||
|
@@ -351,6 +351,11 @@
|
||||||
|
public static boolean shouldVoiceAnnouncementLongBreak() {
|
||||||
|
return getBoolean(R.string.settings_announcements_long_break_key, true);
|
||||||
|
}
|
||||||
|
+
|
||||||
|
+ public static long getLongBreakDuration() {
|
||||||
|
+ int minutes = getInt(R.string.settings_long_break_duration_key, 3);
|
||||||
|
+ return minutes * 60 * 1000L;
|
||||||
|
+ }
|
||||||
|
EOC
|
||||||
|
patch -p0 < src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java.patch
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
cat << 'EOC' > src/main/res/values/strings.xml.patch
|
||||||
|
--- src/main/res/values/strings.xml
|
||||||
|
+++ src/main/res/values/strings.xml
|
||||||
|
@@ -1071,6 +1071,8 @@
|
||||||
|
|
||||||
|
<string name="settings_announcements_long_break_key">settings_announcements_long_break_key</string>
|
||||||
|
<string name="settings_announcements_long_break_title">Long break</string>
|
||||||
|
+ <string name="settings_long_break_duration_key">settings_long_break_duration_key</string>
|
||||||
|
+ <string name="settings_long_break_duration_title">Long break duration (minutes)</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
|
EOC
|
||||||
|
patch -p0 < src/main/res/values/strings.xml.patch
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
cat src/main/java/de/dennisguse/opentracks/data/statistics/TrackStatisticsUpdater.java | grep -n "if (movingDistance != null) {" -B 2 -A 30
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
plugins {
|
||||||
|
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||||
|
}
|
||||||
|
rootProject.name = 'OpenTracks'
|
||||||
+68
-17
@@ -92,7 +92,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -132,7 +135,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -172,7 +178,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -212,7 +221,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -254,7 +266,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -276,7 +291,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -316,7 +334,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -338,7 +359,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -378,7 +402,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -418,7 +445,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -458,7 +488,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -498,7 +531,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -520,7 +556,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -560,7 +599,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -582,7 +624,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -610,7 +655,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -644,7 +692,10 @@ public class VoiceAnnouncementUtilsTest {
|
|||||||
new SensorData<>(HeartRate.of(60), "unused"),
|
new SensorData<>(HeartRate.of(60), "unused"),
|
||||||
null, null,
|
null, null,
|
||||||
null,
|
null,
|
||||||
null
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
|
|||||||
@@ -421,6 +421,16 @@ limitations under the License.
|
|||||||
android:icon="@drawable/ic_logo_color_24dp"
|
android:icon="@drawable/ic_logo_color_24dp"
|
||||||
android:label="@string/recording_service" />
|
android:label="@string/recording_service" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".services.MediaNotificationListenerService"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.notification.NotificationListenerService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
<!--Necessary for SDK < 33 to (re-)store per app locale information-->
|
<!--Necessary for SDK < 33 to (re-)store per app locale information-->
|
||||||
<service
|
<service
|
||||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
|
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
|
||||||
|
|
||||||
private Snackbar snackbar;
|
private Snackbar snackbar;
|
||||||
|
private Snackbar sensorWarningSnackbar;
|
||||||
|
|
||||||
// The following are setFrequency in onCreate
|
// The following are setFrequency in onCreate
|
||||||
private ContentProviderUtils contentProviderUtils;
|
private ContentProviderUtils contentProviderUtils;
|
||||||
@@ -86,6 +87,9 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
service.getGpsStatusObservable()
|
service.getGpsStatusObservable()
|
||||||
.observe(TrackRecordingActivity.this, this::onGpsStatusChanged);
|
.observe(TrackRecordingActivity.this, this::onGpsStatusChanged);
|
||||||
|
|
||||||
|
service.getSensorDisconnectedWarningObservable()
|
||||||
|
.observe(TrackRecordingActivity.this, this::onSensorDisconnectedWarningChanged);
|
||||||
|
|
||||||
if (!service.isRecording()) {
|
if (!service.isRecording()) {
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
@@ -408,4 +412,27 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
});
|
});
|
||||||
snackbar.show();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -151,12 +151,14 @@ public class SegmentStatisticUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void addMovingTime(TrackPoint trackPoint, TrackPoint lastTrackPoint) {
|
public void addMovingTime(TrackPoint trackPoint, TrackPoint lastTrackPoint) {
|
||||||
Duration movingDuration = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
|
addMovingTime(Duration.between(lastTrackPoint.getTime(), trackPoint.getTime()));
|
||||||
|
}
|
||||||
|
|
||||||
if (movingDuration.isNegative()) {
|
public void addMovingTime(Duration duration) {
|
||||||
throw new RuntimeException("Moving time cannot be negative: " + lastTrackPoint.getTime() + " is after " + trackPoint.getTime());
|
if (duration.isNegative()) {
|
||||||
|
throw new RuntimeException("Moving time cannot be negative");
|
||||||
}
|
}
|
||||||
this.movingDuration = this.movingDuration.plus(movingDuration);
|
movingDuration = movingDuration.plus(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Speed getAverageMovingSpeed() {
|
private Speed getAverageMovingSpeed() {
|
||||||
|
|||||||
+21
-11
@@ -143,7 +143,6 @@ public class TrackStatisticsUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Update total distance
|
|
||||||
Distance movingDistance = null;
|
Distance movingDistance = null;
|
||||||
if (trackPoint.sensorDistance() != null) {
|
if (trackPoint.sensorDistance() != null) {
|
||||||
movingDistance = trackPoint.sensorDistance();
|
movingDistance = trackPoint.sensorDistance();
|
||||||
@@ -153,16 +152,7 @@ public class TrackStatisticsUpdater {
|
|||||||
// GPS-based distance/speed
|
// GPS-based distance/speed
|
||||||
movingDistance = trackPoint.distanceToPrevious(lastTrackPoint);
|
movingDistance = trackPoint.distanceToPrevious(lastTrackPoint);
|
||||||
}
|
}
|
||||||
if (movingDistance != null) {
|
boolean isIdleTransition = false;
|
||||||
currentSegment.addTotalDistance(movingDistance);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isIdle
|
|
||||||
&& !trackPoint.isSegmentManualStart()
|
|
||||||
&& lastTrackPoint != null) {
|
|
||||||
currentSegment.addMovingTime(trackPoint, lastTrackPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackPoint.isIdleTriggered()) {
|
if (trackPoint.isIdleTriggered()) {
|
||||||
isIdle = true;
|
isIdle = true;
|
||||||
} else if (isIdle) {
|
} else if (isIdle) {
|
||||||
@@ -170,6 +160,26 @@ public class TrackStatisticsUpdater {
|
|||||||
if (movingDistance != null
|
if (movingDistance != null
|
||||||
&& movingDistance.greaterOrEqualThan(PreferencesUtils.getRecordingDistanceInterval())) {
|
&& movingDistance.greaterOrEqualThan(PreferencesUtils.getRecordingDistanceInterval())) {
|
||||||
isIdle = false;
|
isIdle = false;
|
||||||
|
isIdleTransition = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movingDistance != null && !isIdle) {
|
||||||
|
currentSegment.addTotalDistance(movingDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isIdle
|
||||||
|
&& !trackPoint.isSegmentManualStart()
|
||||||
|
&& lastTrackPoint != null) {
|
||||||
|
if (isIdleTransition) {
|
||||||
|
if (trackPoint.position().hasSpeed() && trackPoint.position().speed().toMPS() > 0) {
|
||||||
|
double timeSeconds = movingDistance.toM() / trackPoint.position().speed().toMPS();
|
||||||
|
currentSegment.addMovingTime(Duration.ofMillis((long)(timeSeconds * 1000)));
|
||||||
|
} else {
|
||||||
|
currentSegment.addMovingTime(Duration.ofMillis((long)(movingDistance.toM() * 1000)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentSegment.addMovingTime(trackPoint, lastTrackPoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -189,6 +189,17 @@ public class DataProvider {
|
|||||||
Log.i(TAG, "Starting dashboard activity with generic intent (package=" + targetPackage + ", class=" + targetClass + ")");
|
Log.i(TAG, "Starting dashboard activity with generic intent (package=" + targetPackage + ", class=" + targetClass + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Explicitly grant URI permissions to all packages that can resolve this intent.
|
||||||
|
// This works around issues where ClipData multiple URI grants fail on some Android versions.
|
||||||
|
android.content.pm.PackageManager packageManager = context.getPackageManager();
|
||||||
|
java.util.List<android.content.pm.ResolveInfo> resolveInfos = packageManager.queryIntentActivities(intent, android.content.pm.PackageManager.MATCH_DEFAULT_ONLY);
|
||||||
|
for (android.content.pm.ResolveInfo resolveInfo : resolveInfos) {
|
||||||
|
String packageName = resolveInfo.activityInfo.packageName;
|
||||||
|
for (Uri uri : uris) {
|
||||||
|
context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
context.startActivity(intent);
|
context.startActivity(intent);
|
||||||
} catch (ActivityNotFoundException e) {
|
} catch (ActivityNotFoundException e) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.annotation.VisibleForTesting;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||||
import de.dennisguse.opentracks.sensors.driver.DriverObserver;
|
import de.dennisguse.opentracks.sensors.driver.DriverObserver;
|
||||||
import de.dennisguse.opentracks.sensors.driver.SensorType;
|
import de.dennisguse.opentracks.sensors.driver.SensorType;
|
||||||
@@ -28,6 +29,9 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
|
|||||||
|
|
||||||
protected Aggregator<AggregatorInput, AggregatorOutput> aggregator;
|
protected Aggregator<AggregatorInput, AggregatorOutput> aggregator;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Integer batteryLevel = null;
|
||||||
|
|
||||||
protected SensorHandler(SensorManager sensorManager) {
|
protected SensorHandler(SensorManager sensorManager) {
|
||||||
this.sensorManager = sensorManager;
|
this.sensorManager = sensorManager;
|
||||||
}
|
}
|
||||||
@@ -115,7 +119,8 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SensorHandler.this.aggregator.resetAggregated();
|
SensorHandler.this.aggregator = null;
|
||||||
|
sensorManager.onChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -130,5 +135,18 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
|
|||||||
@Override
|
@Override
|
||||||
public void onDisconnected() {
|
public void onDisconnected() {
|
||||||
SensorHandler.this.aggregator = null;
|
SensorHandler.this.aggregator = null;
|
||||||
|
batteryLevel = null;
|
||||||
|
sensorManager.onChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBatteryLevelReceived(int percentage) {
|
||||||
|
batteryLevel = percentage;
|
||||||
|
sensorManager.onChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Integer getBatteryLevel() {
|
||||||
|
return batteryLevel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import androidx.annotation.VisibleForTesting;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
||||||
|
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||||
import de.dennisguse.opentracks.data.models.Cadence;
|
import de.dennisguse.opentracks.data.models.Cadence;
|
||||||
import de.dennisguse.opentracks.data.models.Distance;
|
import de.dennisguse.opentracks.data.models.Distance;
|
||||||
import de.dennisguse.opentracks.data.models.Position;
|
import de.dennisguse.opentracks.data.models.Position;
|
||||||
@@ -239,6 +241,9 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
|||||||
}
|
}
|
||||||
|
|
||||||
public SensorDataSet getSensorDataSet(Instant now) {
|
public SensorDataSet getSensorDataSet(Instant now) {
|
||||||
|
if (!isStarted()) {
|
||||||
|
return new SensorDataSet(new SensorData<>(Position.of(now), ""), null, null, null, null, null, null, null, null, null, null);
|
||||||
|
}
|
||||||
//We always need a Position with now
|
//We always need a Position with now
|
||||||
SensorData<Position> gpsSensorData = gpsHandler.getSensorData(now);
|
SensorData<Position> gpsSensorData = gpsHandler.getSensorData(now);
|
||||||
if (gpsSensorData != null && gpsSensorData.data() != null) {
|
if (gpsSensorData != null && gpsSensorData.data() != null) {
|
||||||
@@ -247,6 +252,13 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
|||||||
gpsSensorData = new SensorData<>(Position.of(now), "");
|
gpsSensorData = new SensorData<>(Position.of(now), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BatteryLevel cadenceBattery = cyclingCadenceHandler.getBatteryLevel() != null
|
||||||
|
? BatteryLevel.of(cyclingCadenceHandler.getBatteryLevel()) : null;
|
||||||
|
BatteryLevel speedBattery = cyclingDistanceSpeedHandler.getBatteryLevel() != null
|
||||||
|
? BatteryLevel.of(cyclingDistanceSpeedHandler.getBatteryLevel()) : null;
|
||||||
|
BatteryLevel heartRateBattery = heartRateHandler.getBatteryLevel() != null
|
||||||
|
? BatteryLevel.of(heartRateHandler.getBatteryLevel()) : null;
|
||||||
|
|
||||||
return new SensorDataSet(
|
return new SensorDataSet(
|
||||||
gpsSensorData,
|
gpsSensorData,
|
||||||
getSpeed(now),
|
getSpeed(now),
|
||||||
@@ -255,7 +267,10 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
|||||||
temperatureHandler.getSensorData(now),
|
temperatureHandler.getSensorData(now),
|
||||||
getCadence(now),
|
getCadence(now),
|
||||||
cyclingPowerHandler.getSensorData(now),
|
cyclingPowerHandler.getSensorData(now),
|
||||||
altitudeChangeHandler.getSensorData(now)
|
altitudeChangeHandler.getSensorData(now),
|
||||||
|
cadenceBattery,
|
||||||
|
speedBattery,
|
||||||
|
heartRateBattery
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||||
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
||||||
|
|
||||||
@@ -53,6 +54,9 @@ public class BluetoothDriver<T> implements Driver {
|
|||||||
|
|
||||||
private final BluetoothAdapter bluetoothAdapter;
|
private final BluetoothAdapter bluetoothAdapter;
|
||||||
private BluetoothGatt bluetoothGatt;
|
private BluetoothGatt bluetoothGatt;
|
||||||
|
private Context context;
|
||||||
|
private Handler handler;
|
||||||
|
private String address;
|
||||||
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
|
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
|
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
|
||||||
@@ -68,10 +72,21 @@ public class BluetoothDriver<T> implements Driver {
|
|||||||
case BluetoothProfile.STATE_DISCONNECTED -> {
|
case BluetoothProfile.STATE_DISCONNECTED -> {
|
||||||
//This is also triggered, if no connection was established (ca. 30s)
|
//This is also triggered, if no connection was established (ca. 30s)
|
||||||
Log.i(TAG, gatt.getDevice() + ": disconnected from sensor: trying to reconnect");
|
Log.i(TAG, gatt.getDevice() + ": disconnected from sensor: trying to reconnect");
|
||||||
if (gatt.connect()) {
|
|
||||||
Log.e(TAG, gatt.getDevice() + ": could not trigger reconnect for sensor");
|
|
||||||
}
|
|
||||||
observer.onConnectionLost();
|
observer.onConnectionLost();
|
||||||
|
|
||||||
|
synchronized (BluetoothDriver.this) {
|
||||||
|
gatt.close();
|
||||||
|
if (bluetoothGatt == gatt) {
|
||||||
|
bluetoothGatt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context != null && handler != null && address != null) {
|
||||||
|
final Context ctx = context;
|
||||||
|
final Handler hnd = handler;
|
||||||
|
final String addr = address;
|
||||||
|
hnd.post(() -> connect(ctx, hnd, addr));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,9 +131,39 @@ public class BluetoothDriver<T> implements Driver {
|
|||||||
Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID descriptor could not be written");
|
Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID descriptor could not be written");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: battery read is triggered in onDescriptorWrite, after this write completes.
|
||||||
|
// Attempting readCharacteristic here would fail silently because GATT
|
||||||
|
// allows only one pending operation at a time.
|
||||||
|
|
||||||
observer.onConnected(device.getAddress(), device.getName());
|
observer.onConnected(device.getAddress(), device.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||||
|
// After the notification descriptor write completes, try to read the battery level.
|
||||||
|
// This is the correct moment — GATT is idle and can process a new operation.
|
||||||
|
BluetoothGattService batteryService = gatt.getService(BluetoothUtils.BATTERY.serviceUUID());
|
||||||
|
if (batteryService != null) {
|
||||||
|
BluetoothGattCharacteristic batteryChar = batteryService.getCharacteristic(BluetoothUtils.BATTERY.measurementUUID());
|
||||||
|
if (batteryChar != null) {
|
||||||
|
boolean queued = gatt.readCharacteristic(batteryChar);
|
||||||
|
Log.d(TAG, gatt.getDevice() + ": battery read queued=" + queued);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCharacteristicRead(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic, int status) {
|
||||||
|
if (status == BluetoothGatt.GATT_SUCCESS
|
||||||
|
&& characteristic.getUuid().equals(BluetoothUtils.BATTERY.measurementUUID())) {
|
||||||
|
BatteryLevel level = BluetoothUtils.parseBatteryLevel(characteristic);
|
||||||
|
if (level != null) {
|
||||||
|
Log.d(TAG, gatt.getDevice() + ": battery level " + level.percentage() + "%");
|
||||||
|
observer.onBatteryLevelReceived(level.percentage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
|
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||||
UUID serviceUUID = characteristic.getService().getUuid();
|
UUID serviceUUID = characteristic.getService().getUuid();
|
||||||
@@ -169,6 +214,10 @@ public class BluetoothDriver<T> implements Driver {
|
|||||||
|
|
||||||
Log.d(TAG, device + ": trying to connect");
|
Log.d(TAG, device + ": trying to connect");
|
||||||
|
|
||||||
|
this.context = context;
|
||||||
|
this.handler = handler;
|
||||||
|
this.address = address;
|
||||||
|
|
||||||
bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, handler);
|
bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +233,10 @@ public class BluetoothDriver<T> implements Driver {
|
|||||||
Log.i(TAG, bluetoothGatt.getDevice() + ": disconnect finished");
|
Log.i(TAG, bluetoothGatt.getDevice() + ": disconnect finished");
|
||||||
bluetoothGatt = null;
|
bluetoothGatt = null;
|
||||||
|
|
||||||
|
this.context = null;
|
||||||
|
this.handler = null;
|
||||||
|
this.address = null;
|
||||||
|
|
||||||
observer.onDisconnected();
|
observer.onDisconnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,4 +28,10 @@ public interface DriverObserver<T> {
|
|||||||
* For example, the user deactivated the internal GPS.
|
* For example, the user deactivated the internal GPS.
|
||||||
*/
|
*/
|
||||||
default void onSensorDeactivated() {}
|
default void onSensorDeactivated() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Driver read the battery level from the Battery Service (0x180F).
|
||||||
|
* Only called if the device exposes the Battery Service.
|
||||||
|
*/
|
||||||
|
default void onBatteryLevelReceived(int percentage) {}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-2
@@ -5,6 +5,7 @@ import android.util.Log;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.data.models.Cadence;
|
import de.dennisguse.opentracks.data.models.Cadence;
|
||||||
import de.dennisguse.opentracks.sensors.UintUtils;
|
import de.dennisguse.opentracks.sensors.UintUtils;
|
||||||
@@ -18,17 +19,20 @@ public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth
|
|||||||
super(sensorAddress, sensorName);
|
super(sensorAddress, sensorName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Instant lastValidUpdate;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void computeValue(@NonNull Raw<CyclingCadenceBluetooth.CyclingCadenceMeasurement> current) {
|
protected void computeValue(@NonNull Raw<CyclingCadenceBluetooth.CyclingCadenceMeasurement> current) {
|
||||||
if (current.value() instanceof CyclingCadenceBluetooth.DirectCadenceData directCadenceData) {
|
if (current.value() instanceof CyclingCadenceBluetooth.DirectCadenceData directCadenceData) {
|
||||||
output = directCadenceData.cadence();
|
output = directCadenceData.cadence();
|
||||||
|
lastValidUpdate = current.time();
|
||||||
return;
|
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();
|
CyclingCadenceBluetooth.CrankData previousRaw = getPreviousRaw();
|
||||||
if (previousRaw == null) return;
|
if (previousRaw == null) return;
|
||||||
|
|
||||||
@@ -36,6 +40,9 @@ public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth
|
|||||||
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
||||||
|
|
||||||
if (timeDiff.isZero()) {
|
if (timeDiff.isZero()) {
|
||||||
|
if (lastValidUpdate != null && Duration.between(lastValidUpdate, now).compareTo(getMaxSensorAge()) > 0) {
|
||||||
|
resetOutdated();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (timeDiff.isNegative()) {
|
if (timeDiff.isNegative()) {
|
||||||
@@ -52,9 +59,15 @@ public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth
|
|||||||
}
|
}
|
||||||
|
|
||||||
long crankDiff = UintUtils.diff(currentRaw.crankRevolutionsCount(), previousRaw.crankRevolutionsCount(), UintUtils.UINT32_MAX);
|
long crankDiff = UintUtils.diff(currentRaw.crankRevolutionsCount(), previousRaw.crankRevolutionsCount(), UintUtils.UINT32_MAX);
|
||||||
|
lastValidUpdate = now;
|
||||||
output = Cadence.of(crankDiff, timeDiff);
|
output = Cadence.of(crankDiff, timeDiff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void resetOutdated() {
|
||||||
|
output = Cadence.of(0f);
|
||||||
|
}
|
||||||
|
|
||||||
private CyclingCadenceBluetooth.CrankData getPreviousRaw() {
|
private CyclingCadenceBluetooth.CrankData getPreviousRaw() {
|
||||||
if (previous == null) {
|
if (previous == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+8
@@ -5,6 +5,7 @@ import android.util.Log;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.data.models.Distance;
|
import de.dennisguse.opentracks.data.models.Distance;
|
||||||
import de.dennisguse.opentracks.data.models.Speed;
|
import de.dennisguse.opentracks.data.models.Speed;
|
||||||
@@ -21,9 +22,12 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<CyclingDistanceSp
|
|||||||
super(sensorAddress, sensorName);
|
super(sensorAddress, sensorName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Instant lastValidUpdate;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void computeValue(@NonNull Raw<CyclingDistanceSpeedBluetooth.WheelData> current) {
|
protected void computeValue(@NonNull Raw<CyclingDistanceSpeedBluetooth.WheelData> current) {
|
||||||
if (previous == null) {
|
if (previous == null) {
|
||||||
|
lastValidUpdate = current.time();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +35,9 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<CyclingDistanceSp
|
|||||||
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
||||||
|
|
||||||
if (timeDiff.isZero()) {
|
if (timeDiff.isZero()) {
|
||||||
|
if (lastValidUpdate != null && Duration.between(lastValidUpdate, current.time()).compareTo(getMaxSensorAge()) > 0) {
|
||||||
|
resetOutdated();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (timeDiff.isNegative()) {
|
if (timeDiff.isNegative()) {
|
||||||
@@ -52,6 +59,7 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<CyclingDistanceSp
|
|||||||
distanceOverall = distance.plus(output.distanceOverall);
|
distanceOverall = distance.plus(output.distanceOverall);
|
||||||
}
|
}
|
||||||
Speed speed_mps = Speed.of(distance, timeDiff);
|
Speed speed_mps = Speed.of(distance, timeDiff);
|
||||||
|
lastValidUpdate = current.time();
|
||||||
output = new Data(distance, distanceOverall, speed_mps);
|
output = new Data(distance, distanceOverall, speed_mps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
||||||
|
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||||
import de.dennisguse.opentracks.data.models.Cadence;
|
import de.dennisguse.opentracks.data.models.Cadence;
|
||||||
import de.dennisguse.opentracks.data.models.Distance;
|
import de.dennisguse.opentracks.data.models.Distance;
|
||||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||||
@@ -27,6 +28,12 @@ public record SensorDataSet(
|
|||||||
@Nullable
|
@Nullable
|
||||||
SensorData<Power> power,
|
SensorData<Power> power,
|
||||||
@Nullable
|
@Nullable
|
||||||
SensorData<AltitudeGainLoss> altitudeGainLoss
|
SensorData<AltitudeGainLoss> altitudeGainLoss,
|
||||||
|
@Nullable
|
||||||
|
BatteryLevel cadenceBattery,
|
||||||
|
@Nullable
|
||||||
|
BatteryLevel speedBattery,
|
||||||
|
@Nullable
|
||||||
|
BatteryLevel heartRateBattery
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
+174
@@ -0,0 +1,174 @@
|
|||||||
|
package de.dennisguse.opentracks.services;
|
||||||
|
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.media.MediaMetadata;
|
||||||
|
import android.media.session.MediaController;
|
||||||
|
import android.media.session.MediaSessionManager;
|
||||||
|
import android.media.session.PlaybackState;
|
||||||
|
import android.service.notification.NotificationListenerService;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MediaNotificationListenerService extends NotificationListenerService {
|
||||||
|
|
||||||
|
private static final String TAG = MediaNotificationListenerService.class.getSimpleName();
|
||||||
|
|
||||||
|
private MediaSessionManager mediaSessionManager;
|
||||||
|
private MediaController currentController;
|
||||||
|
|
||||||
|
private static final MutableLiveData<MediaData> mediaDataLiveData = new MutableLiveData<>(new MediaData(null, null, false, false));
|
||||||
|
private static MediaNotificationListenerService instance;
|
||||||
|
|
||||||
|
public static LiveData<MediaData> getMediaDataObservable() {
|
||||||
|
return mediaDataLiveData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MediaNotificationListenerService getInstance() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final MediaController.Callback mediaControllerCallback = new MediaController.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onPlaybackStateChanged(PlaybackState state) {
|
||||||
|
updateMediaData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMetadataChanged(MediaMetadata metadata) {
|
||||||
|
updateMediaData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final MediaSessionManager.OnActiveSessionsChangedListener sessionsChangedListener = controllers -> {
|
||||||
|
Log.d(TAG, "Active sessions changed");
|
||||||
|
findActiveMediaController(controllers);
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
instance = this;
|
||||||
|
mediaSessionManager = (MediaSessionManager) getSystemService(MEDIA_SESSION_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onListenerConnected() {
|
||||||
|
super.onListenerConnected();
|
||||||
|
Log.d(TAG, "Listener connected");
|
||||||
|
mediaDataLiveData.postValue(new MediaData(null, null, false, true));
|
||||||
|
try {
|
||||||
|
ComponentName componentName = new ComponentName(this, MediaNotificationListenerService.class);
|
||||||
|
mediaSessionManager.addOnActiveSessionsChangedListener(sessionsChangedListener, componentName);
|
||||||
|
findActiveMediaController(mediaSessionManager.getActiveSessions(componentName));
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
Log.e(TAG, "SecurityException while accessing MediaSessionManager", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onListenerDisconnected() {
|
||||||
|
super.onListenerDisconnected();
|
||||||
|
Log.d(TAG, "Listener disconnected");
|
||||||
|
mediaDataLiveData.postValue(new MediaData(null, null, false, false));
|
||||||
|
if (mediaSessionManager != null) {
|
||||||
|
mediaSessionManager.removeOnActiveSessionsChangedListener(sessionsChangedListener);
|
||||||
|
}
|
||||||
|
if (currentController != null) {
|
||||||
|
currentController.unregisterCallback(mediaControllerCallback);
|
||||||
|
currentController = null;
|
||||||
|
}
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void findActiveMediaController(List<MediaController> controllers) {
|
||||||
|
if (controllers == null || controllers.isEmpty()) {
|
||||||
|
if (currentController != null) {
|
||||||
|
currentController.unregisterCallback(mediaControllerCallback);
|
||||||
|
currentController = null;
|
||||||
|
}
|
||||||
|
updateMediaData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaController newController = controllers.get(0);
|
||||||
|
for (MediaController controller : controllers) {
|
||||||
|
PlaybackState state = controller.getPlaybackState();
|
||||||
|
if (state != null && (state.getState() == PlaybackState.STATE_PLAYING || state.getState() == PlaybackState.STATE_BUFFERING)) {
|
||||||
|
newController = controller;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentController != null && currentController.getSessionToken().equals(newController.getSessionToken())) {
|
||||||
|
// Already tracking this controller
|
||||||
|
updateMediaData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentController != null) {
|
||||||
|
currentController.unregisterCallback(mediaControllerCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentController = newController;
|
||||||
|
currentController.registerCallback(mediaControllerCallback);
|
||||||
|
updateMediaData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMediaData() {
|
||||||
|
if (currentController == null) {
|
||||||
|
mediaDataLiveData.postValue(new MediaData(null, null, false, true));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaMetadata metadata = currentController.getMetadata();
|
||||||
|
PlaybackState playbackState = currentController.getPlaybackState();
|
||||||
|
|
||||||
|
String title = null;
|
||||||
|
String artist = null;
|
||||||
|
if (metadata != null) {
|
||||||
|
title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE);
|
||||||
|
artist = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isPlaying = false;
|
||||||
|
if (playbackState != null) {
|
||||||
|
isPlaying = playbackState.getState() == PlaybackState.STATE_PLAYING ||
|
||||||
|
playbackState.getState() == PlaybackState.STATE_BUFFERING;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaDataLiveData.postValue(new MediaData(title, artist, isPlaying, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMediaCommand(int keyCode) {
|
||||||
|
if (currentController != null) {
|
||||||
|
currentController.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
|
||||||
|
currentController.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
||||||
|
} else {
|
||||||
|
// Fallback if no active controller (will broadcast to the system)
|
||||||
|
android.media.AudioManager audioManager = (android.media.AudioManager) getSystemService(AUDIO_SERVICE);
|
||||||
|
if (audioManager != null) {
|
||||||
|
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
|
||||||
|
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MediaData {
|
||||||
|
public final String title;
|
||||||
|
public final String artist;
|
||||||
|
public final boolean isPlaying;
|
||||||
|
public final boolean isPermissionGranted;
|
||||||
|
|
||||||
|
public MediaData(String title, String artist, boolean isPlaying, boolean isPermissionGranted) {
|
||||||
|
this.title = title;
|
||||||
|
this.artist = artist;
|
||||||
|
this.isPlaying = isPlaying;
|
||||||
|
this.isPermissionGranted = isPermissionGranted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,9 +16,13 @@ public record RecordingData(
|
|||||||
*/
|
*/
|
||||||
TrackPoint latestTrackPoint,
|
TrackPoint latestTrackPoint,
|
||||||
SensorDataSet sensorDataSet,
|
SensorDataSet sensorDataSet,
|
||||||
Statistics currentSegment) {
|
Statistics currentSegment,
|
||||||
|
String autoPauseStatus,
|
||||||
|
String idleTimer,
|
||||||
|
String walkingTimer,
|
||||||
|
String markerTimer) {
|
||||||
|
|
||||||
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null, null);
|
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null, null, "", "", "", "");
|
||||||
|
|
||||||
public Statistics trackStatistics() {
|
public Statistics trackStatistics() {
|
||||||
return track.statistics();
|
return track.statistics();
|
||||||
|
|||||||
@@ -36,10 +36,12 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
|||||||
private static final AltitudeCorrectionManager ALTITUDE_CORRECTION_MANAGER = new AltitudeCorrectionManager();
|
private static final AltitudeCorrectionManager ALTITUDE_CORRECTION_MANAGER = new AltitudeCorrectionManager();
|
||||||
|
|
||||||
private final Runnable ON_IDLE = this::onIdle;
|
private final Runnable ON_IDLE = this::onIdle;
|
||||||
|
private final Runnable ON_LONG_BREAK = this::onLongBreak;
|
||||||
|
|
||||||
private final ContentProviderUtils contentProviderUtils;
|
private final ContentProviderUtils contentProviderUtils;
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final IdleObserver idleObserver;
|
private final IdleObserver idleObserver;
|
||||||
|
private final SensorDisconnectObserver sensorDisconnectObserver;
|
||||||
|
|
||||||
private final Handler handler;
|
private final Handler handler;
|
||||||
|
|
||||||
@@ -59,9 +61,25 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
|||||||
private TrackPoint lastStoredTrackPoint;
|
private TrackPoint lastStoredTrackPoint;
|
||||||
private TrackPoint lastStoredTrackPointWithLocation;
|
private TrackPoint lastStoredTrackPointWithLocation;
|
||||||
|
|
||||||
TrackRecordingManager(Context context, TrackPointCreator trackPointCreator, IdleObserver idleObserver, Handler handler) {
|
private boolean hasSensorBeenConnected = false;
|
||||||
|
private boolean isSensorDisconnectedWarningActive = false;
|
||||||
|
|
||||||
|
private boolean autoPauseOnCadence0;
|
||||||
|
private de.dennisguse.opentracks.data.models.Speed autoPauseCadence0Speed;
|
||||||
|
private boolean hasCadenceSensorBeenConnected = false;
|
||||||
|
private String autoPauseStatus = "N/A";
|
||||||
|
|
||||||
|
private java.time.Instant idleTimeoutScheduledAt;
|
||||||
|
private java.time.Instant walkingStartedAt;
|
||||||
|
private java.time.Instant idleStartedAt;
|
||||||
|
private boolean longBreakRecorded = false;
|
||||||
|
|
||||||
|
private boolean gpsOverriden = false;
|
||||||
|
|
||||||
|
TrackRecordingManager(Context context, TrackPointCreator trackPointCreator, IdleObserver idleObserver, SensorDisconnectObserver sensorDisconnectObserver, Handler handler) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.idleObserver = idleObserver;
|
this.idleObserver = idleObserver;
|
||||||
|
this.sensorDisconnectObserver = sensorDisconnectObserver;
|
||||||
this.trackPointCreator = trackPointCreator;
|
this.trackPointCreator = trackPointCreator;
|
||||||
this.handler = handler;
|
this.handler = handler;
|
||||||
contentProviderUtils = new ContentProviderUtils(context);
|
contentProviderUtils = new ContentProviderUtils(context);
|
||||||
@@ -97,6 +115,23 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
|||||||
|
|
||||||
onNewTrackPoint(segmentStartTrackPoint);
|
onNewTrackPoint(segmentStartTrackPoint);
|
||||||
|
|
||||||
|
handler.post(() -> {
|
||||||
|
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;
|
return trackId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +151,23 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
|||||||
|
|
||||||
reset();
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +177,12 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
|||||||
|
|
||||||
trackId = null;
|
trackId = null;
|
||||||
trackStatisticsUpdater = null;
|
trackStatisticsUpdater = null;
|
||||||
|
|
||||||
|
idleTimeoutScheduledAt = null;
|
||||||
|
walkingStartedAt = null;
|
||||||
|
idleStartedAt = null;
|
||||||
|
longBreakRecorded = false;
|
||||||
|
handler.removeCallbacks(ON_LONG_BREAK);
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
@@ -144,32 +202,115 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
|||||||
|
|
||||||
TrackBuilder trackBuilder = new TrackBuilder(track);
|
TrackBuilder trackBuilder = new TrackBuilder(track);
|
||||||
trackBuilder.setStatistics(tmpTrackStatisticsUpdater.getTrackStatistics());
|
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);
|
||||||
|
java.time.Duration markerDuration = de.dennisguse.opentracks.settings.PreferencesUtils.getMarkerIdleDuration();
|
||||||
|
if (longBreakRecorded || d.compareTo(markerDuration) >= 0) {
|
||||||
|
d = markerDuration;
|
||||||
|
}
|
||||||
|
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)
|
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||||
public synchronized void onIdle() {
|
public synchronized void onIdle() {
|
||||||
Log.d(TAG, "Becoming idle");
|
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();
|
||||||
|
longBreakRecorded = false;
|
||||||
|
java.time.Duration markerDuration = de.dennisguse.opentracks.settings.PreferencesUtils.getMarkerIdleDuration();
|
||||||
|
handler.removeCallbacks(ON_LONG_BREAK);
|
||||||
|
handler.postDelayed(ON_LONG_BREAK, markerDuration.toMillis());
|
||||||
|
}
|
||||||
|
autoPauseStatus = "IDLE";
|
||||||
|
|
||||||
|
onNewTrackPoint(idlePoint);
|
||||||
|
|
||||||
idleObserver.onIdle();
|
idleObserver.onIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private synchronized void onLongBreak() {
|
||||||
|
if ("IDLE".equals(autoPauseStatus) && idleStartedAt != null && !longBreakRecorded) {
|
||||||
|
longBreakRecorded = true;
|
||||||
|
TrackPoint trackPoint = trackPointCreator.createSegmentStartManual();
|
||||||
|
insertTrackPoint(trackPoint, true);
|
||||||
|
idleObserver.onLongBreak();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return TrackPoint was stored?
|
* @return TrackPoint was stored?
|
||||||
*/
|
*/
|
||||||
synchronized boolean onNewTrackPoint(@NonNull TrackPoint trackPoint) {
|
synchronized boolean onNewTrackPoint(@NonNull TrackPoint trackPoint) {
|
||||||
if (trackPoint.position().hasSpeed()) {
|
|
||||||
lastTrackPointUIWithSpeed = trackPoint;
|
|
||||||
}
|
|
||||||
if (trackPoint.position().hasAltitude()) {
|
if (trackPoint.position().hasAltitude()) {
|
||||||
lastTrackPointUIWithAltitude = trackPoint;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
boolean strictSensorMode = isSensorExpected && !sensorHasDistance && !gpsOverriden;
|
||||||
|
if (strictSensorMode) {
|
||||||
|
if (trackPoint.position().hasSpeed()) {
|
||||||
|
trackPoint = trackPoint.with(trackPoint.position().with(de.dennisguse.opentracks.data.models.Speed.ZERO));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackPoint.position().hasSpeed()) {
|
||||||
|
lastTrackPointUIWithSpeed = trackPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (trackPoint.type() == TrackPoint.Type.IDLE) {
|
if (trackPoint.type() == TrackPoint.Type.IDLE) {
|
||||||
|
longBreakRecorded = false;
|
||||||
insertTrackPoint(trackPoint, true);
|
insertTrackPoint(trackPoint, true);
|
||||||
handler.removeCallbacks(ON_IDLE);
|
handler.removeCallbacks(ON_IDLE);
|
||||||
|
|
||||||
|
long durationMs = PreferencesUtils.getMarkerIdleDuration().toMillis();
|
||||||
|
if (durationMs > 0) {
|
||||||
|
handler.postDelayed(ON_LONG_BREAK, durationMs);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
//Storing trackPoint
|
//Storing trackPoint
|
||||||
@@ -200,11 +341,37 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
|||||||
return true;
|
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().toMPS() > 0 && trackPoint.position().speed().lessThan(autoPauseCadence0Speed)) {
|
||||||
|
isWalkingWithBike = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isWalkingWithBike && "WALKING".equals(autoPauseStatus)) {
|
||||||
|
autoPauseStatus = "REC";
|
||||||
|
walkingStartedAt = null;
|
||||||
|
scheduleNewIdleTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
Distance distanceToLastStoredTrackPoint;
|
Distance distanceToLastStoredTrackPoint;
|
||||||
if (trackPoint.position().hasLocation() && !lastStoredTrackPoint.position().hasLocation()) {
|
if (isWalkingWithBike || strictSensorMode) {
|
||||||
distanceToLastStoredTrackPoint = trackPoint.distanceToPreviousFromLocation(lastStoredTrackPointWithLocation);
|
distanceToLastStoredTrackPoint = Distance.ZERO;
|
||||||
|
if (isWalkingWithBike) {
|
||||||
|
autoPauseStatus = "WALKING";
|
||||||
|
}
|
||||||
} else {
|
} 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)) {
|
if (distanceToLastStoredTrackPoint.greaterThan(maxRecordingDistance)) {
|
||||||
@@ -212,6 +379,20 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
|||||||
insertTrackPoint(trackPoint, true);
|
insertTrackPoint(trackPoint, true);
|
||||||
|
|
||||||
scheduleNewIdleTimeout();
|
scheduleNewIdleTimeout();
|
||||||
|
if (!isWalkingWithBike) {
|
||||||
|
if ("IDLE".equals(autoPauseStatus)) {
|
||||||
|
handler.removeCallbacks(ON_LONG_BREAK);
|
||||||
|
}
|
||||||
|
autoPauseStatus = "REC";
|
||||||
|
} else {
|
||||||
|
if (!"WALKING".equals(autoPauseStatus)) {
|
||||||
|
walkingStartedAt = java.time.Instant.now();
|
||||||
|
if ("IDLE".equals(autoPauseStatus)) {
|
||||||
|
handler.removeCallbacks(ON_LONG_BREAK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
autoPauseStatus = "WALKING";
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +400,20 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
|||||||
insertTrackPoint(trackPoint, false);
|
insertTrackPoint(trackPoint, false);
|
||||||
|
|
||||||
scheduleNewIdleTimeout();
|
scheduleNewIdleTimeout();
|
||||||
|
if (!isWalkingWithBike) {
|
||||||
|
if ("IDLE".equals(autoPauseStatus)) {
|
||||||
|
handler.removeCallbacks(ON_LONG_BREAK);
|
||||||
|
}
|
||||||
|
autoPauseStatus = "REC";
|
||||||
|
} else {
|
||||||
|
if (!"WALKING".equals(autoPauseStatus)) {
|
||||||
|
walkingStartedAt = java.time.Instant.now();
|
||||||
|
if ("IDLE".equals(autoPauseStatus)) {
|
||||||
|
handler.removeCallbacks(ON_LONG_BREAK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
autoPauseStatus = "WALKING";
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +430,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
|||||||
}
|
}
|
||||||
handler.removeCallbacks(ON_IDLE);
|
handler.removeCallbacks(ON_IDLE);
|
||||||
handler.postDelayed(ON_IDLE, idleDuration.toMillis());
|
handler.postDelayed(ON_IDLE, idleDuration.toMillis());
|
||||||
|
idleTimeoutScheduledAt = java.time.Instant.now().plus(idleDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
Statistics getTrackStatistics() {
|
Statistics getTrackStatistics() {
|
||||||
@@ -284,6 +480,13 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
|||||||
|
|
||||||
lastStoredTrackPoint = null;
|
lastStoredTrackPoint = null;
|
||||||
lastStoredTrackPointWithLocation = null;
|
lastStoredTrackPointWithLocation = null;
|
||||||
|
|
||||||
|
hasSensorBeenConnected = false;
|
||||||
|
isSensorDisconnectedWarningActive = false;
|
||||||
|
hasCadenceSensorBeenConnected = false;
|
||||||
|
if (sensorDisconnectObserver != null) {
|
||||||
|
sensorDisconnectObserver.onSensorDisconnectedWarning(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -297,13 +500,36 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
|||||||
if (PreferencesUtils.isKey(R.string.idle_duration_key, key)) {
|
if (PreferencesUtils.isKey(R.string.idle_duration_key, key)) {
|
||||||
idleDuration = PreferencesUtils.getIdleDurationTimeout();
|
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() {
|
public TrackPoint getLastStoredTrackPointWithLocation() {
|
||||||
return lastStoredTrackPointWithLocation;
|
return lastStoredTrackPointWithLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOverrideSensorWithGPS(boolean override) {
|
||||||
|
if (override) {
|
||||||
|
hasSensorBeenConnected = false;
|
||||||
|
gpsOverriden = true;
|
||||||
|
if (isSensorDisconnectedWarningActive) {
|
||||||
|
isSensorDisconnectedWarningActive = false;
|
||||||
|
sensorDisconnectObserver.onSensorDisconnectedWarning(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public interface IdleObserver {
|
public interface IdleObserver {
|
||||||
void onIdle();
|
void onIdle();
|
||||||
|
void onLongBreak();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface SensorDisconnectObserver {
|
||||||
|
void onSensorDisconnectedWarning(boolean show);
|
||||||
|
void onSensorConnected();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import de.dennisguse.opentracks.services.announcement.VoiceAnnouncementManager;
|
|||||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
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();
|
private static final String TAG = TrackRecordingService.class.getSimpleName();
|
||||||
|
|
||||||
@@ -84,6 +84,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
|||||||
private RecordingStatus recordingStatus;
|
private RecordingStatus recordingStatus;
|
||||||
private MutableLiveData<RecordingStatus> recordingStatusObservable;
|
private MutableLiveData<RecordingStatus> recordingStatusObservable;
|
||||||
private MutableLiveData<GpsStatusValue> gpsStatusObservable;
|
private MutableLiveData<GpsStatusValue> gpsStatusObservable;
|
||||||
|
private MutableLiveData<Boolean> sensorDisconnectedWarningObservable;
|
||||||
private MutableLiveData<RecordingData> recordingDataObservable;
|
private MutableLiveData<RecordingData> recordingDataObservable;
|
||||||
|
|
||||||
private Handler handler;
|
private Handler handler;
|
||||||
@@ -104,10 +105,11 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
|||||||
recordingStatusObservable = new MutableLiveData<>();
|
recordingStatusObservable = new MutableLiveData<>();
|
||||||
updateRecordingStatus(STATUS_DEFAULT);
|
updateRecordingStatus(STATUS_DEFAULT);
|
||||||
gpsStatusObservable = new MutableLiveData<>(STATUS_GPS_DEFAULT);
|
gpsStatusObservable = new MutableLiveData<>(STATUS_GPS_DEFAULT);
|
||||||
|
sensorDisconnectedWarningObservable = new MutableLiveData<>(false);
|
||||||
recordingDataObservable = new MutableLiveData<>(RecordingData.NOT_RECORDING);
|
recordingDataObservable = new MutableLiveData<>(RecordingData.NOT_RECORDING);
|
||||||
|
|
||||||
trackPointCreator = new TrackPointCreator(this);
|
trackPointCreator = new TrackPointCreator(this);
|
||||||
trackRecordingManager = new TrackRecordingManager(this, trackPointCreator, this, handler);
|
trackRecordingManager = new TrackRecordingManager(this, trackPointCreator, this, this, handler);
|
||||||
|
|
||||||
voiceAnnouncementManager = new VoiceAnnouncementManager(this);
|
voiceAnnouncementManager = new VoiceAnnouncementManager(this);
|
||||||
notificationManager = new TrackRecordingServiceNotificationManager(this);
|
notificationManager = new TrackRecordingServiceNotificationManager(this);
|
||||||
@@ -138,6 +140,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
|||||||
|
|
||||||
recordingStatusObservable = null;
|
recordingStatusObservable = null;
|
||||||
gpsStatusObservable = null;
|
gpsStatusObservable = null;
|
||||||
|
sensorDisconnectedWarningObservable = null;
|
||||||
recordingDataObservable = null;
|
recordingDataObservable = null;
|
||||||
|
|
||||||
Log.d(TAG, "Destroyed");
|
Log.d(TAG, "Destroyed");
|
||||||
@@ -315,6 +318,10 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
|||||||
return gpsStatusObservable;
|
return gpsStatusObservable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LiveData<Boolean> getSensorDisconnectedWarningObservable() {
|
||||||
|
return sensorDisconnectedWarningObservable;
|
||||||
|
}
|
||||||
|
|
||||||
public LiveData<RecordingData> getRecordingDataObservable() {
|
public LiveData<RecordingData> getRecordingDataObservable() {
|
||||||
return recordingDataObservable;
|
return recordingDataObservable;
|
||||||
}
|
}
|
||||||
@@ -337,6 +344,32 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
|||||||
voiceAnnouncementManager.announceIdle();
|
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
|
@VisibleForTesting
|
||||||
public void stopUpdateRecordingData() {
|
public void stopUpdateRecordingData() {
|
||||||
handler.removeCallbacks(updateRecordingData);
|
handler.removeCallbacks(updateRecordingData);
|
||||||
|
|||||||
+24
@@ -119,6 +119,30 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
|
|||||||
voiceAnnouncement.speak(VoiceAnnouncementUtils.createIdle(context));
|
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) {
|
public void announceStatisticsIfNeeded(@NonNull RecordingData data) {
|
||||||
if (shouldNotAnnounce()) {
|
if (shouldNotAnnounce()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+15
@@ -50,6 +50,21 @@ class VoiceAnnouncementUtils {
|
|||||||
.append(context.getString(R.string.voiceIdle));
|
.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) {
|
static Spannable createStatistics(Context context, Track track, SensorDataSet sensorDataSet, UnitSystem unitSystem, boolean isReportSpeed, @Nullable Statistics currentInterval, @Nullable SensorStatistics sensorStatistics) {
|
||||||
Statistics trackStatistics = track.statistics();
|
Statistics trackStatistics = track.statistics();
|
||||||
|
|
||||||
|
|||||||
@@ -101,8 +101,11 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
|
|||||||
position = position.withCoordinates(lastStoredTrackPointWithLocation.position());
|
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());
|
position = position.with(lastTrackPointUISpeed.position().speed());
|
||||||
|
}
|
||||||
|
|
||||||
if (lastTrackPointUIAltitude != null)
|
if (lastTrackPointUIAltitude != null)
|
||||||
position = position.with(lastTrackPointUIAltitude.position().altitude());
|
position = position.with(lastTrackPointUIAltitude.position().altitude());
|
||||||
|
|||||||
@@ -38,11 +38,13 @@ import java.util.Arrays;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.data.models.ActivityType;
|
import de.dennisguse.opentracks.data.models.ActivityType;
|
||||||
import de.dennisguse.opentracks.data.models.Distance;
|
import de.dennisguse.opentracks.data.models.Distance;
|
||||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
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.HeartRate;
|
||||||
import de.dennisguse.opentracks.data.models.HeartRateZones;
|
import de.dennisguse.opentracks.data.models.HeartRateZones;
|
||||||
import de.dennisguse.opentracks.data.models.Track;
|
import de.dennisguse.opentracks.data.models.Track;
|
||||||
@@ -249,6 +251,10 @@ public class PreferencesUtils {
|
|||||||
setString(R.string.stats_units_key, unitSystem.getPreferenceId());
|
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()
|
//TODO Check if actually needed or can be superseeded by a flexible default in getUnit()
|
||||||
public static void applyDefaultUnit() {
|
public static void applyDefaultUnit() {
|
||||||
if (getString(R.string.stats_units_key, "").isEmpty()) {
|
if (getString(R.string.stats_units_key, "").isEmpty()) {
|
||||||
@@ -342,6 +348,20 @@ public class PreferencesUtils {
|
|||||||
return getBoolean(R.string.voice_announce_idle_key, true);
|
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() {
|
public static boolean shouldVoiceAnnounceTime() {
|
||||||
return getBoolean(R.string.voice_announce_time_key, true);
|
return getBoolean(R.string.voice_announce_time_key, true);
|
||||||
}
|
}
|
||||||
@@ -682,6 +702,51 @@ public class PreferencesUtils {
|
|||||||
return entries;
|
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() {
|
public static boolean shouldInstantExportToFile() {
|
||||||
final boolean INSTANT_POST_WORKOUT_EXPORT_DEFAULT = resources.getBoolean(R.bool.post_workout_export_enabled_default);
|
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();
|
return getBoolean(R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT) && isDefaultExportDirectoryUri();
|
||||||
@@ -839,7 +904,15 @@ 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_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_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_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),
|
||||||
|
new DataField(resources.getString(R.string.stats_custom_layout_media_key), false, false, true)
|
||||||
);
|
);
|
||||||
|
|
||||||
RecordingLayout defaultLayout = new RecordingLayout(resources.getString(R.string.stats_custom_layout_default_layout), 2);
|
RecordingLayout defaultLayout = new RecordingLayout(resources.getString(R.string.stats_custom_layout_default_layout), 2);
|
||||||
@@ -855,7 +928,7 @@ public class PreferencesUtils {
|
|||||||
String selectedLayout = getSelectedLayoutName();
|
String selectedLayout = getSelectedLayoutName();
|
||||||
String csvCustomLayouts = getString(R.string.stats_custom_layouts_key, buildDefaultLayout());
|
String csvCustomLayouts = getString(R.string.stats_custom_layouts_key, buildDefaultLayout());
|
||||||
|
|
||||||
List<RecordingLayout> recordingLayouts = RecordingLayoutIO.fromCsv(csvCustomLayouts, resources);
|
List<RecordingLayout> recordingLayouts = getAllCustomLayouts();
|
||||||
|
|
||||||
RecordingLayout fallback = !recordingLayouts.isEmpty() ? recordingLayouts.get(0) : getDefaultLayout();
|
RecordingLayout fallback = !recordingLayouts.isEmpty() ? recordingLayouts.get(0) : getDefaultLayout();
|
||||||
|
|
||||||
@@ -901,7 +974,18 @@ public class PreferencesUtils {
|
|||||||
public static List<RecordingLayout> getAllCustomLayouts() {
|
public static List<RecordingLayout> getAllCustomLayouts() {
|
||||||
String csvCustomLayouts = getString(R.string.stats_custom_layouts_key, buildDefaultLayout());
|
String csvCustomLayouts = getString(R.string.stats_custom_layouts_key, buildDefaultLayout());
|
||||||
|
|
||||||
return RecordingLayoutIO.fromCsv(csvCustomLayouts, resources);
|
List<RecordingLayout> layouts = RecordingLayoutIO.fromCsv(csvCustomLayouts, resources);
|
||||||
|
List<DataField> 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() {
|
public static void resetCustomLayoutPreferences() {
|
||||||
|
|||||||
@@ -57,5 +57,15 @@ public class RecordingSettingsFragment extends PreferenceFragmentCompat {
|
|||||||
|
|
||||||
ListPreference idleDuration = findPreference(getString(R.string.idle_duration_key));
|
ListPreference idleDuration = findPreference(getString(R.string.idle_duration_key));
|
||||||
idleDuration.setEntries(PreferencesUtils.getIdleDurationEntries());
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-2
@@ -40,6 +40,13 @@ public class RecordingLayoutIO {
|
|||||||
Log.e(TAG, "Invalid CSV layout. It shouldn't happen: " + csvLine);
|
Log.e(TAG, "Invalid CSV layout. It shouldn't happen: " + csvLine);
|
||||||
return recordingLayout;
|
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));
|
recordingLayout.addField(fromCSV(fieldParts, resources));
|
||||||
}
|
}
|
||||||
return recordingLayout;
|
return recordingLayout;
|
||||||
@@ -50,11 +57,24 @@ public class RecordingLayoutIO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static DataField fromCSV(String[] fieldParts, @NonNull Resources resources) {
|
private static DataField fromCSV(String[] fieldParts, @NonNull Resources resources) {
|
||||||
|
boolean isWide;
|
||||||
|
String key = fieldParts[0];
|
||||||
|
|
||||||
|
// Force isWide to true for specific keys that must always be wide
|
||||||
|
if (key.equals(resources.getString(R.string.stats_custom_layout_coordinates_key)) ||
|
||||||
|
key.equals(resources.getString(R.string.stats_custom_layout_media_key))) {
|
||||||
|
isWide = true;
|
||||||
|
} else if (fieldParts.length > 3) {
|
||||||
|
isWide = YES_VALUE.equals(fieldParts[3]);
|
||||||
|
} else {
|
||||||
|
isWide = false;
|
||||||
|
}
|
||||||
|
|
||||||
return new DataField(
|
return new DataField(
|
||||||
fieldParts[0],
|
key,
|
||||||
YES_VALUE.equals(fieldParts[1]),
|
YES_VALUE.equals(fieldParts[1]),
|
||||||
YES_VALUE.equals(fieldParts[2]),
|
YES_VALUE.equals(fieldParts[2]),
|
||||||
fieldParts[0].equals(resources.getString(R.string.stats_custom_layout_coordinates_key)));
|
isWide);
|
||||||
}
|
}
|
||||||
|
|
||||||
static String toCsv(DataField datafield) {
|
static String toCsv(DataField datafield) {
|
||||||
|
|||||||
+1
-1
@@ -77,7 +77,7 @@ public class SettingsCustomLayoutEditAdapter extends RecyclerView.Adapter<Recycl
|
|||||||
Speed.ZERO
|
Speed.ZERO
|
||||||
);
|
);
|
||||||
|
|
||||||
demoData = new RecordingData(track, new TrackPoint(TrackPoint.Type.TRACKPOINT, position), null, statistics);
|
demoData = new RecordingData(track, new TrackPoint(TrackPoint.Type.TRACKPOINT, position), null, statistics, "", "", "", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
private RecordingLayout recordingLayout;
|
private RecordingLayout recordingLayout;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.util.Pair;
|
|||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
|
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||||
import de.dennisguse.opentracks.data.models.Speed;
|
import de.dennisguse.opentracks.data.models.Speed;
|
||||||
import de.dennisguse.opentracks.data.models.SpeedFormatter;
|
import de.dennisguse.opentracks.data.models.SpeedFormatter;
|
||||||
@@ -288,4 +289,86 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
|||||||
getBinding().statsDescriptionMain.setText(R.string.stats_coordinates);
|
getBinding().statsDescriptionMain.setText(R.string.stats_coordinates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class AutoPauseStatus extends GenericStatisticsViewHolder {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||||
|
String value = data.autoPauseStatus();
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
value = "N/A";
|
||||||
|
}
|
||||||
|
getBinding().statsValue.setText(value);
|
||||||
|
getBinding().statsUnit.setText("");
|
||||||
|
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_auto_pause_status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SensorStatusHR extends GenericStatisticsViewHolder {
|
||||||
|
@Override
|
||||||
|
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||||
|
String status = getContext().getString(R.string.value_unknown);
|
||||||
|
if (data.sensorDataSet() != null && data.sensorDataSet().heartRate() != null) {
|
||||||
|
BatteryLevel battery = data.sensorDataSet().heartRateBattery();
|
||||||
|
status = battery != null ? battery.percentage() + "%" : "OK";
|
||||||
|
}
|
||||||
|
getBinding().statsValue.setText(status);
|
||||||
|
getBinding().statsUnit.setText("");
|
||||||
|
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_sensor_status_hr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SensorStatusCadence extends GenericStatisticsViewHolder {
|
||||||
|
@Override
|
||||||
|
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||||
|
String status = getContext().getString(R.string.value_unknown);
|
||||||
|
if (data.sensorDataSet() != null && data.sensorDataSet().cadence() != null) {
|
||||||
|
BatteryLevel battery = data.sensorDataSet().cadenceBattery();
|
||||||
|
status = battery != null ? battery.percentage() + "%" : "OK";
|
||||||
|
}
|
||||||
|
getBinding().statsValue.setText(status);
|
||||||
|
getBinding().statsUnit.setText("");
|
||||||
|
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_sensor_status_cadence));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SensorStatusSpeed extends GenericStatisticsViewHolder {
|
||||||
|
@Override
|
||||||
|
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||||
|
String status = getContext().getString(R.string.value_unknown);
|
||||||
|
if (data.sensorDataSet() != null && data.sensorDataSet().speed() != null) {
|
||||||
|
BatteryLevel battery = data.sensorDataSet().speedBattery();
|
||||||
|
status = battery != null ? battery.percentage() + "%" : "OK";
|
||||||
|
}
|
||||||
|
getBinding().statsValue.setText(status);
|
||||||
|
getBinding().statsUnit.setText("");
|
||||||
|
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_sensor_status_speed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static class DebugIdleTimer extends GenericStatisticsViewHolder {
|
||||||
|
@Override
|
||||||
|
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||||
|
getBinding().statsValue.setText(data.idleTimer().isEmpty() ? "---" : data.idleTimer());
|
||||||
|
getBinding().statsUnit.setText("");
|
||||||
|
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_debug_idle_timer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DebugWalkingTimer extends GenericStatisticsViewHolder {
|
||||||
|
@Override
|
||||||
|
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||||
|
getBinding().statsValue.setText(data.walkingTimer().isEmpty() ? "---" : data.walkingTimer());
|
||||||
|
getBinding().statsUnit.setText("");
|
||||||
|
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_debug_walking_timer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DebugMarkerTimer extends GenericStatisticsViewHolder {
|
||||||
|
@Override
|
||||||
|
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||||
|
getBinding().statsValue.setText(data.markerTimer().isEmpty() ? "---" : data.markerTimer());
|
||||||
|
getBinding().statsUnit.setText("");
|
||||||
|
getBinding().statsDescriptionMain.setText(getContext().getString(R.string.stats_debug_marker_timer));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ public class Mapping {
|
|||||||
m.put(context.getString(R.string.stats_custom_layout_cadence_key), SensorStatisticsViewHolder.SensorCadence::new);
|
m.put(context.getString(R.string.stats_custom_layout_cadence_key), SensorStatisticsViewHolder.SensorCadence::new);
|
||||||
m.put(context.getString(R.string.stats_custom_layout_power_key), SensorStatisticsViewHolder.SensorPower::new);
|
m.put(context.getString(R.string.stats_custom_layout_power_key), SensorStatisticsViewHolder.SensorPower::new);
|
||||||
m.put(context.getString(R.string.stats_custom_layout_clock_key), ClockViewHolder::new);
|
m.put(context.getString(R.string.stats_custom_layout_clock_key), ClockViewHolder::new);
|
||||||
|
m.put(context.getString(R.string.stats_custom_layout_auto_pause_status_key), GenericStatisticsViewHolder.AutoPauseStatus::new);
|
||||||
|
m.put(context.getString(R.string.stats_custom_layout_sensor_status_hr_key), GenericStatisticsViewHolder.SensorStatusHR::new);
|
||||||
|
m.put(context.getString(R.string.stats_custom_layout_sensor_status_cadence_key), GenericStatisticsViewHolder.SensorStatusCadence::new);
|
||||||
|
m.put(context.getString(R.string.stats_custom_layout_sensor_status_speed_key), GenericStatisticsViewHolder.SensorStatusSpeed::new);
|
||||||
|
m.put(context.getString(R.string.stats_custom_layout_debug_idle_timer_key), GenericStatisticsViewHolder.DebugIdleTimer::new);
|
||||||
|
m.put(context.getString(R.string.stats_custom_layout_debug_walking_timer_key), GenericStatisticsViewHolder.DebugWalkingTimer::new);
|
||||||
|
m.put(context.getString(R.string.stats_custom_layout_debug_marker_timer_key), GenericStatisticsViewHolder.DebugMarkerTimer::new);
|
||||||
|
m.put(context.getString(R.string.stats_custom_layout_media_key), MediaViewHolder::new);
|
||||||
|
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package de.dennisguse.opentracks.viewmodels;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.provider.Settings;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.R;
|
||||||
|
import de.dennisguse.opentracks.databinding.StatsMediaItemBinding;
|
||||||
|
import de.dennisguse.opentracks.services.MediaNotificationListenerService;
|
||||||
|
import de.dennisguse.opentracks.services.RecordingData;
|
||||||
|
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||||
|
import de.dennisguse.opentracks.ui.customRecordingLayout.DataField;
|
||||||
|
|
||||||
|
public class MediaViewHolder extends StatisticViewHolder<StatsMediaItemBinding> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StatsMediaItemBinding createViewBinding(LayoutInflater inflater) {
|
||||||
|
return StatsMediaItemBinding.inflate(inflater);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureUI(DataField dataField) {
|
||||||
|
getBinding().statsMediaPrev.setOnClickListener(v -> {
|
||||||
|
MediaNotificationListenerService service = MediaNotificationListenerService.getInstance();
|
||||||
|
if (service != null) {
|
||||||
|
service.sendMediaCommand(KeyEvent.KEYCODE_MEDIA_PREVIOUS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
getBinding().statsMediaNext.setOnClickListener(v -> {
|
||||||
|
MediaNotificationListenerService service = MediaNotificationListenerService.getInstance();
|
||||||
|
if (service != null) {
|
||||||
|
service.sendMediaCommand(KeyEvent.KEYCODE_MEDIA_NEXT);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
getBinding().statsMediaPlayPause.setOnClickListener(v -> {
|
||||||
|
MediaNotificationListenerService service = MediaNotificationListenerService.getInstance();
|
||||||
|
if (service != null) {
|
||||||
|
service.sendMediaCommand(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
getBinding().statsMediaPermissionButton.setOnClickListener(v -> {
|
||||||
|
Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
getContext().startActivity(intent);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (getContext() instanceof LifecycleOwner) {
|
||||||
|
MediaNotificationListenerService.getMediaDataObservable().observe((LifecycleOwner) getContext(), mediaData -> {
|
||||||
|
if (mediaData.isPermissionGranted) {
|
||||||
|
getBinding().statsMediaPermissionButton.setVisibility(View.GONE);
|
||||||
|
getBinding().statsMediaControls.setVisibility(View.VISIBLE);
|
||||||
|
getBinding().statsMediaTitle.setVisibility(View.VISIBLE);
|
||||||
|
getBinding().statsMediaArtist.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
getBinding().statsMediaTitle.setText(mediaData.title != null ? mediaData.title : getContext().getString(R.string.media_title_default));
|
||||||
|
getBinding().statsMediaArtist.setText(mediaData.artist != null ? mediaData.artist : getContext().getString(R.string.media_artist_default));
|
||||||
|
|
||||||
|
if (mediaData.isPlaying) {
|
||||||
|
getBinding().statsMediaPlayPause.setImageResource(R.drawable.ic_baseline_pause_24); // Needs to exist, or fallback to something else
|
||||||
|
} else {
|
||||||
|
getBinding().statsMediaPlayPause.setImageResource(R.drawable.ic_baseline_play_arrow_24);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getBinding().statsMediaPermissionButton.setVisibility(View.VISIBLE);
|
||||||
|
getBinding().statsMediaControls.setVisibility(View.GONE);
|
||||||
|
getBinding().statsMediaTitle.setVisibility(View.GONE);
|
||||||
|
getBinding().statsMediaArtist.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||||
|
// Not used, as we observe LiveData directly
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M6,19h4V5H6v14zm8,-14v14h4V5h-4z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M8,5v14l11,-7z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M6,6h2v12H6zm3.5,6l8.5,6V6z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/stats_media_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="@string/media_title_default"
|
||||||
|
android:textAppearance="@style/TextAppearance.OpenTracks.PrimaryValue"
|
||||||
|
android:textSize="20sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/stats_media_artist"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="@string/media_artist_default"
|
||||||
|
android:textAppearance="@style/TextAppearance.OpenTracks.SecondaryHeader"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/stats_media_title" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/stats_media_controls"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/stats_media_artist"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/stats_media_prev"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/media_prev"
|
||||||
|
android:src="@drawable/ic_baseline_skip_previous_24"
|
||||||
|
app:tint="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/stats_media_play_pause"
|
||||||
|
android:layout_width="64dp"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/media_play_pause"
|
||||||
|
android:src="@drawable/ic_baseline_play_arrow_24"
|
||||||
|
app:tint="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/stats_media_next"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/media_next"
|
||||||
|
android:src="@drawable/ic_baseline_skip_next_24"
|
||||||
|
app:tint="?attr/colorOnSurface" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/stats_media_permission_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/media_permission_request"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -197,6 +197,7 @@ limitations under the License.
|
|||||||
<string name="settings_recording_max_recording_distance_summary">Nowy segment jest tworzony, jeśli odległość między dwiema lokalizacjami jest większa niż %1$s</string>
|
<string name="settings_recording_max_recording_distance_summary">Nowy segment jest tworzony, jeśli odległość między dwiema lokalizacjami jest większa niż %1$s</string>
|
||||||
<string name="settings_recording_max_recording_distance_title">Maksymalna odległość</string>
|
<string name="settings_recording_max_recording_distance_title">Maksymalna odległość</string>
|
||||||
<string name="settings_recording_min_recording_distance_title">Rejestrowanie odcinka</string>
|
<string name="settings_recording_min_recording_distance_title">Rejestrowanie odcinka</string>
|
||||||
|
<string name="settings_recording_marker_idle_duration_title">Minimalny czas długiej przerwy</string>
|
||||||
<string name="settings_recording_min_recording_interval_title">Rejestrowanie interwału</string>
|
<string name="settings_recording_min_recording_interval_title">Rejestrowanie interwału</string>
|
||||||
<string name="settings_recording_min_required_accuracy_title">Dokładność GPS-u</string>
|
<string name="settings_recording_min_required_accuracy_title">Dokładność GPS-u</string>
|
||||||
<string name="settings_recording_track_name_date_iso_8601_option">Data (ISO 8601)</string>
|
<string name="settings_recording_track_name_date_iso_8601_option">Data (ISO 8601)</string>
|
||||||
@@ -281,6 +282,7 @@ limitations under the License.
|
|||||||
<string name="value_integer_meter_recommended">%1$d m (zalecane)</string>
|
<string name="value_integer_meter_recommended">%1$d m (zalecane)</string>
|
||||||
<string name="value_integer_mile_hour">%1$d mil/h</string>
|
<string name="value_integer_mile_hour">%1$d mil/h</string>
|
||||||
<string name="value_integer_minute">%1$d min</string>
|
<string name="value_integer_minute">%1$d min</string>
|
||||||
|
<string name="value_integer_minute_recommended">%1$d min (zalecane)</string>
|
||||||
<string name="value_integer_second">%1$d s</string>
|
<string name="value_integer_second">%1$d s</string>
|
||||||
<string name="value_none">Brak</string>
|
<string name="value_none">Brak</string>
|
||||||
<string name="value_off">Wyłącz</string>
|
<string name="value_off">Wyłącz</string>
|
||||||
@@ -443,6 +445,8 @@ limitations under the License.
|
|||||||
<string name="settings_announcements_totaltime_frequency">Przedział czasu</string>
|
<string name="settings_announcements_totaltime_frequency">Przedział czasu</string>
|
||||||
<string name="stats_sensors_heart_rate">Tętno</string>
|
<string name="stats_sensors_heart_rate">Tętno</string>
|
||||||
<string name="stats_clock">Zegar</string>
|
<string name="stats_clock">Zegar</string>
|
||||||
|
<string name="stats_auto_pause_status">Status auto-pauzy</string>
|
||||||
|
<string name="stats_sensor_status">Status czujników</string>
|
||||||
<string name="settings_announcements_distance_frequency">Długość okrążenia</string>
|
<string name="settings_announcements_distance_frequency">Długość okrążenia</string>
|
||||||
<string name="settings_layout_reset_confirm_title">Zresetować twoje układy\?</string>
|
<string name="settings_layout_reset_confirm_title">Zresetować twoje układy\?</string>
|
||||||
<string name="settings_cycling_sensor">Kolarstwo</string>
|
<string name="settings_cycling_sensor">Kolarstwo</string>
|
||||||
@@ -523,6 +527,10 @@ limitations under the License.
|
|||||||
<string name="settings_announcements_lap_speed_pace">Tempo/prędkość okrążenia</string>
|
<string name="settings_announcements_lap_speed_pace">Tempo/prędkość okrążenia</string>
|
||||||
<string name="track_delete_number_of_tracks">%1$d ścieżki</string>
|
<string name="track_delete_number_of_tracks">%1$d ścieżki</string>
|
||||||
<string name="settings_announcements_idle_title">Przerwa</string>
|
<string name="settings_announcements_idle_title">Przerwa</string>
|
||||||
|
<string name="settings_announcements_long_break_title">Długa przerwa (filiżanka)</string>
|
||||||
|
<string name="settings_announcements_sensor_category_title">Czujniki</string>
|
||||||
|
<string name="settings_announcements_sensor_disconnected_title">Odłączono czujnik</string>
|
||||||
|
<string name="settings_announcements_sensor_connected_title">Podłączono czujnik</string>
|
||||||
<string name="settings_recording_title">Nagrywanie</string>
|
<string name="settings_recording_title">Nagrywanie</string>
|
||||||
<string name="settings_theme_switch_restart">Zmiana motywu może wymagać ręcznego restartu.</string>
|
<string name="settings_theme_switch_restart">Zmiana motywu może wymagać ręcznego restartu.</string>
|
||||||
<string name="settings_ui_dynamic_colors_summary">Użyj ręcznie zdefiniowanych Dynamicznych Kolorów (tylko Android 12+; wymaga ręcznego restartu)</string>
|
<string name="settings_ui_dynamic_colors_summary">Użyj ręcznie zdefiniowanych Dynamicznych Kolorów (tylko Android 12+; wymaga ręcznego restartu)</string>
|
||||||
@@ -585,4 +593,15 @@ limitations under the License.
|
|||||||
<string name="show_on_dashboard">API panelu OpenTracks</string>
|
<string name="show_on_dashboard">API panelu OpenTracks</string>
|
||||||
<string name="marker_detail_title">%1$d z %2$d: %3$s</string>
|
<string name="marker_detail_title">%1$d z %2$d: %3$s</string>
|
||||||
<string name="voice_on_device_speaker_title">Użyj wbudowanego głośnika</string>
|
<string name="voice_on_device_speaker_title">Użyj wbudowanego głośnika</string>
|
||||||
|
<string name="sensor_disconnected_warning">Czujnik rozłączony. Kontynuować z GPS?</string>
|
||||||
|
<string name="sensor_disconnected_switch_action">Przełącz</string>
|
||||||
|
<string name="settings_recording_auto_pause_on_cadence_0_title">Zatrzymaj przy prowadzeniu roweru</string>
|
||||||
|
<string name="settings_recording_auto_pause_on_cadence_0_summary">Uruchom autopauzę jeżeli kadencja wynosi 0 a prędkość wskazuje na chód</string>
|
||||||
|
<string name="settings_recording_auto_pause_on_cadence_0_speed_title">Maksymalna prędkość prowadzenia</string>
|
||||||
|
<string name="settings_recording_auto_pause_on_cadence_0_speed_summary">Prędkość poniżej której wykrywane jest prowadzenie roweru (gdy kadencja to 0)</string>
|
||||||
|
<string name="settings_recording_prevent_idle_if_cadence_title">Zapobiegaj pauzie podczas pedałowania</string>
|
||||||
|
<string name="settings_recording_prevent_idle_if_cadence_summary">Nie włączaj trybu postoju jeżeli pedałujesz, nawet przy braku ruchu</string>
|
||||||
|
<string name="voiceLongBreak">Rozpoczęto długą przerwę.</string>
|
||||||
|
<string name="voiceSensorDisconnected">Odłączono czujnik.</string>
|
||||||
|
<string name="voiceSensorConnected">Podłączono czujnik.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -128,6 +128,14 @@
|
|||||||
<string name="idle_duration_key" translatable="false">idleSpeedDuration</string>
|
<string name="idle_duration_key" translatable="false">idleSpeedDuration</string>
|
||||||
<string name="idle_duration_default" translatable="false">10</string>
|
<string name="idle_duration_default" translatable="false">10</string>
|
||||||
<string name="idle_duration_off" translatable="false">0</string>
|
<string name="idle_duration_off" translatable="false">0</string>
|
||||||
|
<string-array name="auto_pause_cadence_0_speed_values">
|
||||||
|
<item>4</item>
|
||||||
|
<item>6</item>
|
||||||
|
<item>8</item>
|
||||||
|
<item>10</item>
|
||||||
|
<item>12</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string-array name="idle_duration_values">
|
<string-array name="idle_duration_values">
|
||||||
<item>@string/idle_duration_off</item>
|
<item>@string/idle_duration_off</item>
|
||||||
<item>5</item>
|
<item>5</item>
|
||||||
@@ -139,6 +147,17 @@
|
|||||||
<item>120</item>
|
<item>120</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string name="marker_idle_duration_default" translatable="false">3</string>
|
||||||
|
<string-array name="marker_idle_duration_values">
|
||||||
|
<item>1</item>
|
||||||
|
<item>2</item>
|
||||||
|
<item>@string/marker_idle_duration_default</item>
|
||||||
|
<item>5</item>
|
||||||
|
<item>10</item>
|
||||||
|
<item>15</item>
|
||||||
|
<item>30</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string name="stats_rate_key" translatable="false">statsRate</string>
|
<string name="stats_rate_key" translatable="false">statsRate</string>
|
||||||
<string name="stats_rate_default" translatable="false">@string/stats_rate_speed_or_pace_default</string>
|
<string name="stats_rate_default" translatable="false">@string/stats_rate_speed_or_pace_default</string>
|
||||||
<string-array name="stats_rate_metric_options">
|
<string-array name="stats_rate_metric_options">
|
||||||
@@ -243,6 +262,12 @@
|
|||||||
<bool name="voice_on_device_speaker_default" translatable="false">true</bool>
|
<bool name="voice_on_device_speaker_default" translatable="false">true</bool>
|
||||||
<string name="voice_announce_idle_key" translatable="false">voice_announce_idle_key</string>
|
<string name="voice_announce_idle_key" translatable="false">voice_announce_idle_key</string>
|
||||||
<bool name="voice_announce_idle_default" translatable="false">false</bool>
|
<bool name="voice_announce_idle_default" translatable="false">false</bool>
|
||||||
|
<string name="voice_announce_long_break_key" translatable="false">voice_announce_long_break_key</string>
|
||||||
|
<bool name="voice_announce_long_break_default" translatable="false">true</bool>
|
||||||
|
<string name="voice_announce_sensor_disconnected_key" translatable="false">voice_announce_sensor_disconnected_key</string>
|
||||||
|
<bool name="voice_announce_sensor_disconnected_default" translatable="false">true</bool>
|
||||||
|
<string name="voice_announce_sensor_connected_key" translatable="false">voice_announce_sensor_connected_key</string>
|
||||||
|
<bool name="voice_announce_sensor_connected_default" translatable="false">true</bool>
|
||||||
|
|
||||||
<string name="voice_announce_total_distance_key" translatable="false">voiceAnnounceTotalDistance</string>
|
<string name="voice_announce_total_distance_key" translatable="false">voiceAnnounceTotalDistance</string>
|
||||||
<bool name="voice_announce_total_distance_default" translatable="false">true</bool>
|
<bool name="voice_announce_total_distance_default" translatable="false">true</bool>
|
||||||
@@ -379,6 +404,16 @@
|
|||||||
<string name="stats_custom_layout_average_cadence_key" translatable="false">average_cadence</string>
|
<string name="stats_custom_layout_average_cadence_key" translatable="false">average_cadence</string>
|
||||||
<string name="stats_custom_layout_power_key" translatable="false">power</string>
|
<string name="stats_custom_layout_power_key" translatable="false">power</string>
|
||||||
<string name="stats_custom_layout_clock_key" translatable="false">time</string>
|
<string name="stats_custom_layout_clock_key" translatable="false">time</string>
|
||||||
|
<string name="stats_custom_layout_auto_pause_status_key" translatable="false">auto_pause_status</string>
|
||||||
|
<string name="stats_custom_layout_sensor_status_key" translatable="false">sensor_status</string>
|
||||||
|
<string name="stats_custom_layout_sensor_status_hr_key" translatable="false">sensor_status_hr</string>
|
||||||
|
<string name="stats_custom_layout_sensor_status_cadence_key" translatable="false">sensor_status_cadence</string>
|
||||||
|
<string name="stats_custom_layout_sensor_status_speed_key" translatable="false">sensor_status_speed</string>
|
||||||
|
<string name="stats_custom_layout_debug_idle_timer_key" translatable="false">debug_idle_timer</string>
|
||||||
|
<string name="stats_custom_layout_debug_walking_timer_key" translatable="false">debug_walking_timer</string>
|
||||||
|
<string name="stats_custom_layout_debug_marker_timer_key" translatable="false">debug_marker_timer</string>
|
||||||
|
|
||||||
|
<string name="stats_custom_layout_media_key" translatable="false">media_control</string>
|
||||||
|
|
||||||
<!-- Stats empty values -->
|
<!-- Stats empty values -->
|
||||||
<string name="stats_empty_value_integer" translatable="false">0</string>
|
<string name="stats_empty_value_integer" translatable="false">0</string>
|
||||||
|
|||||||
@@ -331,6 +331,10 @@ limitations under the License.
|
|||||||
<string name="settings_sensors_summary">Cycling Sensors, Wheel Circumference, Running Sensors</string>
|
<string name="settings_sensors_summary">Cycling Sensors, Wheel Circumference, Running Sensors</string>
|
||||||
<string name="settings_announcements_idle_category_title">Idle Announcements</string>
|
<string name="settings_announcements_idle_category_title">Idle Announcements</string>
|
||||||
<string name="settings_announcements_idle_title">Idle</string>
|
<string name="settings_announcements_idle_title">Idle</string>
|
||||||
|
<string name="settings_announcements_long_break_title">Long break</string>
|
||||||
|
<string name="settings_announcements_sensor_category_title">Sensors</string>
|
||||||
|
<string name="settings_announcements_sensor_disconnected_title">Sensor disconnected</string>
|
||||||
|
<string name="settings_announcements_sensor_connected_title">Sensor connected</string>
|
||||||
<string name="settings_announcements_statistics_title">Statistics Announcements</string>
|
<string name="settings_announcements_statistics_title">Statistics Announcements</string>
|
||||||
<string name="settings_announcements_summary">Time, Distance, Voice Speed</string>
|
<string name="settings_announcements_summary">Time, Distance, Voice Speed</string>
|
||||||
<string name="settings_import_export_title">Import and Export</string>
|
<string name="settings_import_export_title">Import and Export</string>
|
||||||
@@ -369,7 +373,9 @@ limitations under the License.
|
|||||||
<string name="settings_recording_max_recording_distance_summary">A new segment is created if the distance between two locations is greater than %1$s</string>
|
<string name="settings_recording_max_recording_distance_summary">A new segment is created if the distance between two locations is greater than %1$s</string>
|
||||||
<string name="settings_recording_max_recording_distance_title">Max distance</string>
|
<string name="settings_recording_max_recording_distance_title">Max distance</string>
|
||||||
<string name="settings_recording_min_recording_distance_title">Recording distance interval</string>
|
<string name="settings_recording_min_recording_distance_title">Recording distance interval</string>
|
||||||
<string name="settings_recording_min_recording_interval_title">Recording time interval</string>
|
<string name="marker_idle_duration_key" translatable="false">marker_idle_duration</string>
|
||||||
|
<string name="settings_recording_marker_idle_duration_title">Marker idle duration</string>
|
||||||
|
<string name="settings_recording_min_recording_interval_key" translatable="false">recording_min_recording_interval_key</string>
|
||||||
<string name="settings_recording_min_required_accuracy_summary">Location is ignored if its accuracy is greater than %1$s</string>
|
<string name="settings_recording_min_required_accuracy_summary">Location is ignored if its accuracy is greater than %1$s</string>
|
||||||
<string name="settings_recording_min_required_accuracy_title">GPS accuracy</string>
|
<string name="settings_recording_min_required_accuracy_title">GPS accuracy</string>
|
||||||
<string name="settings_recording_track_name_date_iso_8601_option">Date (ISO 8601)</string>
|
<string name="settings_recording_track_name_date_iso_8601_option">Date (ISO 8601)</string>
|
||||||
@@ -478,6 +484,14 @@ limitations under the License.
|
|||||||
<string name="stats_sensors_cadence">Cadence</string>
|
<string name="stats_sensors_cadence">Cadence</string>
|
||||||
<string name="stats_sensors_power">Power</string>
|
<string name="stats_sensors_power">Power</string>
|
||||||
<string name="stats_clock">Clock</string>
|
<string name="stats_clock">Clock</string>
|
||||||
|
<string name="stats_auto_pause_status">Auto-Pause Status</string>
|
||||||
|
<string name="stats_sensor_status_hr">Sensor HR</string>
|
||||||
|
<string name="stats_sensor_status_cadence">Sensor Cadence</string>
|
||||||
|
<string name="stats_sensor_status_speed">Sensor Speed</string>
|
||||||
|
<string name="stats_debug_idle_timer">Idle Timer</string>
|
||||||
|
<string name="stats_debug_walking_timer">Walking Timer</string>
|
||||||
|
<string name="stats_debug_marker_timer">Marker Timer</string>
|
||||||
|
|
||||||
<!-- Track -->
|
<!-- Track -->
|
||||||
<string name="track_name_format">Track %1$d</string>
|
<string name="track_name_format">Track %1$d</string>
|
||||||
<string name="track_recording_notification_accuracy">Location accuracy: %1$s</string>
|
<string name="track_recording_notification_accuracy">Location accuracy: %1$s</string>
|
||||||
@@ -536,6 +550,7 @@ limitations under the License.
|
|||||||
<string name="value_float_mile_hour_recommended">%1$.1f mi/h (recommended)</string>
|
<string name="value_float_mile_hour_recommended">%1$.1f mi/h (recommended)</string>
|
||||||
<string name="value_float_knots_recommended">%1$.1f knots (recommended)</string>
|
<string name="value_float_knots_recommended">%1$.1f knots (recommended)</string>
|
||||||
<string name="value_integer_minute">%1$d min</string>
|
<string name="value_integer_minute">%1$d min</string>
|
||||||
|
<string name="value_integer_minute_recommended">%1$d min (recommended)</string>
|
||||||
<string name="value_integer_second">%1$d s</string>
|
<string name="value_integer_second">%1$d s</string>
|
||||||
<string name="value_none">None</string>
|
<string name="value_none">None</string>
|
||||||
<string name="value_off">Off</string>
|
<string name="value_off">Off</string>
|
||||||
@@ -543,6 +558,9 @@ limitations under the License.
|
|||||||
<string name="value_smallest_recommended">Smallest (recommended)</string>
|
<string name="value_smallest_recommended">Smallest (recommended)</string>
|
||||||
<string name="value_int_seconds">%1$d s (recommended)</string>
|
<string name="value_int_seconds">%1$d s (recommended)</string>
|
||||||
<string name="voiceIdle">Becoming idle.</string>
|
<string name="voiceIdle">Becoming idle.</string>
|
||||||
|
<string name="voiceLongBreak">Long break recorded.</string>
|
||||||
|
<string name="voiceSensorDisconnected">Sensor disconnected.</string>
|
||||||
|
<string name="voiceSensorConnected">Sensor connected.</string>
|
||||||
<string name="voiceHoursPlural">
|
<string name="voiceHoursPlural">
|
||||||
{n, plural,
|
{n, plural,
|
||||||
=1 {1 hour}
|
=1 {1 hour}
|
||||||
@@ -705,4 +723,22 @@ limitations under the License.
|
|||||||
<string name="settings_announcements_lap_power">Lap power</string>
|
<string name="settings_announcements_lap_power">Lap power</string>
|
||||||
<string name="power_x_watt">Power {x, plural, =1 {1 watt} other {# watts} }</string>
|
<string name="power_x_watt">Power {x, plural, =1 {1 watt} other {# watts} }</string>
|
||||||
<string name="power">Power</string>
|
<string name="power">Power</string>
|
||||||
|
<string name="settings_recording_prevent_idle_if_cadence_key" translatable="false">preventIdleIfCadence</string>
|
||||||
|
<string name="settings_recording_prevent_idle_if_cadence_title">Prevent auto-pause if pedaling</string>
|
||||||
|
<string name="settings_recording_prevent_idle_if_cadence_summary">Do not enter idle mode while pedaling even if not moving</string>
|
||||||
|
<string name="sensor_disconnected_warning">Sensor disconnected. Continue with GPS?</string>
|
||||||
|
<string name="sensor_disconnected_switch_action">Switch</string>
|
||||||
|
<string name="settings_recording_auto_pause_on_cadence_0_key" translatable="false">autoPauseCadence0</string>
|
||||||
|
<string name="settings_recording_auto_pause_on_cadence_0_title">Auto-pause when carrying bike</string>
|
||||||
|
<string name="settings_recording_auto_pause_on_cadence_0_summary">Pause recording if cadence is 0 and speed is low</string>
|
||||||
|
<string name="settings_recording_auto_pause_on_cadence_0_speed_key" translatable="false">autoPauseCadence0Speed</string>
|
||||||
|
<string name="settings_recording_auto_pause_on_cadence_0_speed_title">Auto-pause max speed</string>
|
||||||
|
<string name="settings_recording_auto_pause_on_cadence_0_speed_summary">Speed below which walking with bike is detected (if cadence is 0)</string>
|
||||||
|
<string name="auto_pause_cadence_0_speed_default" translatable="false">8</string> <string name="media_title_default">No media playing</string>
|
||||||
|
<string name="media_artist_default">OpenTracks</string>
|
||||||
|
<string name="media_prev">Previous</string>
|
||||||
|
<string name="media_next">Next</string>
|
||||||
|
<string name="media_play_pause">Play/Pause</string>
|
||||||
|
<string name="media_permission_request">Enable Notification Access</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -22,6 +22,21 @@
|
|||||||
android:defaultValue="@bool/voice_announce_idle_default"
|
android:defaultValue="@bool/voice_announce_idle_default"
|
||||||
android:key="@string/voice_announce_idle_key"
|
android:key="@string/voice_announce_idle_key"
|
||||||
android:title="@string/settings_announcements_idle_title" />
|
android:title="@string/settings_announcements_idle_title" />
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="@bool/voice_announce_long_break_default"
|
||||||
|
android:key="@string/voice_announce_long_break_key"
|
||||||
|
android:title="@string/settings_announcements_long_break_title" />
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<PreferenceCategory android:title="@string/settings_announcements_sensor_category_title">
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="@bool/voice_announce_sensor_disconnected_default"
|
||||||
|
android:key="@string/voice_announce_sensor_disconnected_key"
|
||||||
|
android:title="@string/settings_announcements_sensor_disconnected_title" />
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="@bool/voice_announce_sensor_connected_default"
|
||||||
|
android:key="@string/voice_announce_sensor_connected_key"
|
||||||
|
android:title="@string/settings_announcements_sensor_connected_title" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/settings_announcements_statistics_title">
|
<PreferenceCategory android:title="@string/settings_announcements_statistics_title">
|
||||||
|
|||||||
@@ -4,6 +4,25 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:title="@string/settings_recording_title">
|
android:title="@string/settings_recording_title">
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="@string/settings_recording_auto_pause_on_cadence_0_key"
|
||||||
|
android:title="@string/settings_recording_auto_pause_on_cadence_0_title"
|
||||||
|
android:summary="@string/settings_recording_auto_pause_on_cadence_0_summary" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="true"
|
||||||
|
android:key="@string/settings_recording_prevent_idle_if_cadence_key"
|
||||||
|
android:title="@string/settings_recording_prevent_idle_if_cadence_title"
|
||||||
|
android:summary="@string/settings_recording_prevent_idle_if_cadence_summary" />
|
||||||
|
|
||||||
|
<ListPreference
|
||||||
|
android:defaultValue="@string/auto_pause_cadence_0_speed_default"
|
||||||
|
android:entryValues="@array/auto_pause_cadence_0_speed_values"
|
||||||
|
android:key="@string/settings_recording_auto_pause_on_cadence_0_speed_key"
|
||||||
|
android:title="@string/settings_recording_auto_pause_on_cadence_0_speed_title"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
|
||||||
<ListPreference
|
<ListPreference
|
||||||
android:defaultValue="@string/recording_distance_interval_default"
|
android:defaultValue="@string/recording_distance_interval_default"
|
||||||
android:entryValues="@array/recording_distance_interval_values"
|
android:entryValues="@array/recording_distance_interval_values"
|
||||||
@@ -19,8 +38,17 @@
|
|||||||
<ListPreference
|
<ListPreference
|
||||||
android:defaultValue="@string/idle_duration_default"
|
android:defaultValue="@string/idle_duration_default"
|
||||||
android:entryValues="@array/idle_duration_values"
|
android:entryValues="@array/idle_duration_values"
|
||||||
|
android:entries="@array/idle_duration_values"
|
||||||
android:key="@string/idle_duration_key"
|
android:key="@string/idle_duration_key"
|
||||||
android:title="@string/settings_recording_idle_timeout_title"
|
android:title="@string/settings_recording_idle_timeout_title"
|
||||||
app:useSimpleSummaryProvider="true" />
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
|
||||||
|
<ListPreference
|
||||||
|
android:defaultValue="@string/marker_idle_duration_default"
|
||||||
|
android:entryValues="@array/marker_idle_duration_values"
|
||||||
|
android:entries="@array/marker_idle_duration_values"
|
||||||
|
android:key="@string/marker_idle_duration_key"
|
||||||
|
android:title="@string/settings_recording_marker_idle_duration_title"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
+1466
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user