forked from upstream-mirrors/OpenTracks
Merge pull request #444 from OpenTracksApp/cyclingPower
Bluetooth Sensor: add cycling powermeter.
This commit is contained in:
@@ -26,3 +26,13 @@ However, often only one value is provided.
|
|||||||
* Wahoo Cadence (Model: WFPODCAD2)
|
* Wahoo Cadence (Model: WFPODCAD2)
|
||||||
This sensor reports cadence data as speed.
|
This sensor reports cadence data as speed.
|
||||||
A workaround is in place.
|
A workaround is in place.
|
||||||
|
|
||||||
|
## Cycling Power Measurement
|
||||||
|
|
||||||
|
We only support _Instantaneous Power_.
|
||||||
|
|
||||||
|
* QUARQ Red DZero Powermeter
|
||||||
|
* Wahoo Kickr v4.0
|
||||||
|
WARNING: we only support physically movements with valid GPS coordinates.
|
||||||
|
Using a device indoors (no GPS available) will only show the current power values, but won't store anything.
|
||||||
|
Also the distance is not computed.
|
||||||
+29
@@ -9,6 +9,7 @@ import de.dennisguse.opentracks.util.UintUtils;
|
|||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class SensorDataCyclingTest {
|
public class SensorDataCyclingTest {
|
||||||
@@ -117,4 +118,32 @@ public class SensorDataCyclingTest {
|
|||||||
// then
|
// then
|
||||||
assertEquals(2, current.getSpeed_mps(), 0.01);
|
assertEquals(2, current.getSpeed_mps(), 0.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void equals_speed_with_no_data() {
|
||||||
|
// given
|
||||||
|
SensorDataCycling.Speed previous = new SensorDataCycling.Speed("sensorAddress");
|
||||||
|
SensorDataCycling.Speed current = new SensorDataCycling.Speed("sensorAddress", "sensorName", 0, 2048);
|
||||||
|
|
||||||
|
// when
|
||||||
|
previous.toString();
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertNotEquals(previous, current);
|
||||||
|
assertNotEquals(previous, previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void equals_cadence_with_no_data() {
|
||||||
|
// given
|
||||||
|
SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress");
|
||||||
|
SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 0, 2048);
|
||||||
|
|
||||||
|
// when
|
||||||
|
previous.toString();
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertNotEquals(previous, current);
|
||||||
|
assertNotEquals(previous, previous);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -75,4 +75,16 @@ public class BluetoothUtilsTest {
|
|||||||
assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
|
assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
|
||||||
assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount());
|
assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseCyclingPower_power() {
|
||||||
|
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER_MEASUREMENT_CHAR_UUID, 0, 0);
|
||||||
|
characteristic.setValue(new byte[]{0, 0, 40, 0});
|
||||||
|
|
||||||
|
// when
|
||||||
|
int power_w = BluetoothUtils.parseCyclingPower(characteristic);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals(40, power_w);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package de.dennisguse.opentracks.adapters;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Pair;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.R;
|
||||||
|
import de.dennisguse.opentracks.content.sensor.SensorData;
|
||||||
|
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||||
|
import de.dennisguse.opentracks.content.sensor.SensorDataCyclingPower;
|
||||||
|
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
|
||||||
|
import de.dennisguse.opentracks.util.StringUtils;
|
||||||
|
|
||||||
|
public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||||
|
|
||||||
|
public static final int HEART_RATE_TYPE = 0;
|
||||||
|
public static final int CADENCE_TYPE = 1;
|
||||||
|
public static final int POWER_TYPE = 2;
|
||||||
|
|
||||||
|
private List<Pair<Integer, SensorData>> sensorDataList;
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
public SensorsAdapter(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.sensor_item, parent, false);
|
||||||
|
return new SensorsAdapter.ViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||||
|
SensorsAdapter.ViewHolder viewHolder = (SensorsAdapter.ViewHolder) holder;
|
||||||
|
int type = sensorDataList.get(position).first;
|
||||||
|
SensorData sensorData = sensorDataList.get(position).second;
|
||||||
|
viewHolder.setData(sensorData, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
if (sensorDataList == null) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return sensorDataList.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemViewType(int position) {
|
||||||
|
return sensorDataList.get(position).first;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Pair<Integer, SensorData>> swapData(List<Pair<Integer, SensorData>> data) {
|
||||||
|
if (sensorDataList == data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
sensorDataList = data;
|
||||||
|
|
||||||
|
if (data != null) {
|
||||||
|
this.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
TextView label;
|
||||||
|
TextView sensorValue;
|
||||||
|
TextView value;
|
||||||
|
TextView unit;
|
||||||
|
|
||||||
|
public ViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
label = itemView.findViewById(R.id.stats_sensor_label);
|
||||||
|
sensorValue = itemView.findViewById(R.id.stats_sensor_sensor_value);
|
||||||
|
value = itemView.findViewById(R.id.stats_sensor_value);
|
||||||
|
unit = itemView.findViewById(R.id.stats_sensor_unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setData(SensorData sensorData, int type) {
|
||||||
|
switch (type) {
|
||||||
|
case HEART_RATE_TYPE:
|
||||||
|
setHeartRateSensorData((SensorDataHeartRate) sensorData);
|
||||||
|
break;
|
||||||
|
case CADENCE_TYPE:
|
||||||
|
setCadenceSensorData((SensorDataCycling.Cadence) sensorData);
|
||||||
|
break;
|
||||||
|
case POWER_TYPE:
|
||||||
|
setPowerSensorData((SensorDataCyclingPower) sensorData);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new RuntimeException("Unknown sensor type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setHeartRateSensorData(SensorDataHeartRate data) {
|
||||||
|
String sensorValue = context.getString(R.string.value_unknown);
|
||||||
|
String sensorName = context.getString(R.string.value_unknown);
|
||||||
|
if (data != null) {
|
||||||
|
sensorName = data.getSensorNameOrAddress();
|
||||||
|
if (data.hasHeartRate_bpm() && data.isRecent()) {
|
||||||
|
sensorValue = StringUtils.formatDecimal(data.getHeartRate_bpm(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.label.setText(context.getString(R.string.sensor_state_heart_rate));
|
||||||
|
this.sensorValue.setText(sensorName);
|
||||||
|
this.value.setText(sensorValue);
|
||||||
|
this.unit.setText(R.string.sensor_unit_beats_per_minute);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCadenceSensorData(SensorDataCycling.Cadence data) {
|
||||||
|
String sensorValue = context.getString(R.string.value_unknown);
|
||||||
|
String sensorName = context.getString(R.string.value_unknown);
|
||||||
|
if (data != null) {
|
||||||
|
sensorName = data.getSensorNameOrAddress();
|
||||||
|
|
||||||
|
if (data.hasCadence_rpm() && data.isRecent()) {
|
||||||
|
sensorValue = StringUtils.formatDecimal(data.getCadence_rpm(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.label.setText(context.getString(R.string.sensor_state_cadence));
|
||||||
|
this.sensorValue.setText(sensorName);
|
||||||
|
this.value.setText(sensorValue);
|
||||||
|
this.unit.setText(R.string.sensor_unit_rounds_per_minute);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPowerSensorData(SensorDataCyclingPower data) {
|
||||||
|
String sensorValue = context.getString(R.string.value_unknown);
|
||||||
|
String sensorName = context.getString(R.string.value_unknown);
|
||||||
|
if (data != null) {
|
||||||
|
sensorName = data.getSensorNameOrAddress();
|
||||||
|
|
||||||
|
if (data.hasPower_w() && data.isRecent()) {
|
||||||
|
sensorValue = StringUtils.formatDecimal(data.getPower_w(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.label.setText(context.getString(R.string.sensor_state_power));
|
||||||
|
this.sensorValue.setText(sensorName);
|
||||||
|
this.value.setText(sensorValue);
|
||||||
|
this.unit.setText(R.string.sensor_unit_power);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ public final class SensorDataCycling {
|
|||||||
* Workaround for Wahoo CADENCE: provides speed instead of cadence
|
* Workaround for Wahoo CADENCE: provides speed instead of cadence
|
||||||
*/
|
*/
|
||||||
public Cadence(@NonNull SensorDataCycling.Speed speed) {
|
public Cadence(@NonNull SensorDataCycling.Speed speed) {
|
||||||
this(speed.getSensorAddress(), speed.getSensorName(), speed.getWheelRevolutionsCount(), speed.getWheelRevolutionsTime());
|
this(speed.getSensorAddress(), speed.getSensorName(), speed.wheelRevolutionsCount, speed.wheelRevolutionsTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasData() {
|
public boolean hasData() {
|
||||||
@@ -82,7 +82,7 @@ public final class SensorDataCycling {
|
|||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "cadence=" + getCadence_rpm() + " time=" + getCrankRevolutionsTime() + " count=" + getCrankRevolutionsCount();
|
return "cadence=" + cadence_rpm + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -90,7 +90,11 @@ public final class SensorDataCycling {
|
|||||||
if (!(obj instanceof Cadence)) return false;
|
if (!(obj instanceof Cadence)) return false;
|
||||||
|
|
||||||
Cadence comp = (Cadence) obj;
|
Cadence comp = (Cadence) obj;
|
||||||
return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime();
|
if (hasData() && comp.hasData() == hasData()) {
|
||||||
|
return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +153,7 @@ public final class SensorDataCycling {
|
|||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "speed=" + getSpeed_mps() + " time=" + getWheelRevolutionsTime() + " count=" + getWheelRevolutionsCount();
|
return "speed=" + speed_mps + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -157,7 +161,11 @@ public final class SensorDataCycling {
|
|||||||
if (!(obj instanceof Speed)) return false;
|
if (!(obj instanceof Speed)) return false;
|
||||||
|
|
||||||
Speed comp = (Speed) obj;
|
Speed comp = (Speed) obj;
|
||||||
return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime();
|
if (hasData() && comp.hasData() == hasData()) {
|
||||||
|
return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package de.dennisguse.opentracks.content.sensor;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class SensorDataCyclingPower extends SensorData {
|
||||||
|
|
||||||
|
private final Float power_w;
|
||||||
|
|
||||||
|
public SensorDataCyclingPower(String address) {
|
||||||
|
super(address);
|
||||||
|
this.power_w = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SensorDataCyclingPower(String name, String address, float power_w) {
|
||||||
|
super(name, address);
|
||||||
|
this.power_w = power_w;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasPower_w() {
|
||||||
|
return power_w != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getPower_w() {
|
||||||
|
return power_w;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "power=" + power_w;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ public final class SensorDataSet {
|
|||||||
|
|
||||||
private SensorDataCycling.Speed cyclingSpeed;
|
private SensorDataCycling.Speed cyclingSpeed;
|
||||||
|
|
||||||
|
private SensorDataCyclingPower cyclingPower;
|
||||||
|
|
||||||
public SensorDataSet() {
|
public SensorDataSet() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +29,10 @@ public final class SensorDataSet {
|
|||||||
return cyclingSpeed;
|
return cyclingSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SensorDataCyclingPower getCyclingPower() {
|
||||||
|
return cyclingPower;
|
||||||
|
}
|
||||||
|
|
||||||
public void set(SensorData data) {
|
public void set(SensorData data) {
|
||||||
set(data, data);
|
set(data, data);
|
||||||
}
|
}
|
||||||
@@ -39,6 +45,7 @@ public final class SensorDataSet {
|
|||||||
this.heartRate = null;
|
this.heartRate = null;
|
||||||
this.cyclingCadence = null;
|
this.cyclingCadence = null;
|
||||||
this.cyclingSpeed = null;
|
this.cyclingSpeed = null;
|
||||||
|
this.cyclingPower = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void fillTrackPoint(TrackPoint trackPoint) {
|
public void fillTrackPoint(TrackPoint trackPoint) {
|
||||||
@@ -53,6 +60,10 @@ public final class SensorDataSet {
|
|||||||
if (cyclingSpeed != null && cyclingSpeed.hasSpeed_mps()) {
|
if (cyclingSpeed != null && cyclingSpeed.hasSpeed_mps()) {
|
||||||
trackPoint.setSpeed(cyclingSpeed.getSpeed_mps());
|
trackPoint.setSpeed(cyclingSpeed.getSpeed_mps());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cyclingPower != null && cyclingPower.hasPower_w()) {
|
||||||
|
trackPoint.setPower(cyclingPower.getPower_w());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@@ -60,10 +71,11 @@ public final class SensorDataSet {
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
return (getHeartRate() != null ? "" + getHeartRate() : "")
|
return (getHeartRate() != null ? "" + getHeartRate() : "")
|
||||||
+ (getCyclingCadence() != null ? " " + getCyclingCadence() : "")
|
+ (getCyclingCadence() != null ? " " + getCyclingCadence() : "")
|
||||||
+ (getCyclingSpeed() != null ? " " + getCyclingSpeed() : "");
|
+ (getCyclingSpeed() != null ? " " + getCyclingSpeed() : "")
|
||||||
|
+ (getCyclingPower() != null ? " " + getCyclingPower() : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void set(SensorData type, SensorData data) {
|
private void set(@NonNull SensorData type, SensorData data) {
|
||||||
if (type instanceof SensorDataHeartRate) {
|
if (type instanceof SensorDataHeartRate) {
|
||||||
this.heartRate = (SensorDataHeartRate) data;
|
this.heartRate = (SensorDataHeartRate) data;
|
||||||
return;
|
return;
|
||||||
@@ -78,6 +90,11 @@ public final class SensorDataSet {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UnsupportedOperationException();
|
if (type instanceof SensorDataCyclingPower) {
|
||||||
|
this.cyclingPower = (SensorDataCyclingPower) data;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnsupportedOperationException(type.getClass().getCanonicalName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,16 +13,22 @@ import android.view.ViewGroup;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.TrackRecordingActivity;
|
import de.dennisguse.opentracks.TrackRecordingActivity;
|
||||||
|
import de.dennisguse.opentracks.adapters.SensorsAdapter;
|
||||||
import de.dennisguse.opentracks.content.TrackDataHub;
|
import de.dennisguse.opentracks.content.TrackDataHub;
|
||||||
import de.dennisguse.opentracks.content.TrackDataListener;
|
import de.dennisguse.opentracks.content.TrackDataListener;
|
||||||
import de.dennisguse.opentracks.content.data.Marker;
|
import de.dennisguse.opentracks.content.data.Marker;
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||||
|
import de.dennisguse.opentracks.content.sensor.SensorData;
|
||||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||||
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
|
|
||||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||||
import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
|
import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||||
@@ -60,6 +66,9 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
|
|
||||||
private StatisticsRecordingBinding viewBinding;
|
private StatisticsRecordingBinding viewBinding;
|
||||||
|
|
||||||
|
private SensorsAdapter sensorsAdapter;
|
||||||
|
private RecyclerView sensorsRecyclerView;
|
||||||
|
|
||||||
public static Fragment newInstance() {
|
public static Fragment newInstance() {
|
||||||
return new StatisticsRecordingFragment();
|
return new StatisticsRecordingFragment();
|
||||||
}
|
}
|
||||||
@@ -114,6 +123,11 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
sensorsAdapter = new SensorsAdapter(getContext());
|
||||||
|
sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
|
||||||
|
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
|
||||||
|
sensorsRecyclerView.setAdapter(sensorsAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -271,55 +285,23 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
} else {
|
} else {
|
||||||
SensorDataSet sensorDataSet = trackRecordingService.getSensorData();
|
SensorDataSet sensorDataSet = trackRecordingService.getSensorData();
|
||||||
if (sensorDataSet != null) {
|
if (sensorDataSet != null) {
|
||||||
setHeartRateSensorData(sensorDataSet);
|
List<Pair<Integer, SensorData>> sensorDataList = new ArrayList<>();
|
||||||
setCadenceSensorData(sensorDataSet);
|
if (sensorDataSet.getHeartRate() != null) {
|
||||||
setSpeedSensorData(sensorDataSet, isSelectedTrackRecording());
|
sensorDataList.add(new Pair<>(SensorsAdapter.HEART_RATE_TYPE, sensorDataSet.getHeartRate()));
|
||||||
|
}
|
||||||
|
if (sensorDataSet.getCyclingCadence() != null) {
|
||||||
|
sensorDataList.add(new Pair<>(SensorsAdapter.CADENCE_TYPE, sensorDataSet.getCyclingCadence()));
|
||||||
|
}
|
||||||
|
if(sensorDataSet.getCyclingPower() != null) {
|
||||||
|
sensorDataList.add(new Pair<>(SensorsAdapter.POWER_TYPE, sensorDataSet.getCyclingPower()));
|
||||||
|
}
|
||||||
|
sensorsAdapter.swapData(sensorDataList);
|
||||||
|
setSpeedSensorData(sensorDataSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTotalElevationGain(trackRecordingService.getElevationGain_m());
|
setTotalElevationGain(trackRecordingService.getElevationGain_m());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setHeartRateSensorData(SensorDataSet sensorDataSet) {
|
|
||||||
int isVisible = sensorDataSet.getHeartRate() != null ? View.VISIBLE : View.GONE;
|
|
||||||
viewBinding.statsSensorHeartRateGroup.setVisibility(isVisible);
|
|
||||||
setVisibilitySensorHorizontalLine();
|
|
||||||
|
|
||||||
String sensorValue = getContext().getString(R.string.value_unknown);
|
|
||||||
String sensorName = getContext().getString(R.string.value_unknown);
|
|
||||||
if (sensorDataSet.getHeartRate() != null) {
|
|
||||||
SensorDataHeartRate data = sensorDataSet.getHeartRate();
|
|
||||||
|
|
||||||
sensorName = data.getSensorNameOrAddress();
|
|
||||||
if (data.hasHeartRate_bpm() && data.isRecent()) {
|
|
||||||
sensorValue = StringUtils.formatDecimal(data.getHeartRate_bpm(), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewBinding.statsSensorHeartRateSensorValue.setText(sensorName);
|
|
||||||
viewBinding.statsSensorHeartRateValue.setText(sensorValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setCadenceSensorData(SensorDataSet sensorDataSet) {
|
|
||||||
int isVisible = sensorDataSet.getCyclingCadence() != null ? View.VISIBLE : View.GONE;
|
|
||||||
viewBinding.statsSensorCadenceGroup.setVisibility(isVisible);
|
|
||||||
setVisibilitySensorHorizontalLine();
|
|
||||||
|
|
||||||
String sensorValue = getContext().getString(R.string.value_unknown);
|
|
||||||
String sensorName = getContext().getString(R.string.value_unknown);
|
|
||||||
if (sensorDataSet.getCyclingCadence() != null) {
|
|
||||||
SensorDataCycling.Cadence data = sensorDataSet.getCyclingCadence();
|
|
||||||
sensorName = data.getSensorNameOrAddress();
|
|
||||||
|
|
||||||
if (data.hasCadence_rpm() && data.isRecent()) {
|
|
||||||
sensorValue = StringUtils.formatDecimal(data.getCadence_rpm(), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewBinding.statsSensorCadenceSensorValue.setText(sensorName);
|
|
||||||
viewBinding.statsSensorCadenceValue.setText(sensorValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set elevation gain
|
// Set elevation gain
|
||||||
private void setTotalElevationGain(Float elevationGain_m) {
|
private void setTotalElevationGain(Float elevationGain_m) {
|
||||||
//TODO Check if we can distribute the total elevation gain via trackStatistics instead of doing some computation in the UI layer.
|
//TODO Check if we can distribute the total elevation gain via trackStatistics instead of doing some computation in the UI layer.
|
||||||
@@ -340,23 +322,12 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
viewBinding.statsElevationGainUnit.setText(parts.second);
|
viewBinding.statsElevationGainUnit.setText(parts.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void setSpeedSensorData(SensorDataSet sensorDataSet) {
|
||||||
* If cadence and hear rate groups are invisible then sensor horizontal line hast to be invisible too.
|
if (sensorDataSet != null && sensorDataSet.getCyclingSpeed() != null) {
|
||||||
*/
|
SensorDataCycling.Speed data = sensorDataSet.getCyclingSpeed();
|
||||||
private void setVisibilitySensorHorizontalLine() {
|
|
||||||
if (viewBinding.statsSensorCadenceGroup.getVisibility() != View.VISIBLE && viewBinding.statsSensorHeartRateGroup.getVisibility() != View.VISIBLE) {
|
|
||||||
viewBinding.statsSensorHorizontalLine.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setSpeedSensorData(SensorDataSet sensorDataSet, boolean isRecording) {
|
if (data.hasSpeed_mps() && data.isRecent()) {
|
||||||
if (isRecording) {
|
setSpeed(data.getSpeed_mps());
|
||||||
if (sensorDataSet != null && sensorDataSet.getCyclingSpeed() != null) {
|
|
||||||
SensorDataCycling.Speed data = sensorDataSet.getCyclingSpeed();
|
|
||||||
|
|
||||||
if (data.hasSpeed_mps() && data.isRecent()) {
|
|
||||||
setSpeed(data.getSpeed_mps());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-2
@@ -32,6 +32,7 @@ import java.util.UUID;
|
|||||||
|
|
||||||
import de.dennisguse.opentracks.content.sensor.SensorData;
|
import de.dennisguse.opentracks.content.sensor.SensorData;
|
||||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||||
|
import de.dennisguse.opentracks.content.sensor.SensorDataCyclingPower;
|
||||||
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
|
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
|
||||||
import de.dennisguse.opentracks.util.BluetoothUtils;
|
import de.dennisguse.opentracks.util.BluetoothUtils;
|
||||||
|
|
||||||
@@ -88,8 +89,14 @@ public abstract class BluetoothConnectionManager {
|
|||||||
|
|
||||||
// Register for updates.
|
// Register for updates.
|
||||||
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(BluetoothUtils.CLIENT_CHARACTERISTIC_CONFIG_UUID);
|
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(BluetoothUtils.CLIENT_CHARACTERISTIC_CONFIG_UUID);
|
||||||
|
if (descriptor == null) {
|
||||||
|
Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID characteristic not available; cannot request notifications for changed data.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
|
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
|
||||||
gatt.writeDescriptor(descriptor);
|
gatt.writeDescriptor(descriptor);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -170,7 +177,7 @@ public abstract class BluetoothConnectionManager {
|
|||||||
public static class CyclingCadence extends BluetoothConnectionManager {
|
public static class CyclingCadence extends BluetoothConnectionManager {
|
||||||
|
|
||||||
CyclingCadence(SensorDataObserver observer) {
|
CyclingCadence(SensorDataObserver observer) {
|
||||||
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
|
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -201,7 +208,7 @@ public abstract class BluetoothConnectionManager {
|
|||||||
public static class CyclingSpeed extends BluetoothConnectionManager {
|
public static class CyclingSpeed extends BluetoothConnectionManager {
|
||||||
|
|
||||||
CyclingSpeed(SensorDataObserver observer) {
|
CyclingSpeed(SensorDataObserver observer) {
|
||||||
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
|
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -219,6 +226,25 @@ public abstract class BluetoothConnectionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class CyclingPower extends BluetoothConnectionManager {
|
||||||
|
|
||||||
|
CyclingPower(@NonNull SensorDataObserver observer) {
|
||||||
|
super(BluetoothUtils.CYCLING_POWER_UUID, BluetoothUtils.CYCLING_POWER_MEASUREMENT_CHAR_UUID, observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SensorData createEmptySensorData(String address) {
|
||||||
|
return new SensorDataCyclingPower(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SensorDataCyclingPower parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||||
|
Integer cyclingPower = BluetoothUtils.parseCyclingPower(characteristic);
|
||||||
|
|
||||||
|
return cyclingPower != null ? new SensorDataCyclingPower(address, sensorName, cyclingPower) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface SensorDataObserver {
|
interface SensorDataObserver {
|
||||||
|
|
||||||
void onChanged(SensorData sensorData);
|
void onChanged(SensorData sensorData);
|
||||||
|
|||||||
+9
@@ -59,6 +59,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
|
|||||||
private final BluetoothConnectionManager.HeartRate heartRate = new BluetoothConnectionManager.HeartRate(this);
|
private final BluetoothConnectionManager.HeartRate heartRate = new BluetoothConnectionManager.HeartRate(this);
|
||||||
private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this);
|
private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this);
|
||||||
private final BluetoothConnectionManager.CyclingSpeed cyclingSpeed = new BluetoothConnectionManager.CyclingSpeed(this);
|
private final BluetoothConnectionManager.CyclingSpeed cyclingSpeed = new BluetoothConnectionManager.CyclingSpeed(this);
|
||||||
|
private final BluetoothConnectionManager.CyclingPower cyclingPower = new BluetoothConnectionManager.CyclingPower(this);
|
||||||
|
|
||||||
private final SensorDataSet sensorDataSet = new SensorDataSet();
|
private final SensorDataSet sensorDataSet = new SensorDataSet();
|
||||||
|
|
||||||
@@ -82,6 +83,12 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
|
|||||||
|
|
||||||
connect(cyclingSpeed, address);
|
connect(cyclingSpeed, address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_power_key, key)) {
|
||||||
|
String address = PreferencesUtils.getBluetoothCyclingPowerSensorAddress(context);
|
||||||
|
|
||||||
|
connect(cyclingPower, address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,6 +109,8 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
|
|||||||
public synchronized void stop() {
|
public synchronized void stop() {
|
||||||
heartRate.disconnect();
|
heartRate.disconnect();
|
||||||
cyclingCadence.disconnect();
|
cyclingCadence.disconnect();
|
||||||
|
cyclingSpeed.disconnect();
|
||||||
|
cyclingPower.disconnect();
|
||||||
|
|
||||||
sensorDataSet.clear();
|
sensorDataSet.clear();
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import de.dennisguse.opentracks.R;
|
|||||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||||
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference;
|
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference;
|
||||||
|
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingPowerPreference;
|
||||||
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeHeartRatePreference;
|
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeHeartRatePreference;
|
||||||
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeSensorPreference;
|
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeSensorPreference;
|
||||||
import de.dennisguse.opentracks.util.ActivityUtils;
|
import de.dennisguse.opentracks.util.ActivityUtils;
|
||||||
@@ -132,6 +133,8 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
|
|||||||
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.HEART_RATE_SERVICE_UUID);
|
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.HEART_RATE_SERVICE_UUID);
|
||||||
} else if (preference instanceof BluetoothLeCyclingCadenceAndSpeedPreference) {
|
} else if (preference instanceof BluetoothLeCyclingCadenceAndSpeedPreference) {
|
||||||
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID);
|
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID);
|
||||||
|
} else if (preference instanceof BluetoothLeCyclingPowerPreference) {
|
||||||
|
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.CYCLING_POWER_UUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dialogFragment != null) {
|
if (dialogFragment != null) {
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package de.dennisguse.opentracks.settings.bluetooth;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreference {
|
||||||
|
|
||||||
|
public BluetoothLeCyclingPowerPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BluetoothLeCyclingPowerPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BluetoothLeCyclingPowerPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BluetoothLeCyclingPowerPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,8 +39,11 @@ public class BluetoothUtils {
|
|||||||
public static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL);
|
public static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL);
|
||||||
public static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
|
public static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
|
||||||
|
|
||||||
|
public static final UUID CYCLING_POWER_UUID = new UUID(0x181800001000L, 0x800000805f9b34fbL);
|
||||||
|
public static final UUID CYCLING_POWER_MEASUREMENT_CHAR_UUID = new UUID(0x2A6300001000L, 0x800000805f9b34fbL);
|
||||||
|
|
||||||
public static final UUID CYCLING_SPEED_CADENCE_SERVICE_UUID = new UUID(0x181600001000L, 0x800000805f9b34fbL);
|
public static final UUID CYCLING_SPEED_CADENCE_SERVICE_UUID = new UUID(0x181600001000L, 0x800000805f9b34fbL);
|
||||||
public static final UUID CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID = new UUID(0x2A5B00001000L, 0x800000805f9b34fbL);
|
public static final UUID CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID = new UUID(0x2A5B00001000L, 0x800000805f9b34fbL);
|
||||||
|
|
||||||
private static final String TAG = BluetoothUtils.class.getSimpleName();
|
private static final String TAG = BluetoothUtils.class.getSimpleName();
|
||||||
|
|
||||||
@@ -62,7 +65,7 @@ public class BluetoothUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Integer parseHeartRate(BluetoothGattCharacteristic characteristic) {
|
public static Integer parseHeartRate(BluetoothGattCharacteristic characteristic) {
|
||||||
//DOCUMENTATION https://www.bluetooth.com/specifications/gatt/characteristics/
|
//DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.heart_rate_measurement.xml
|
||||||
byte[] raw = characteristic.getValue();
|
byte[] raw = characteristic.getValue();
|
||||||
if (raw.length == 0) {
|
if (raw.length == 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -79,10 +82,21 @@ public class BluetoothUtils {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Integer parseCyclingPower(BluetoothGattCharacteristic characteristic) {
|
||||||
|
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.cycling_power_measurement.xml
|
||||||
|
int valueLength = characteristic.getValue().length;
|
||||||
|
if (valueLength < 4) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, 2);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Documentation: https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=261449
|
* Documentation: https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=261449
|
||||||
*/
|
*/
|
||||||
public static SensorDataCycling.CadenceAndSpeed parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
|
public static SensorDataCycling.CadenceAndSpeed parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||||
|
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.csc_measurement.xml
|
||||||
int valueLength = characteristic.getValue().length;
|
int valueLength = characteristic.getValue().length;
|
||||||
if (valueLength == 0) {
|
if (valueLength == 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -262,6 +262,15 @@ public class PreferencesUtils {
|
|||||||
return getInt(context, R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, DEFAULT);
|
return getInt(context, R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, DEFAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isBluetoothCyclingPowerSensorAddressNone(Context context) {
|
||||||
|
return isBluetoothSensorAddressNone(context, getBluetoothCyclingPowerSensorAddress(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getBluetoothCyclingPowerSensorAddress(Context context) {
|
||||||
|
return getString(context, R.string.settings_sensor_bluetooth_cycling_power_key, getBluetoothSensorAddressNone(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static boolean shouldShowStatsOnLockscreen(Context context) {
|
public static boolean shouldShowStatsOnLockscreen(Context context) {
|
||||||
final boolean STATS_SHOW_ON_LOCKSCREEN_DEFAULT = context.getResources().getBoolean(R.bool.stats_show_on_lockscreen_while_recording_default);
|
final boolean STATS_SHOW_ON_LOCKSCREEN_DEFAULT = context.getResources().getBoolean(R.bool.stats_show_on_lockscreen_while_recording_default);
|
||||||
return getBoolean(context, R.string.stats_show_on_lockscreen_while_recording_key, STATS_SHOW_ON_LOCKSCREEN_DEFAULT);
|
return getBoolean(context, R.string.stats_show_on_lockscreen_while_recording_key, STATS_SHOW_ON_LOCKSCREEN_DEFAULT);
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/stats_sensor_label"
|
||||||
|
style="@style/StatsLargeLabel"
|
||||||
|
tools:text="@string/sensor_state_heart_rate"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/stats_sensor_sensor_value"
|
||||||
|
style="@style/StatsTinyValue"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/stats_sensor_label"
|
||||||
|
tools:text="90" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/stats_sensor_value"
|
||||||
|
style="@style/StatsLargeValue"
|
||||||
|
android:value="@string/value_unknown"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/stats_sensor_sensor_value"
|
||||||
|
tools:text="90" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/stats_sensor_unit"
|
||||||
|
style="@style/StatsUnit"
|
||||||
|
tools:text="@string/sensor_unit_beats_per_minute"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/stats_sensor_value" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -403,89 +403,14 @@
|
|||||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/stats_location_barrier" />
|
app:layout_constraintTop_toBottomOf="@+id/stats_location_barrier" />
|
||||||
|
|
||||||
<!-- Heart rate -->
|
<!-- Sensors data -->
|
||||||
<androidx.constraintlayout.widget.Group
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/stats_sensor_heart_rate_group"
|
android:id="@+id/stats_sensors_recycler_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:constraint_referenced_ids="stats_sensor_heart_rate_label,stats_sensor_heart_rate_sensor_value,stats_sensor_heart_rate_value,stats_sensor_heart_rate_unit" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/stats_sensor_heart_rate_label"
|
|
||||||
style="@style/StatsLargeLabel"
|
|
||||||
android:text="@string/sensor_state_heart_rate"
|
|
||||||
app:layout_constrainedWidth="true"
|
|
||||||
app:layout_constraintLeft_toRightOf="@+id/guideline"
|
app:layout_constraintLeft_toRightOf="@+id/guideline"
|
||||||
app:layout_constraintRight_toLeftOf="@+id/guideline2"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/stats_sensor_horizontal_line" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/stats_sensor_heart_rate_sensor_value"
|
|
||||||
style="@style/StatsTinyValue"
|
|
||||||
android:text="@string/value_unknown"
|
|
||||||
app:layout_constraintLeft_toRightOf="@+id/guideline"
|
|
||||||
app:layout_constraintRight_toLeftOf="@+id/guideline2"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/stats_sensor_heart_rate_label"
|
|
||||||
tools:text="90" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/stats_sensor_heart_rate_value"
|
|
||||||
style="@style/StatsLargeValue"
|
|
||||||
android:value="@string/value_unknown"
|
|
||||||
app:layout_constraintLeft_toRightOf="@+id/guideline"
|
|
||||||
app:layout_constraintRight_toLeftOf="@+id/guideline2"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/stats_sensor_heart_rate_sensor_value"
|
|
||||||
tools:text="90" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/stats_sensor_heart_rate_unit"
|
|
||||||
style="@style/StatsUnit"
|
|
||||||
android:text="@string/sensor_unit_beats_per_minute"
|
|
||||||
app:layout_constraintLeft_toRightOf="@+id/guideline"
|
|
||||||
app:layout_constraintRight_toLeftOf="@+id/guideline2"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/stats_sensor_heart_rate_value" />
|
|
||||||
|
|
||||||
<!-- Cadence -->
|
|
||||||
<androidx.constraintlayout.widget.Group
|
|
||||||
android:id="@+id/stats_sensor_cadence_group"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:constraint_referenced_ids="stats_sensor_cadence_label,stats_sensor_cadence_sensor_value,stats_sensor_cadence_value,stats_sensor_cadence_unit" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/stats_sensor_cadence_label"
|
|
||||||
style="@style/StatsLargeLabel"
|
|
||||||
android:text="@string/sensor_state_cadence"
|
|
||||||
app:layout_constrainedWidth="true"
|
|
||||||
app:layout_constraintLeft_toRightOf="@+id/guideline2"
|
|
||||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
||||||
app:layout_constraintTop_toBottomOf="@id/stats_sensor_horizontal_line" />
|
app:layout_constraintTop_toBottomOf="@id/stats_sensor_horizontal_line" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/stats_sensor_cadence_sensor_value"
|
|
||||||
style="@style/StatsTinyValue"
|
|
||||||
android:text="@string/value_unknown"
|
|
||||||
app:layout_constraintLeft_toRightOf="@+id/guideline2"
|
|
||||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/stats_sensor_cadence_label"
|
|
||||||
tools:text="90" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/stats_sensor_cadence_value"
|
|
||||||
style="@style/StatsLargeValue"
|
|
||||||
android:value="@string/value_unknown"
|
|
||||||
app:layout_constraintLeft_toRightOf="@+id/guideline2"
|
|
||||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/stats_sensor_cadence_sensor_value"
|
|
||||||
tools:text="90" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/stats_sensor_cadence_unit"
|
|
||||||
style="@style/StatsUnit"
|
|
||||||
android:text="@string/sensor_unit_rounds_per_minute"
|
|
||||||
app:layout_constraintLeft_toRightOf="@+id/guideline2"
|
|
||||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/stats_sensor_cadence_value" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -22,10 +22,10 @@ limitations under the License.
|
|||||||
<color name="chart_heart_rate_fill">#40E88A70</color> <!-- transparent red -->
|
<color name="chart_heart_rate_fill">#40E88A70</color> <!-- transparent red -->
|
||||||
<color name="chart_pace_border">#FF0066FF</color> <!-- blue -->
|
<color name="chart_pace_border">#FF0066FF</color> <!-- blue -->
|
||||||
<color name="chart_pace_fill">#40C8E0FF</color> <!-- transparent blue -->
|
<color name="chart_pace_fill">#40C8E0FF</color> <!-- transparent blue -->
|
||||||
<color name="chart_power_border">#FF401465</color> <!-- purple -->
|
|
||||||
<color name="chart_power_fill">#40401465</color> <!-- transparent purple -->
|
|
||||||
<color name="chart_speed_border">#FF0066FF</color> <!-- blue -->
|
<color name="chart_speed_border">#FF0066FF</color> <!-- blue -->
|
||||||
<color name="chart_speed_fill">#40C8E0FF</color> <!-- transparent blue -->
|
<color name="chart_speed_fill">#40C8E0FF</color> <!-- transparent blue -->
|
||||||
|
<color name="chart_power_border">#FF7A26C0</color> <!-- purple -->
|
||||||
|
<color name="chart_power_fill">#407A26C0</color> <!-- transparent purple -->
|
||||||
|
|
||||||
<color name="colorAccent">@android:color/holo_orange_dark</color>
|
<color name="colorAccent">@android:color/holo_orange_dark</color>
|
||||||
<color name="colorAccentTransparent">#55FF8800</color>
|
<color name="colorAccentTransparent">#55FF8800</color>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ limitations under the License.
|
|||||||
|
|
||||||
<string name="sensor_unit_beats_per_minute" translatable="false">BPM</string>
|
<string name="sensor_unit_beats_per_minute" translatable="false">BPM</string>
|
||||||
<string name="sensor_unit_rounds_per_minute" translatable="false">RPM</string>
|
<string name="sensor_unit_rounds_per_minute" translatable="false">RPM</string>
|
||||||
|
<string name="sensor_unit_power" translatable="false">W</string>
|
||||||
|
|
||||||
<string name="value_unknown" translatable="false">-</string>
|
<string name="value_unknown" translatable="false">-</string>
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<string name="settings_sensor_bluetooth_heart_rate_key" translatable="false">bluetoothSensor</string>
|
<string name="settings_sensor_bluetooth_heart_rate_key" translatable="false">bluetoothSensor</string>
|
||||||
<string name="settings_sensor_bluetooth_cycling_cadence_key" translatable="false">bluetoothCyclingCadenceSensor</string>
|
<string name="settings_sensor_bluetooth_cycling_cadence_key" translatable="false">bluetoothCyclingCadenceSensor</string>
|
||||||
<string name="settings_sensor_bluetooth_cycling_speed_key" translatable="false">bluetoothCyclingSpeedSensor</string>
|
<string name="settings_sensor_bluetooth_cycling_speed_key" translatable="false">bluetoothCyclingSpeedSensor</string>
|
||||||
|
<string name="settings_sensor_bluetooth_cycling_power_key" translatable="false">bluetoothCyclingPowerSensor</string>
|
||||||
<string name="sensor_type_value_none" translatable="false">NONE</string>
|
<string name="sensor_type_value_none" translatable="false">NONE</string>
|
||||||
|
|
||||||
<string name="settings_sensor_bluetooth_cycling_speed_wheel_circumference_key" translatable="false">bluetoothCyclingSpeedWheelCircumference</string>
|
<string name="settings_sensor_bluetooth_cycling_speed_wheel_circumference_key" translatable="false">bluetoothCyclingSpeedWheelCircumference</string>
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ limitations under the License.
|
|||||||
android:defaultValue="@string/sensor_type_value_none"
|
android:defaultValue="@string/sensor_type_value_none"
|
||||||
android:key="@string/settings_sensor_bluetooth_cycling_cadence_key"
|
android:key="@string/settings_sensor_bluetooth_cycling_cadence_key"
|
||||||
android:title="@string/sensor_state_cadence" />
|
android:title="@string/sensor_state_cadence" />
|
||||||
|
<de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingPowerPreference
|
||||||
|
android:defaultValue="@string/sensor_type_value_none"
|
||||||
|
android:key="@string/settings_sensor_bluetooth_cycling_power_key"
|
||||||
|
android:title="@string/sensor_state_power" />
|
||||||
<de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference
|
<de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference
|
||||||
android:defaultValue="@string/sensor_type_value_none"
|
android:defaultValue="@string/sensor_type_value_none"
|
||||||
android:key="@string/settings_sensor_bluetooth_cycling_speed_key"
|
android:key="@string/settings_sensor_bluetooth_cycling_speed_key"
|
||||||
|
|||||||
Reference in New Issue
Block a user