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:
+68
-17
@@ -92,7 +92,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -132,7 +135,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -172,7 +178,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -212,7 +221,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -254,7 +266,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -276,7 +291,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -316,7 +334,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -338,7 +359,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -378,7 +402,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -418,7 +445,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -458,7 +488,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -498,7 +531,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -520,7 +556,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -560,7 +599,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -582,7 +624,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -610,7 +655,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
@@ -644,7 +692,10 @@ public class VoiceAnnouncementUtilsTest {
|
||||
new SensorData<>(HeartRate.of(60), "unused"),
|
||||
null, null,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// when
|
||||
|
||||
@@ -421,6 +421,16 @@ limitations under the License.
|
||||
android:icon="@drawable/ic_logo_color_24dp"
|
||||
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-->
|
||||
<service
|
||||
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 Snackbar snackbar;
|
||||
private Snackbar sensorWarningSnackbar;
|
||||
|
||||
// The following are setFrequency in onCreate
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
@@ -86,6 +87,9 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
service.getGpsStatusObservable()
|
||||
.observe(TrackRecordingActivity.this, this::onGpsStatusChanged);
|
||||
|
||||
service.getSensorDisconnectedWarningObservable()
|
||||
.observe(TrackRecordingActivity.this, this::onSensorDisconnectedWarningChanged);
|
||||
|
||||
if (!service.isRecording()) {
|
||||
finish();
|
||||
return;
|
||||
@@ -408,4 +412,27 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
});
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
private void onSensorDisconnectedWarningChanged(boolean show) {
|
||||
if (show) {
|
||||
if (sensorWarningSnackbar == null) {
|
||||
sensorWarningSnackbar = Snackbar
|
||||
.make(viewBinding.trackRecordingCoordinatorLayout,
|
||||
getString(R.string.sensor_disconnected_warning),
|
||||
Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(getString(R.string.sensor_disconnected_switch_action), v -> {
|
||||
if (trackRecordingServiceConnection != null && trackRecordingServiceConnection.getTrackRecordingService() != null) {
|
||||
trackRecordingServiceConnection.getTrackRecordingService().setOverrideSensorWithGPS(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!sensorWarningSnackbar.isShown()) {
|
||||
sensorWarningSnackbar.show();
|
||||
}
|
||||
} else {
|
||||
if (sensorWarningSnackbar != null && sensorWarningSnackbar.isShown()) {
|
||||
sensorWarningSnackbar.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,12 +151,14 @@ public class SegmentStatisticUpdater {
|
||||
}
|
||||
|
||||
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()) {
|
||||
throw new RuntimeException("Moving time cannot be negative: " + lastTrackPoint.getTime() + " is after " + trackPoint.getTime());
|
||||
public void addMovingTime(Duration duration) {
|
||||
if (duration.isNegative()) {
|
||||
throw new RuntimeException("Moving time cannot be negative");
|
||||
}
|
||||
this.movingDuration = this.movingDuration.plus(movingDuration);
|
||||
movingDuration = movingDuration.plus(duration);
|
||||
}
|
||||
|
||||
private Speed getAverageMovingSpeed() {
|
||||
|
||||
+21
-11
@@ -143,7 +143,6 @@ public class TrackStatisticsUpdater {
|
||||
}
|
||||
|
||||
{
|
||||
// Update total distance
|
||||
Distance movingDistance = null;
|
||||
if (trackPoint.sensorDistance() != null) {
|
||||
movingDistance = trackPoint.sensorDistance();
|
||||
@@ -153,16 +152,7 @@ public class TrackStatisticsUpdater {
|
||||
// GPS-based distance/speed
|
||||
movingDistance = trackPoint.distanceToPrevious(lastTrackPoint);
|
||||
}
|
||||
if (movingDistance != null) {
|
||||
currentSegment.addTotalDistance(movingDistance);
|
||||
}
|
||||
|
||||
if (!isIdle
|
||||
&& !trackPoint.isSegmentManualStart()
|
||||
&& lastTrackPoint != null) {
|
||||
currentSegment.addMovingTime(trackPoint, lastTrackPoint);
|
||||
}
|
||||
|
||||
boolean isIdleTransition = false;
|
||||
if (trackPoint.isIdleTriggered()) {
|
||||
isIdle = true;
|
||||
} else if (isIdle) {
|
||||
@@ -170,6 +160,26 @@ public class TrackStatisticsUpdater {
|
||||
if (movingDistance != null
|
||||
&& movingDistance.greaterOrEqualThan(PreferencesUtils.getRecordingDistanceInterval())) {
|
||||
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 + ")");
|
||||
}
|
||||
|
||||
// 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 {
|
||||
context.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||
import de.dennisguse.opentracks.sensors.driver.Driver;
|
||||
import de.dennisguse.opentracks.sensors.driver.DriverObserver;
|
||||
import de.dennisguse.opentracks.sensors.driver.SensorType;
|
||||
@@ -28,6 +29,9 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
|
||||
|
||||
protected Aggregator<AggregatorInput, AggregatorOutput> aggregator;
|
||||
|
||||
@Nullable
|
||||
private Integer batteryLevel = null;
|
||||
|
||||
protected SensorHandler(SensorManager sensorManager) {
|
||||
this.sensorManager = sensorManager;
|
||||
}
|
||||
@@ -115,7 +119,8 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
|
||||
return;
|
||||
}
|
||||
|
||||
SensorHandler.this.aggregator.resetAggregated();
|
||||
SensorHandler.this.aggregator = null;
|
||||
sensorManager.onChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -130,5 +135,18 @@ public abstract class SensorHandler<AggregatorInput, AggregatorOutput> implement
|
||||
@Override
|
||||
public void onDisconnected() {
|
||||
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 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.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
@@ -239,6 +241,9 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
}
|
||||
|
||||
public SensorDataSet getSensorDataSet(Instant now) {
|
||||
if (!isStarted()) {
|
||||
return new SensorDataSet(new SensorData<>(Position.of(now), ""), null, null, null, null, null, null, null, null, null, null);
|
||||
}
|
||||
//We always need a Position with now
|
||||
SensorData<Position> gpsSensorData = gpsHandler.getSensorData(now);
|
||||
if (gpsSensorData != null && gpsSensorData.data() != null) {
|
||||
@@ -247,6 +252,13 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
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(
|
||||
gpsSensorData,
|
||||
getSpeed(now),
|
||||
@@ -255,7 +267,10 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
temperatureHandler.getSensorData(now),
|
||||
getCadence(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.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.BatteryLevel;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
||||
|
||||
@@ -53,6 +54,9 @@ public class BluetoothDriver<T> implements Driver {
|
||||
|
||||
private final BluetoothAdapter bluetoothAdapter;
|
||||
private BluetoothGatt bluetoothGatt;
|
||||
private Context context;
|
||||
private Handler handler;
|
||||
private String address;
|
||||
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
|
||||
@@ -68,10 +72,21 @@ public class BluetoothDriver<T> implements Driver {
|
||||
case BluetoothProfile.STATE_DISCONNECTED -> {
|
||||
//This is also triggered, if no connection was established (ca. 30s)
|
||||
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();
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
@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
|
||||
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
UUID serviceUUID = characteristic.getService().getUuid();
|
||||
@@ -169,6 +214,10 @@ public class BluetoothDriver<T> implements Driver {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -184,6 +233,10 @@ public class BluetoothDriver<T> implements Driver {
|
||||
Log.i(TAG, bluetoothGatt.getDevice() + ": disconnect finished");
|
||||
bluetoothGatt = null;
|
||||
|
||||
this.context = null;
|
||||
this.handler = null;
|
||||
this.address = null;
|
||||
|
||||
observer.onDisconnected();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,4 +28,10 @@ public interface DriverObserver<T> {
|
||||
* For example, the user deactivated the internal GPS.
|
||||
*/
|
||||
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 java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.sensors.UintUtils;
|
||||
@@ -18,17 +19,20 @@ public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth
|
||||
super(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
private Instant lastValidUpdate;
|
||||
|
||||
@Override
|
||||
protected void computeValue(@NonNull Raw<CyclingCadenceBluetooth.CyclingCadenceMeasurement> current) {
|
||||
if (current.value() instanceof CyclingCadenceBluetooth.DirectCadenceData directCadenceData) {
|
||||
output = directCadenceData.cadence();
|
||||
lastValidUpdate = current.time();
|
||||
return;
|
||||
}
|
||||
|
||||
computeRawCadence((CyclingCadenceBluetooth.CrankData) current.value());
|
||||
computeRawCadence((CyclingCadenceBluetooth.CrankData) current.value(), current.time());
|
||||
}
|
||||
|
||||
private void computeRawCadence(@NonNull CyclingCadenceBluetooth.CrankData currentRaw) {
|
||||
private void computeRawCadence(@NonNull CyclingCadenceBluetooth.CrankData currentRaw, Instant now) {
|
||||
CyclingCadenceBluetooth.CrankData previousRaw = getPreviousRaw();
|
||||
if (previousRaw == null) return;
|
||||
|
||||
@@ -36,6 +40,9 @@ public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth
|
||||
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
||||
|
||||
if (timeDiff.isZero()) {
|
||||
if (lastValidUpdate != null && Duration.between(lastValidUpdate, now).compareTo(getMaxSensorAge()) > 0) {
|
||||
resetOutdated();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (timeDiff.isNegative()) {
|
||||
@@ -52,9 +59,15 @@ public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth
|
||||
}
|
||||
|
||||
long crankDiff = UintUtils.diff(currentRaw.crankRevolutionsCount(), previousRaw.crankRevolutionsCount(), UintUtils.UINT32_MAX);
|
||||
lastValidUpdate = now;
|
||||
output = Cadence.of(crankDiff, timeDiff);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resetOutdated() {
|
||||
output = Cadence.of(0f);
|
||||
}
|
||||
|
||||
private CyclingCadenceBluetooth.CrankData getPreviousRaw() {
|
||||
if (previous == null) {
|
||||
return null;
|
||||
|
||||
+8
@@ -5,6 +5,7 @@ import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
@@ -21,9 +22,12 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<CyclingDistanceSp
|
||||
super(sensorAddress, sensorName);
|
||||
}
|
||||
|
||||
private Instant lastValidUpdate;
|
||||
|
||||
@Override
|
||||
protected void computeValue(@NonNull Raw<CyclingDistanceSpeedBluetooth.WheelData> current) {
|
||||
if (previous == null) {
|
||||
lastValidUpdate = current.time();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,6 +35,9 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<CyclingDistanceSp
|
||||
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
|
||||
|
||||
if (timeDiff.isZero()) {
|
||||
if (lastValidUpdate != null && Duration.between(lastValidUpdate, current.time()).compareTo(getMaxSensorAge()) > 0) {
|
||||
resetOutdated();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (timeDiff.isNegative()) {
|
||||
@@ -52,6 +59,7 @@ public class AggregatorCyclingDistanceSpeed extends Aggregator<CyclingDistanceSp
|
||||
distanceOverall = distance.plus(output.distanceOverall);
|
||||
}
|
||||
Speed speed_mps = Speed.of(distance, timeDiff);
|
||||
lastValidUpdate = current.time();
|
||||
output = new Data(distance, distanceOverall, speed_mps);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
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.Distance;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
@@ -27,6 +28,12 @@ public record SensorDataSet(
|
||||
@Nullable
|
||||
SensorData<Power> power,
|
||||
@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,
|
||||
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() {
|
||||
return track.statistics();
|
||||
|
||||
@@ -36,10 +36,12 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
private static final AltitudeCorrectionManager ALTITUDE_CORRECTION_MANAGER = new AltitudeCorrectionManager();
|
||||
|
||||
private final Runnable ON_IDLE = this::onIdle;
|
||||
private final Runnable ON_LONG_BREAK = this::onLongBreak;
|
||||
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
private final Context context;
|
||||
private final IdleObserver idleObserver;
|
||||
private final SensorDisconnectObserver sensorDisconnectObserver;
|
||||
|
||||
private final Handler handler;
|
||||
|
||||
@@ -59,9 +61,25 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
private TrackPoint lastStoredTrackPoint;
|
||||
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.idleObserver = idleObserver;
|
||||
this.sensorDisconnectObserver = sensorDisconnectObserver;
|
||||
this.trackPointCreator = trackPointCreator;
|
||||
this.handler = handler;
|
||||
contentProviderUtils = new ContentProviderUtils(context);
|
||||
@@ -97,6 +115,23 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -116,6 +151,23 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
reset();
|
||||
|
||||
handler.post(() -> {
|
||||
if (this.trackId != null) {
|
||||
onIdle();
|
||||
}
|
||||
});
|
||||
String address = de.dennisguse.opentracks.settings.PreferencesUtils.getString(de.dennisguse.opentracks.R.string.settings_sensor_bluetooth_cycling_speed_key, de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue());
|
||||
if (!de.dennisguse.opentracks.sensors.driver.SensorType.NONE.getPreferenceValue().equals(address)) {
|
||||
handler.postDelayed(() -> {
|
||||
if (trackId != null && !hasSensorBeenConnected && !isSensorDisconnectedWarningActive) {
|
||||
isSensorDisconnectedWarningActive = true;
|
||||
if (sensorDisconnectObserver != null) {
|
||||
sensorDisconnectObserver.onSensorDisconnectedWarning(true);
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -125,6 +177,12 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
trackId = null;
|
||||
trackStatisticsUpdater = null;
|
||||
|
||||
idleTimeoutScheduledAt = null;
|
||||
walkingStartedAt = null;
|
||||
idleStartedAt = null;
|
||||
longBreakRecorded = false;
|
||||
handler.removeCallbacks(ON_LONG_BREAK);
|
||||
|
||||
reset();
|
||||
}
|
||||
@@ -144,32 +202,115 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
TrackBuilder trackBuilder = new TrackBuilder(track);
|
||||
trackBuilder.setStatistics(tmpTrackStatisticsUpdater.getTrackStatistics());
|
||||
|
||||
return new RecordingData(trackBuilder.getTrack(), TrackPointWithEGM2008, current.second, tmpTrackStatisticsUpdater.getCurrentSegment());
|
||||
|
||||
java.time.Instant now = java.time.Instant.now();
|
||||
String idleTimer = "";
|
||||
if (idleTimeoutScheduledAt != null && !"IDLE".equals(autoPauseStatus) && !"WALKING".equals(autoPauseStatus)) {
|
||||
java.time.Duration d = java.time.Duration.between(now, idleTimeoutScheduledAt);
|
||||
if (!d.isNegative()) idleTimer = de.dennisguse.opentracks.util.StringUtils.formatElapsedTime(d);
|
||||
}
|
||||
|
||||
String walkingTimer = "";
|
||||
if (walkingStartedAt != null && "WALKING".equals(autoPauseStatus)) {
|
||||
java.time.Duration d = java.time.Duration.between(walkingStartedAt, now);
|
||||
if (!d.isNegative()) walkingTimer = de.dennisguse.opentracks.util.StringUtils.formatElapsedTime(d);
|
||||
}
|
||||
|
||||
String markerTimer = "";
|
||||
if (idleStartedAt != null && "IDLE".equals(autoPauseStatus)) {
|
||||
java.time.Duration d = java.time.Duration.between(idleStartedAt, now);
|
||||
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)
|
||||
public synchronized void onIdle() {
|
||||
Log.d(TAG, "Becoming idle");
|
||||
onNewTrackPoint(trackPointCreator.createIdle());
|
||||
TrackPoint idlePoint = trackPointCreator.createIdle();
|
||||
|
||||
boolean preventIdle = de.dennisguse.opentracks.settings.PreferencesUtils.shouldPreventIdleIfCadence();
|
||||
if (preventIdle && hasCadenceSensorBeenConnected) {
|
||||
if (idlePoint.cadence() != null && idlePoint.cadence().value_rpm() > 0) {
|
||||
scheduleNewIdleTimeout();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!"IDLE".equals(autoPauseStatus)) {
|
||||
idleStartedAt = java.time.Instant.now();
|
||||
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();
|
||||
}
|
||||
|
||||
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?
|
||||
*/
|
||||
synchronized boolean onNewTrackPoint(@NonNull TrackPoint trackPoint) {
|
||||
if (trackPoint.position().hasSpeed()) {
|
||||
lastTrackPointUIWithSpeed = trackPoint;
|
||||
}
|
||||
if (trackPoint.position().hasAltitude()) {
|
||||
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) {
|
||||
longBreakRecorded = false;
|
||||
insertTrackPoint(trackPoint, true);
|
||||
handler.removeCallbacks(ON_IDLE);
|
||||
|
||||
long durationMs = PreferencesUtils.getMarkerIdleDuration().toMillis();
|
||||
if (durationMs > 0) {
|
||||
handler.postDelayed(ON_LONG_BREAK, durationMs);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
//Storing trackPoint
|
||||
@@ -200,11 +341,37 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
return true;
|
||||
}
|
||||
|
||||
if (trackPoint.cadence() != null) {
|
||||
hasCadenceSensorBeenConnected = true;
|
||||
}
|
||||
|
||||
boolean isWalkingWithBike = false;
|
||||
if (autoPauseOnCadence0 && hasCadenceSensorBeenConnected && autoPauseCadence0Speed != null) {
|
||||
if (trackPoint.cadence() == null || trackPoint.cadence().getRPM() == 0) {
|
||||
if (trackPoint.position().hasSpeed() && trackPoint.position().speed().toMPS() > 0 && trackPoint.position().speed().lessThan(autoPauseCadence0Speed)) {
|
||||
isWalkingWithBike = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isWalkingWithBike && "WALKING".equals(autoPauseStatus)) {
|
||||
autoPauseStatus = "REC";
|
||||
walkingStartedAt = null;
|
||||
scheduleNewIdleTimeout();
|
||||
}
|
||||
|
||||
Distance distanceToLastStoredTrackPoint;
|
||||
if (trackPoint.position().hasLocation() && !lastStoredTrackPoint.position().hasLocation()) {
|
||||
distanceToLastStoredTrackPoint = trackPoint.distanceToPreviousFromLocation(lastStoredTrackPointWithLocation);
|
||||
if (isWalkingWithBike || strictSensorMode) {
|
||||
distanceToLastStoredTrackPoint = Distance.ZERO;
|
||||
if (isWalkingWithBike) {
|
||||
autoPauseStatus = "WALKING";
|
||||
}
|
||||
} else {
|
||||
distanceToLastStoredTrackPoint = trackPoint.distanceToPrevious(lastStoredTrackPoint);
|
||||
if (trackPoint.position().hasLocation() && !lastStoredTrackPoint.position().hasLocation()) {
|
||||
distanceToLastStoredTrackPoint = trackPoint.distanceToPreviousFromLocation(lastStoredTrackPointWithLocation);
|
||||
} else {
|
||||
distanceToLastStoredTrackPoint = trackPoint.distanceToPrevious(lastStoredTrackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
if (distanceToLastStoredTrackPoint.greaterThan(maxRecordingDistance)) {
|
||||
@@ -212,6 +379,20 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
insertTrackPoint(trackPoint, true);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -219,6 +400,20 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
insertTrackPoint(trackPoint, false);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -235,6 +430,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
}
|
||||
handler.removeCallbacks(ON_IDLE);
|
||||
handler.postDelayed(ON_IDLE, idleDuration.toMillis());
|
||||
idleTimeoutScheduledAt = java.time.Instant.now().plus(idleDuration);
|
||||
}
|
||||
|
||||
Statistics getTrackStatistics() {
|
||||
@@ -284,6 +480,13 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
lastStoredTrackPoint = null;
|
||||
lastStoredTrackPointWithLocation = null;
|
||||
|
||||
hasSensorBeenConnected = false;
|
||||
isSensorDisconnectedWarningActive = false;
|
||||
hasCadenceSensorBeenConnected = false;
|
||||
if (sensorDisconnectObserver != null) {
|
||||
sensorDisconnectObserver.onSensorDisconnectedWarning(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -297,13 +500,36 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
if (PreferencesUtils.isKey(R.string.idle_duration_key, key)) {
|
||||
idleDuration = PreferencesUtils.getIdleDurationTimeout();
|
||||
}
|
||||
if (PreferencesUtils.isKey(R.string.settings_recording_auto_pause_on_cadence_0_key, key)) {
|
||||
autoPauseOnCadence0 = PreferencesUtils.shouldAutoPauseOnCadence0();
|
||||
}
|
||||
if (PreferencesUtils.isKey(R.string.settings_recording_auto_pause_on_cadence_0_speed_key, key)) {
|
||||
autoPauseCadence0Speed = PreferencesUtils.getAutoPauseCadence0Speed();
|
||||
}
|
||||
}
|
||||
|
||||
public TrackPoint getLastStoredTrackPointWithLocation() {
|
||||
return lastStoredTrackPointWithLocation;
|
||||
}
|
||||
|
||||
public void setOverrideSensorWithGPS(boolean override) {
|
||||
if (override) {
|
||||
hasSensorBeenConnected = false;
|
||||
gpsOverriden = true;
|
||||
if (isSensorDisconnectedWarningActive) {
|
||||
isSensorDisconnectedWarningActive = false;
|
||||
sensorDisconnectObserver.onSensorDisconnectedWarning(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IdleObserver {
|
||||
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.settings.PreferencesUtils;
|
||||
|
||||
public class TrackRecordingService extends Service implements TrackPointCreator.Callback, SharedPreferences.OnSharedPreferenceChangeListener, TrackRecordingManager.IdleObserver {
|
||||
public class TrackRecordingService extends Service implements TrackPointCreator.Callback, SharedPreferences.OnSharedPreferenceChangeListener, TrackRecordingManager.IdleObserver, TrackRecordingManager.SensorDisconnectObserver {
|
||||
|
||||
private static final String TAG = TrackRecordingService.class.getSimpleName();
|
||||
|
||||
@@ -84,6 +84,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
private RecordingStatus recordingStatus;
|
||||
private MutableLiveData<RecordingStatus> recordingStatusObservable;
|
||||
private MutableLiveData<GpsStatusValue> gpsStatusObservable;
|
||||
private MutableLiveData<Boolean> sensorDisconnectedWarningObservable;
|
||||
private MutableLiveData<RecordingData> recordingDataObservable;
|
||||
|
||||
private Handler handler;
|
||||
@@ -104,10 +105,11 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
recordingStatusObservable = new MutableLiveData<>();
|
||||
updateRecordingStatus(STATUS_DEFAULT);
|
||||
gpsStatusObservable = new MutableLiveData<>(STATUS_GPS_DEFAULT);
|
||||
sensorDisconnectedWarningObservable = new MutableLiveData<>(false);
|
||||
recordingDataObservable = new MutableLiveData<>(RecordingData.NOT_RECORDING);
|
||||
|
||||
trackPointCreator = new TrackPointCreator(this);
|
||||
trackRecordingManager = new TrackRecordingManager(this, trackPointCreator, this, handler);
|
||||
trackRecordingManager = new TrackRecordingManager(this, trackPointCreator, this, this, handler);
|
||||
|
||||
voiceAnnouncementManager = new VoiceAnnouncementManager(this);
|
||||
notificationManager = new TrackRecordingServiceNotificationManager(this);
|
||||
@@ -138,6 +140,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
|
||||
recordingStatusObservable = null;
|
||||
gpsStatusObservable = null;
|
||||
sensorDisconnectedWarningObservable = null;
|
||||
recordingDataObservable = null;
|
||||
|
||||
Log.d(TAG, "Destroyed");
|
||||
@@ -315,6 +318,10 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
return gpsStatusObservable;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getSensorDisconnectedWarningObservable() {
|
||||
return sensorDisconnectedWarningObservable;
|
||||
}
|
||||
|
||||
public LiveData<RecordingData> getRecordingDataObservable() {
|
||||
return recordingDataObservable;
|
||||
}
|
||||
@@ -337,6 +344,32 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
voiceAnnouncementManager.announceIdle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongBreak() {
|
||||
voiceAnnouncementManager.announceLongBreak();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorDisconnectedWarning(boolean show) {
|
||||
if (show) {
|
||||
voiceAnnouncementManager.announceSensorDisconnected();
|
||||
}
|
||||
if (sensorDisconnectedWarningObservable != null) {
|
||||
sensorDisconnectedWarningObservable.postValue(show);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorConnected() {
|
||||
voiceAnnouncementManager.announceSensorConnected();
|
||||
}
|
||||
|
||||
public void setOverrideSensorWithGPS(boolean override) {
|
||||
if (trackRecordingManager != null) {
|
||||
trackRecordingManager.setOverrideSensorWithGPS(override);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void stopUpdateRecordingData() {
|
||||
handler.removeCallbacks(updateRecordingData);
|
||||
|
||||
+24
@@ -119,6 +119,30 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe
|
||||
voiceAnnouncement.speak(VoiceAnnouncementUtils.createIdle(context));
|
||||
}
|
||||
|
||||
public void announceLongBreak() {
|
||||
if (shouldNotAnnounce() || !PreferencesUtils.shouldVoiceAnnouncementLongBreak()) {
|
||||
return;
|
||||
}
|
||||
|
||||
voiceAnnouncement.speak(VoiceAnnouncementUtils.createLongBreak(context));
|
||||
}
|
||||
|
||||
public void announceSensorDisconnected() {
|
||||
if (shouldNotAnnounce() || !PreferencesUtils.shouldVoiceAnnouncementSensorDisconnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
voiceAnnouncement.speak(VoiceAnnouncementUtils.createSensorDisconnected(context));
|
||||
}
|
||||
|
||||
public void announceSensorConnected() {
|
||||
if (shouldNotAnnounce() || !PreferencesUtils.shouldVoiceAnnouncementSensorConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
voiceAnnouncement.speak(VoiceAnnouncementUtils.createSensorConnected(context));
|
||||
}
|
||||
|
||||
public void announceStatisticsIfNeeded(@NonNull RecordingData data) {
|
||||
if (shouldNotAnnounce()) {
|
||||
return;
|
||||
|
||||
+15
@@ -50,6 +50,21 @@ class VoiceAnnouncementUtils {
|
||||
.append(context.getString(R.string.voiceIdle));
|
||||
}
|
||||
|
||||
static Spannable createLongBreak(Context context) {
|
||||
return new SpannableStringBuilder()
|
||||
.append(context.getString(R.string.voiceLongBreak));
|
||||
}
|
||||
|
||||
static Spannable createSensorDisconnected(Context context) {
|
||||
return new SpannableStringBuilder()
|
||||
.append(context.getString(R.string.voiceSensorDisconnected));
|
||||
}
|
||||
|
||||
static Spannable createSensorConnected(Context context) {
|
||||
return new SpannableStringBuilder()
|
||||
.append(context.getString(R.string.voiceSensorConnected));
|
||||
}
|
||||
|
||||
static Spannable createStatistics(Context context, Track track, SensorDataSet sensorDataSet, UnitSystem unitSystem, boolean isReportSpeed, @Nullable Statistics currentInterval, @Nullable SensorStatistics sensorStatistics) {
|
||||
Statistics trackStatistics = track.statistics();
|
||||
|
||||
|
||||
@@ -101,8 +101,11 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
|
||||
position = position.withCoordinates(lastStoredTrackPointWithLocation.position());
|
||||
}
|
||||
|
||||
if (lastTrackPointUISpeed != null)
|
||||
if (sensorDataSet.speed() != null) {
|
||||
position = position.with(sensorDataSet.speed().data());
|
||||
} else if (lastTrackPointUISpeed != null) {
|
||||
position = position.with(lastTrackPointUISpeed.position().speed());
|
||||
}
|
||||
|
||||
if (lastTrackPointUIAltitude != null)
|
||||
position = position.with(lastTrackPointUIAltitude.position().altitude());
|
||||
|
||||
@@ -38,11 +38,13 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.ActivityType;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.HeartRateZones;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
@@ -249,6 +251,10 @@ public class PreferencesUtils {
|
||||
setString(R.string.stats_units_key, unitSystem.getPreferenceId());
|
||||
}
|
||||
|
||||
public static Duration getMarkerIdleDuration() {
|
||||
return Duration.ofMinutes(getInt(R.string.marker_idle_duration_key, 3));
|
||||
}
|
||||
|
||||
//TODO Check if actually needed or can be superseeded by a flexible default in getUnit()
|
||||
public static void applyDefaultUnit() {
|
||||
if (getString(R.string.stats_units_key, "").isEmpty()) {
|
||||
@@ -342,6 +348,20 @@ public class PreferencesUtils {
|
||||
return getBoolean(R.string.voice_announce_idle_key, true);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnouncementLongBreak() {
|
||||
return getBoolean(R.string.voice_announce_long_break_key, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static boolean shouldVoiceAnnouncementSensorDisconnected() {
|
||||
return getBoolean(R.string.voice_announce_sensor_disconnected_key, true);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnouncementSensorConnected() {
|
||||
return getBoolean(R.string.voice_announce_sensor_connected_key, true);
|
||||
}
|
||||
|
||||
public static boolean shouldVoiceAnnounceTime() {
|
||||
return getBoolean(R.string.voice_announce_time_key, true);
|
||||
}
|
||||
@@ -682,6 +702,51 @@ public class PreferencesUtils {
|
||||
return entries;
|
||||
}
|
||||
|
||||
static String[] getMarkerIdleDurationEntries() {
|
||||
String[] entryValues = resources.getStringArray(R.array.marker_idle_duration_values);
|
||||
String[] entries = new String[entryValues.length];
|
||||
|
||||
final int defaultDuration = Integer.parseInt(resources.getString(R.string.marker_idle_duration_default));
|
||||
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
int value = Integer.parseInt(entryValues[i]);
|
||||
|
||||
if (value == defaultDuration) {
|
||||
entries[i] = resources.getString(R.string.value_integer_minute_recommended, value);
|
||||
} else {
|
||||
entries[i] = resources.getString(R.string.value_integer_minute, value);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public static boolean shouldAutoPauseOnCadence0() {
|
||||
return getBoolean(R.string.settings_recording_auto_pause_on_cadence_0_key, false);
|
||||
}
|
||||
|
||||
public static boolean shouldPreventIdleIfCadence() {
|
||||
return getBoolean(R.string.settings_recording_prevent_idle_if_cadence_key, true);
|
||||
}
|
||||
|
||||
public static Speed getAutoPauseCadence0Speed() {
|
||||
final int DEFAULT = Integer.parseInt(resources.getString(R.string.auto_pause_cadence_0_speed_default));
|
||||
int value = getInt(R.string.settings_recording_auto_pause_on_cadence_0_speed_key, DEFAULT);
|
||||
return Speed.ofKMH(value);
|
||||
}
|
||||
|
||||
static String[] getAutoPauseCadence0SpeedEntries() {
|
||||
String[] entryValues = resources.getStringArray(R.array.auto_pause_cadence_0_speed_values);
|
||||
String[] entries = new String[entryValues.length];
|
||||
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
int value = Integer.parseInt(entryValues[i]);
|
||||
entries[i] = value + " km/h";
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public static boolean shouldInstantExportToFile() {
|
||||
final boolean INSTANT_POST_WORKOUT_EXPORT_DEFAULT = resources.getBoolean(R.bool.post_workout_export_enabled_default);
|
||||
return getBoolean(R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT) && isDefaultExportDirectoryUri();
|
||||
@@ -839,7 +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_power_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_clock_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_temperature_key), false, false, false)
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_temperature_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_auto_pause_status_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_sensor_status_hr_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_sensor_status_cadence_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_sensor_status_speed_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_debug_idle_timer_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_debug_walking_timer_key), false, false, false),
|
||||
new DataField(resources.getString(R.string.stats_custom_layout_debug_marker_timer_key), false, false, false),
|
||||
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);
|
||||
@@ -855,7 +928,7 @@ public class PreferencesUtils {
|
||||
String selectedLayout = getSelectedLayoutName();
|
||||
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();
|
||||
|
||||
@@ -901,7 +974,18 @@ public class PreferencesUtils {
|
||||
public static List<RecordingLayout> getAllCustomLayouts() {
|
||||
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() {
|
||||
|
||||
@@ -57,5 +57,15 @@ public class RecordingSettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
ListPreference idleDuration = findPreference(getString(R.string.idle_duration_key));
|
||||
idleDuration.setEntries(PreferencesUtils.getIdleDurationEntries());
|
||||
|
||||
ListPreference autoPauseCadence0Speed = findPreference(getString(R.string.settings_recording_auto_pause_on_cadence_0_speed_key));
|
||||
if (autoPauseCadence0Speed != null) {
|
||||
autoPauseCadence0Speed.setEntries(PreferencesUtils.getAutoPauseCadence0SpeedEntries());
|
||||
}
|
||||
|
||||
ListPreference markerIdleDuration = findPreference(getString(R.string.marker_idle_duration_key));
|
||||
if (markerIdleDuration != null) {
|
||||
markerIdleDuration.setEntries(PreferencesUtils.getMarkerIdleDurationEntries());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-2
@@ -40,6 +40,13 @@ public class RecordingLayoutIO {
|
||||
Log.e(TAG, "Invalid CSV layout. It shouldn't happen: " + csvLine);
|
||||
return recordingLayout;
|
||||
}
|
||||
|
||||
String key = fieldParts[0];
|
||||
if (key.equals(resources.getString(R.string.stats_custom_layout_sensor_status_key))) {
|
||||
// Ignore deprecated sensor_status key to safely remove it from user's custom layout
|
||||
continue;
|
||||
}
|
||||
|
||||
recordingLayout.addField(fromCSV(fieldParts, resources));
|
||||
}
|
||||
return recordingLayout;
|
||||
@@ -50,11 +57,24 @@ public class RecordingLayoutIO {
|
||||
}
|
||||
|
||||
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(
|
||||
fieldParts[0],
|
||||
key,
|
||||
YES_VALUE.equals(fieldParts[1]),
|
||||
YES_VALUE.equals(fieldParts[2]),
|
||||
fieldParts[0].equals(resources.getString(R.string.stats_custom_layout_coordinates_key)));
|
||||
isWide);
|
||||
}
|
||||
|
||||
static String toCsv(DataField datafield) {
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ public class SettingsCustomLayoutEditAdapter extends RecyclerView.Adapter<Recycl
|
||||
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;
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
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.Speed;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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_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_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;
|
||||
}
|
||||
|
||||
@@ -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_title">Maksymalna odległość</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_required_accuracy_title">Dokładność GPS-u</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_mile_hour">%1$d mil/h</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_none">Brak</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="stats_sensors_heart_rate">Tętno</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_layout_reset_confirm_title">Zresetować twoje układy\?</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="track_delete_number_of_tracks">%1$d ścieżki</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_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>
|
||||
@@ -585,4 +593,15 @@ limitations under the License.
|
||||
<string name="show_on_dashboard">API panelu OpenTracks</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="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>
|
||||
|
||||
@@ -128,6 +128,14 @@
|
||||
<string name="idle_duration_key" translatable="false">idleSpeedDuration</string>
|
||||
<string name="idle_duration_default" translatable="false">10</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">
|
||||
<item>@string/idle_duration_off</item>
|
||||
<item>5</item>
|
||||
@@ -139,6 +147,17 @@
|
||||
<item>120</item>
|
||||
</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_default" translatable="false">@string/stats_rate_speed_or_pace_default</string>
|
||||
<string-array name="stats_rate_metric_options">
|
||||
@@ -243,6 +262,12 @@
|
||||
<bool name="voice_on_device_speaker_default" translatable="false">true</bool>
|
||||
<string name="voice_announce_idle_key" translatable="false">voice_announce_idle_key</string>
|
||||
<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>
|
||||
<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_power_key" translatable="false">power</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 -->
|
||||
<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_announcements_idle_category_title">Idle Announcements</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_summary">Time, Distance, Voice Speed</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_title">Max distance</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_title">GPS accuracy</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_power">Power</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 -->
|
||||
<string name="track_name_format">Track %1$d</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_knots_recommended">%1$.1f knots (recommended)</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_none">None</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_int_seconds">%1$d s (recommended)</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">
|
||||
{n, plural,
|
||||
=1 {1 hour}
|
||||
@@ -705,4 +723,22 @@ limitations under the License.
|
||||
<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">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>
|
||||
|
||||
@@ -22,6 +22,21 @@
|
||||
android:defaultValue="@bool/voice_announce_idle_default"
|
||||
android:key="@string/voice_announce_idle_key"
|
||||
android:title="@string/settings_announcements_idle_title" />
|
||||
<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 android:title="@string/settings_announcements_statistics_title">
|
||||
|
||||
@@ -4,6 +4,25 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
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
|
||||
android:defaultValue="@string/recording_distance_interval_default"
|
||||
android:entryValues="@array/recording_distance_interval_values"
|
||||
@@ -19,8 +38,17 @@
|
||||
<ListPreference
|
||||
android:defaultValue="@string/idle_duration_default"
|
||||
android:entryValues="@array/idle_duration_values"
|
||||
android:entries="@array/idle_duration_values"
|
||||
android:key="@string/idle_duration_key"
|
||||
android:title="@string/settings_recording_idle_timeout_title"
|
||||
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>
|
||||
Reference in New Issue
Block a user