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:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user