mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-10-03 01:53:05 +02:00
Recording stats layout customizable.
Users can select the order, visibility and importance (primary or secondary) of all stats for recording layout from the Settings. Also, users can select the number of columns per row in the recording layout: 1, 2 or 3. Finally, show altitude/coordinate from settings have been removed (not needed anymore). Fixes #659. Fixes #302. Fixes #820.
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package de.dennisguse.opentracks.adapters;
|
||||
|
||||
enum CustomLayoutFieldType {
|
||||
SHORT(0),
|
||||
WIDE(1);
|
||||
|
||||
private final int value;
|
||||
|
||||
CustomLayoutFieldType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int value() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package de.dennisguse.opentracks.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
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.viewmodels.SensorDataModel;
|
||||
|
||||
public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
private List<SensorDataModel> 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;
|
||||
SensorDataModel sensorDataModel = sensorDataList.get(position);
|
||||
viewHolder.setData(sensorDataModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if (sensorDataList == null) {
|
||||
return 0;
|
||||
} else {
|
||||
return sensorDataList.size();
|
||||
}
|
||||
}
|
||||
|
||||
public List<SensorDataModel> swapData(List<SensorDataModel> data) {
|
||||
if (sensorDataList == data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
sensorDataList = data;
|
||||
|
||||
if (data != null) {
|
||||
this.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private class ViewHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView label;
|
||||
private final TextView sensorValue;
|
||||
private final TextView value;
|
||||
private final 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(SensorDataModel sensorDataModel) {
|
||||
String sensorName = sensorDataModel.getSensorName();
|
||||
String sensorValue = sensorDataModel.hasValue() ? sensorDataModel.getSensorValue() : context.getString(R.string.value_unknown);
|
||||
|
||||
this.label.setText(context.getString(sensorDataModel.getLabelId()));
|
||||
if (sensorName == null) {
|
||||
this.sensorValue.setVisibility(View.GONE);
|
||||
} else {
|
||||
this.sensorValue.setText(sensorName);
|
||||
}
|
||||
this.value.setText(sensorValue);
|
||||
this.unit.setText(context.getString(sensorDataModel.getUnitId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package de.dennisguse.opentracks.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.DataField;
|
||||
import de.dennisguse.opentracks.content.data.Layout;
|
||||
import de.dennisguse.opentracks.util.StatisticsUtils;
|
||||
|
||||
public class SettingsCustomLayoutAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
private Layout layout;
|
||||
private final Context context;
|
||||
private final SettingsCustomLayoutItemClickListener itemClickListener;
|
||||
|
||||
public SettingsCustomLayoutAdapter(Context context, SettingsCustomLayoutItemClickListener itemClickListener, Layout layout) {
|
||||
this.context = context;
|
||||
this.itemClickListener = itemClickListener;
|
||||
this.layout = layout;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.custom_stats_item, parent, false);
|
||||
return new SettingsCustomLayoutAdapter.ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
SettingsCustomLayoutAdapter.ViewHolder viewHolder = (SettingsCustomLayoutAdapter.ViewHolder) holder;
|
||||
DataField field = layout.getFields().get(position);
|
||||
viewHolder.itemView.setTag(field.getKey());
|
||||
viewHolder.title.setText(field.getTitle());
|
||||
viewHolder.value.setText(StatisticsUtils.emptyValue(context, field.getKey()));
|
||||
|
||||
viewHolder.title.setTextAppearance(context, field.isVisible() ? (field.isPrimary() ? R.style.StatsPrimaryLabel : R.style.StatsSecondaryLabel) : R.style.StatsLabelHidden);
|
||||
viewHolder.value.setTextAppearance(context, field.isVisible() ? (field.isPrimary() ? R.style.StatsPrimaryValue : R.style.StatsSecondaryValue) : R.style.StatsValueHidden);
|
||||
viewHolder.statusIcon.setVisibility(field.isVisible() ? View.GONE : View.VISIBLE);
|
||||
viewHolder.statusIcon.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.ic_baseline_visibility_off_24));
|
||||
viewHolder.moveIcon.setVisibility(field.isVisible() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if (layout == null) {
|
||||
return 0;
|
||||
} else {
|
||||
return layout.getFields().size();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return layout.getFields().get(position).isWide() ? CustomLayoutFieldType.WIDE.value() : CustomLayoutFieldType.SHORT.value();
|
||||
}
|
||||
|
||||
public boolean isItemWide(int position) {
|
||||
return getItemViewType(position) == CustomLayoutFieldType.WIDE.value();
|
||||
}
|
||||
|
||||
public DataField getItem(int position) {
|
||||
return layout.getFields().get(position);
|
||||
}
|
||||
|
||||
public void swapValues(Layout data) {
|
||||
this.layout = data;
|
||||
if (this.layout != null) {
|
||||
this.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public Layout move(int fromPosition, int toPosition) {
|
||||
layout.moveField(fromPosition, toPosition);
|
||||
notifyItemMoved(fromPosition, toPosition);
|
||||
return layout;
|
||||
}
|
||||
|
||||
private class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
final TextView title;
|
||||
final TextView value;
|
||||
final TextView unit;
|
||||
final ImageView statusIcon;
|
||||
final ImageView moveIcon;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
title = itemView.findViewById(R.id.stats_description_main);
|
||||
value = itemView.findViewById(R.id.stats_value);
|
||||
unit = itemView.findViewById(R.id.stats_unit);
|
||||
statusIcon = itemView.findViewById(R.id.stats_icon_show_status);
|
||||
moveIcon = itemView.findViewById(R.id.stats_iv_drag_indicator);
|
||||
itemView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
String statTitle = (String) view.getTag();
|
||||
Optional<DataField> optionalField = layout.getFields().stream().filter(f -> f.getKey().equals(statTitle)).findFirst();
|
||||
optionalField.ifPresent(itemClickListener::onSettingsCustomLayoutItemClicked);
|
||||
}
|
||||
}
|
||||
|
||||
public interface SettingsCustomLayoutItemClickListener {
|
||||
void onSettingsCustomLayoutItemClicked(@NonNull DataField field);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package de.dennisguse.opentracks.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
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.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.viewmodels.StatisticData;
|
||||
|
||||
public class StatisticsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
private List<StatisticData> statisticDataList;
|
||||
private final Context context;
|
||||
|
||||
public StatisticsAdapter(Context context) {
|
||||
this.context = context;
|
||||
this.statisticDataList = new ArrayList<>();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.stats_item, parent, false);
|
||||
return new StatisticsAdapter.ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
StatisticsAdapter.ViewHolder viewHolder = (StatisticsAdapter.ViewHolder) holder;
|
||||
StatisticData statisticData = statisticDataList.get(position);
|
||||
viewHolder.setData(statisticData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if (statisticDataList == null) {
|
||||
return 0;
|
||||
} else {
|
||||
return statisticDataList.size();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return statisticDataList.get(position).getField().isWide() ? CustomLayoutFieldType.WIDE.value() : CustomLayoutFieldType.SHORT.value();
|
||||
}
|
||||
|
||||
public boolean isItemWide(int position) {
|
||||
return getItemViewType(position) == CustomLayoutFieldType.WIDE.value();
|
||||
}
|
||||
|
||||
public List<StatisticData> swapData(List<StatisticData> data) {
|
||||
if (statisticDataList == data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
statisticDataList = data;
|
||||
|
||||
if (data != null) {
|
||||
this.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public static class WithRecordedLayout extends StatisticsAdapter {
|
||||
public WithRecordedLayout(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.stats_recorded_item, parent, false);
|
||||
return new StatisticsAdapter.ViewRecordedHolder(view);
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewHolder extends RecyclerView.ViewHolder {
|
||||
final TextView value;
|
||||
final TextView unit;
|
||||
final TextView descMain;
|
||||
final TextView descSecondary;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
value = itemView.findViewById(R.id.stats_value);
|
||||
unit = itemView.findViewById(R.id.stats_unit);
|
||||
descMain = itemView.findViewById(R.id.stats_description_main);
|
||||
descSecondary = itemView.findViewById(R.id.stats_description_secondary);
|
||||
}
|
||||
|
||||
public void setData(StatisticData statisticData) {
|
||||
if (statisticData == null) {
|
||||
return;
|
||||
}
|
||||
this.value.setText(statisticData.hasValue() ? statisticData.getValue() : context.getString(R.string.value_unknown));
|
||||
this.value.setTextAppearance(context, statisticData.getField().isPrimary() ? R.style.StatsPrimaryValue : R.style.StatsSecondaryValue);
|
||||
this.unit.setText(statisticData.getUnit());
|
||||
this.descMain.setText(statisticData.getField().getTitle());
|
||||
this.descMain.setTextAppearance(context, statisticData.getField().isPrimary() ? R.style.StatsPrimaryLabel : R.style.StatsSecondaryLabel);
|
||||
if (statisticData.hasDescription()) {
|
||||
this.descSecondary.setVisibility(View.VISIBLE);
|
||||
this.descSecondary.setText(statisticData.getDescription());
|
||||
} else {
|
||||
this.descSecondary.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewRecordedHolder extends ViewHolder {
|
||||
|
||||
public ViewRecordedHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
public void setData(StatisticData statisticData) {
|
||||
if (statisticData == null) {
|
||||
return;
|
||||
}
|
||||
this.value.setText(statisticData.hasValue() ? statisticData.getValue() : context.getString(R.string.value_unknown));
|
||||
this.unit.setText(statisticData.getUnit());
|
||||
this.descMain.setText(statisticData.getField().getTitle());
|
||||
if (statisticData.hasDescription()) {
|
||||
this.descSecondary.setVisibility(View.VISIBLE);
|
||||
this.descSecondary.setText(statisticData.getDescription());
|
||||
} else {
|
||||
this.descSecondary.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package de.dennisguse.opentracks.content.data;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.util.CsvConstants;
|
||||
|
||||
public class DataField {
|
||||
public static final String YES_VALUE = "1";
|
||||
public static final String NOT_VALUE = "0";
|
||||
|
||||
private final String key;
|
||||
private final String title;
|
||||
private boolean isVisible;
|
||||
private boolean isPrimary;
|
||||
private final boolean isWide;
|
||||
|
||||
public DataField(String key, String title, boolean isVisible, boolean isPrimary, boolean isWide) {
|
||||
this.key = key;
|
||||
this.title = title;
|
||||
this.isVisible = isVisible;
|
||||
this.isPrimary = isPrimary;
|
||||
this.isWide = isWide;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return isVisible;
|
||||
}
|
||||
|
||||
public void toggleVisibility() {
|
||||
isVisible = !isVisible;
|
||||
}
|
||||
|
||||
public boolean isPrimary() {
|
||||
return isPrimary;
|
||||
}
|
||||
|
||||
public void togglePrimary() {
|
||||
isPrimary = !isPrimary;
|
||||
}
|
||||
|
||||
public boolean isWide() {
|
||||
return isWide;
|
||||
}
|
||||
|
||||
public String toCsv() {
|
||||
String visible = this.isVisible ? YES_VALUE : NOT_VALUE;
|
||||
String primary = this.isPrimary ? YES_VALUE : NOT_VALUE;
|
||||
return key + CsvConstants.ITEM_SEPARATOR + visible + CsvConstants.ITEM_SEPARATOR + primary;
|
||||
}
|
||||
|
||||
public static String getTitleByKey(Context context, String key) {
|
||||
if (key.equals(context.getString(R.string.stats_custom_layout_total_time_key))) {
|
||||
return context.getString(R.string.stats_total_time);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_moving_time_key))) {
|
||||
return context.getString(R.string.stats_moving_time);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_distance_key))) {
|
||||
return context.getString(R.string.stats_distance);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_speed_key))) {
|
||||
return context.getString(R.string.stats_speed);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_pace_key))) {
|
||||
return context.getString(R.string.stats_pace);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_average_moving_speed_key))) {
|
||||
return context.getString(R.string.stats_average_moving_speed);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_average_speed_key))) {
|
||||
return context.getString(R.string.stats_average_speed);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_max_speed_key))) {
|
||||
return context.getString(R.string.stats_max_speed);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_average_moving_pace_key))) {
|
||||
return context.getString(R.string.stats_average_moving_pace);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_average_pace_key))) {
|
||||
return context.getString(R.string.stats_average_pace);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_fastest_pace_key))) {
|
||||
return context.getString(R.string.stats_fastest_pace);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_altitude_key))) {
|
||||
return context.getString(R.string.stats_altitude);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_gain_key))) {
|
||||
return context.getString(R.string.stats_gain);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_loss_key))) {
|
||||
return context.getString(R.string.stats_loss);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_coordinates_key))) {
|
||||
return context.getString(R.string.stats_coordinates);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_heart_rate_key))) {
|
||||
return context.getString(R.string.stats_sensors_heart_rate);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_cadence_key))) {
|
||||
return context.getString(R.string.stats_sensors_cadence);
|
||||
} else if (key.equals(context.getString(R.string.stats_custom_layout_power_key))) {
|
||||
return context.getString(R.string.stats_sensors_power);
|
||||
} else {
|
||||
throw new RuntimeException("It doesn't exists a field with key: " + key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package de.dennisguse.opentracks.content.data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Layout {
|
||||
private final String profile;
|
||||
private final List<DataField> dataFields = new ArrayList<>();
|
||||
|
||||
public Layout(String profile) {
|
||||
this.profile = profile;
|
||||
}
|
||||
|
||||
public void addField(String key, String title, boolean visible, boolean primary, boolean isWide) {
|
||||
dataFields.add(new DataField(key, title, visible, primary, isWide));
|
||||
}
|
||||
|
||||
public void addField(DataField dataField) {
|
||||
dataFields.add(dataField);
|
||||
}
|
||||
|
||||
public void addFields(List<DataField> dataFields) {
|
||||
this.dataFields.addAll(dataFields);
|
||||
}
|
||||
|
||||
public void removeField(DataField dataField) {
|
||||
dataFields.remove(dataField);
|
||||
}
|
||||
|
||||
public List<DataField> getFields() {
|
||||
return new ArrayList<>(dataFields);
|
||||
}
|
||||
|
||||
public void moveField(int from, int to) {
|
||||
DataField dataFieldToMove = dataFields.remove(from);
|
||||
dataFields.add(to, dataFieldToMove);
|
||||
}
|
||||
|
||||
public String getProfile() {
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
@@ -254,9 +254,9 @@ public class MarkerDetailFragment extends Fragment {
|
||||
|
||||
private void setLocation(boolean addShadow) {
|
||||
String value = "[" + getString(R.string.stats_latitude) + " "
|
||||
+ StringUtils.formatCoordinate(marker.getLatitude()) + ", "
|
||||
+ StringUtils.formatCoordinate(getContext(), marker.getLatitude()) + ", "
|
||||
+ getString(R.string.stats_longitude) + " "
|
||||
+ StringUtils.formatCoordinate(marker.getLongitude()) + "]";
|
||||
+ StringUtils.formatCoordinate(getContext(), marker.getLongitude()) + "]";
|
||||
|
||||
ListItemUtils.setTextView(getActivity(), viewBinding.markerDetailMarkerLocation, value, addShadow);
|
||||
}
|
||||
|
||||
@@ -31,12 +31,11 @@ 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.TrackRecordedActivity;
|
||||
import de.dennisguse.opentracks.adapters.SensorsAdapter;
|
||||
import de.dennisguse.opentracks.adapters.StatisticsAdapter;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.databinding.StatisticsRecordedBinding;
|
||||
@@ -45,7 +44,8 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
|
||||
import de.dennisguse.opentracks.viewmodels.StatisticData;
|
||||
import de.dennisguse.opentracks.viewmodels.StatisticDataBuilder;
|
||||
|
||||
/**
|
||||
* A fragment to display track statistics to the user for a recorded {@link Track}.
|
||||
@@ -69,7 +69,7 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
}
|
||||
|
||||
private SensorStatistics sensorStatistics;
|
||||
private SensorsAdapter sensorsAdapter;
|
||||
private StatisticsAdapter.WithRecordedLayout sensorsAdapter;
|
||||
|
||||
private Track.Id trackId;
|
||||
@Nullable // Lazily loaded.
|
||||
@@ -114,7 +114,7 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
|
||||
|
||||
sensorsAdapter = new SensorsAdapter(getContext());
|
||||
sensorsAdapter = new StatisticsAdapter.WithRecordedLayout(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -238,10 +238,6 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
|
||||
// Set altitude gain and loss
|
||||
{
|
||||
// Make altitude visible?
|
||||
boolean show = PreferencesUtils.isShowStatsAltitude(sharedPreferences, getContext());
|
||||
viewBinding.statsAltitudeGroup.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||
|
||||
Float altitudeGain_m = trackStatistics.getTotalAltitudeGain();
|
||||
Float altitudeLoss_m = trackStatistics.getTotalAltitudeLoss();
|
||||
|
||||
@@ -254,6 +250,9 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
parts = StringUtils.getAltitudeParts(getContext(), altitudeLoss_m, preferenceMetricUnits);
|
||||
viewBinding.statsAltitudeLossValue.setText(parts.first);
|
||||
viewBinding.statsAltitudeLossUnit.setText(parts.second);
|
||||
|
||||
boolean show = altitudeGain_m != null && altitudeLoss_m != null;
|
||||
viewBinding.statsAltitudeGroup.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,18 +261,7 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
return;
|
||||
}
|
||||
|
||||
List<SensorDataModel> sensorDataList = new ArrayList<>();
|
||||
if (sensorStatistics.hasHeartRate()) {
|
||||
sensorDataList.add(new SensorDataModel(R.string.sensor_state_heart_rate_max, R.string.sensor_unit_beats_per_minute, sensorStatistics.getMaxHeartRate()));
|
||||
sensorDataList.add(new SensorDataModel(R.string.sensor_state_heart_rate_avg, R.string.sensor_unit_beats_per_minute, sensorStatistics.getAvgHeartRate()));
|
||||
}
|
||||
if (sensorStatistics.hasCadence()) {
|
||||
sensorDataList.add(new SensorDataModel(R.string.sensor_state_cadence_max, R.string.sensor_unit_rounds_per_minute, sensorStatistics.getMaxCadence()));
|
||||
sensorDataList.add(new SensorDataModel(R.string.sensor_state_cadence_avg, R.string.sensor_unit_rounds_per_minute, sensorStatistics.getAvgCadence()));
|
||||
}
|
||||
if (sensorStatistics.hasPower()) {
|
||||
sensorDataList.add(new SensorDataModel(R.string.sensor_state_power_avg, R.string.sensor_unit_power, sensorStatistics.getAvgPower()));
|
||||
}
|
||||
List<StatisticData> sensorDataList = StatisticDataBuilder.fromSensorStatistics(getContext(), sensorStatistics);
|
||||
if (sensorDataList.size() > 0) {
|
||||
sensorsAdapter.swapData(sensorDataList);
|
||||
}
|
||||
|
||||
+39
-178
@@ -3,7 +3,6 @@ package de.dennisguse.opentracks.fragments;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -11,28 +10,25 @@ import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TrackRecordingActivity;
|
||||
import de.dennisguse.opentracks.adapters.SensorsAdapter;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.adapters.StatisticsAdapter;
|
||||
import de.dennisguse.opentracks.content.data.Layout;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
import de.dennisguse.opentracks.viewmodels.SensorDataModel;
|
||||
import de.dennisguse.opentracks.viewmodels.StatisticData;
|
||||
import de.dennisguse.opentracks.viewmodels.StatisticsDataModel;
|
||||
|
||||
/**
|
||||
* A fragment to display track statistics to the user for a currently recording {@link Track}.
|
||||
@@ -51,15 +47,16 @@ public class StatisticsRecordingFragment extends Fragment {
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private TrackRecordingService.RecordingData recordingData = TrackRecordingService.NOT_RECORDING;
|
||||
private TrackPoint latestTrackPoint;
|
||||
private Layout layout;
|
||||
|
||||
private StatisticsRecordingBinding viewBinding;
|
||||
private SensorsAdapter sensorsAdapter;
|
||||
private StatisticsAdapter statisticsAdapter;
|
||||
private GridLayoutManager gridLayoutManager;
|
||||
private StatisticsDataModel viewModel;
|
||||
private LiveData<List<StatisticData>> statisticsLiveData;
|
||||
|
||||
private SharedPreferences sharedPreferences;
|
||||
private boolean preferenceMetricUnits;
|
||||
private boolean preferenceReportSpeed;
|
||||
private boolean preferenceShowAltitude;
|
||||
private boolean preferenceShowCoordinate;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
||||
boolean updateUInecessary = false;
|
||||
@@ -69,19 +66,13 @@ public class StatisticsRecordingFragment extends Fragment {
|
||||
preferenceMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key) && recordingData != null) {
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_custom_layout_fields_key, key)) {
|
||||
updateUInecessary = true;
|
||||
preferenceReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), recordingData.getTrackCategory());
|
||||
layout = PreferencesUtils.getCustomLayout(sharedPreferences, getContext());
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_grade_altitude_key, key)) {
|
||||
updateUInecessary = true;
|
||||
preferenceShowAltitude = PreferencesUtils.isShowStatsAltitude(sharedPreferences, getContext());
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_show_coordinate_key, key)) {
|
||||
updateUInecessary = true;
|
||||
preferenceShowCoordinate = PreferencesUtils.isStatsShowCoordinate(sharedPreferences, getContext());
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_custom_layout_columns_key, key)) {
|
||||
gridLayoutManager.setSpanCount(PreferencesUtils.getLayoutColumns(sharedPreferences, getContext()));
|
||||
}
|
||||
|
||||
if (key != null && updateUInecessary && isResumed()) {
|
||||
@@ -109,17 +100,28 @@ public class StatisticsRecordingFragment extends Fragment {
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||
|
||||
sensorsAdapter = new SensorsAdapter(getContext());
|
||||
statisticsAdapter = new StatisticsAdapter(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false);
|
||||
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(recordingData.getTrackCategory()));
|
||||
|
||||
RecyclerView sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
|
||||
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
|
||||
sensorsRecyclerView.setAdapter(sensorsAdapter);
|
||||
RecyclerView recyclerView = viewBinding.statsRecyclerView;
|
||||
recyclerView.addItemDecoration(new DividerItemDecoration(getContext(), RecyclerView.VERTICAL));
|
||||
final int numColumns = PreferencesUtils.getLayoutColumns(sharedPreferences, getContext());
|
||||
gridLayoutManager = new GridLayoutManager(getContext(), numColumns);
|
||||
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
|
||||
@Override
|
||||
public int getSpanSize(int position) {
|
||||
if (statisticsAdapter.isItemWide(position)) {
|
||||
return numColumns;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
recyclerView.setLayoutManager(gridLayoutManager);
|
||||
recyclerView.setAdapter(statisticsAdapter);
|
||||
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
@@ -131,13 +133,16 @@ public class StatisticsRecordingFragment extends Fragment {
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
|
||||
|
||||
viewModel = new ViewModelProvider(getActivity()).get(StatisticsDataModel.class);
|
||||
statisticsLiveData = viewModel.getStatsData();
|
||||
statisticsLiveData.observe(getActivity(), statsDataList -> statisticsAdapter.swapData(statsDataList));
|
||||
|
||||
trackRecordingServiceConnection.startConnection(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
}
|
||||
|
||||
@@ -158,160 +163,16 @@ public class StatisticsRecordingFragment extends Fragment {
|
||||
super.onDestroy();
|
||||
trackRecordingServiceConnection = null;
|
||||
sharedPreferences = null;
|
||||
viewModel = null;
|
||||
statisticsLiveData.removeObservers(getActivity());
|
||||
}
|
||||
|
||||
private void updateUI() {
|
||||
if (isResumed()) {
|
||||
updateStats();
|
||||
setLocationValues();
|
||||
updateSensorDataUI();
|
||||
viewModel.update(recordingData, layout, preferenceMetricUnits);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to fetch most recent {@link SensorDataSet} from {@link de.dennisguse.opentracks.services.TrackRecordingService}.
|
||||
* Also sets altitude gain and loss.
|
||||
*/
|
||||
private void updateSensorDataUI() {
|
||||
SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
|
||||
if (sensorDataSet != null) {
|
||||
List<SensorDataModel> sensorDataList = new ArrayList<>();
|
||||
if (sensorDataSet.getHeartRate() != null) {
|
||||
sensorDataList.add(new SensorDataModel(sensorDataSet.getHeartRate()));
|
||||
}
|
||||
if (sensorDataSet.getCyclingCadence() != null) {
|
||||
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingCadence()));
|
||||
}
|
||||
if (sensorDataSet.getCyclingPower() != null) {
|
||||
sensorDataList.add(new SensorDataModel(sensorDataSet.getCyclingPower()));
|
||||
}
|
||||
sensorsAdapter.swapData(sensorDataList);
|
||||
}
|
||||
|
||||
{
|
||||
Pair<String, String> parts = StringUtils.getAltitudeParts(getContext(), recordingData.getTrackStatistics().getTotalAltitudeGain(), preferenceMetricUnits);
|
||||
viewBinding.statsAltitudeGainValue.setText(parts.first);
|
||||
viewBinding.statsAltitudeGainUnit.setText(parts.second);
|
||||
}
|
||||
{
|
||||
Pair<String, String> parts = StringUtils.getAltitudeParts(getContext(), recordingData.getTrackStatistics().getTotalAltitudeLoss(), preferenceMetricUnits);
|
||||
viewBinding.statsAltitudeLossValue.setText(parts.first);
|
||||
viewBinding.statsAltitudeLossUnit.setText(parts.second);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStats() {
|
||||
// Set activity type
|
||||
String trackIconValue = TrackIconUtils.getIconValue(getContext(), recordingData.getTrackCategory());
|
||||
viewBinding.statsActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(trackIconValue));
|
||||
|
||||
// Set time
|
||||
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(recordingData.getTrackStatistics().getMovingTime()));
|
||||
updateTotalTime();
|
||||
|
||||
// Set average speed/pace
|
||||
{
|
||||
Speed speed = recordingData.getTrackStatistics().getAverageSpeed();
|
||||
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
|
||||
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
|
||||
viewBinding.statsAverageSpeedValue.setText(parts.first);
|
||||
viewBinding.statsAverageSpeedUnit.setText(parts.second);
|
||||
}
|
||||
|
||||
// Set max speed/pace
|
||||
{
|
||||
Speed speed = recordingData.getTrackStatistics().getMaxSpeed();
|
||||
|
||||
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
|
||||
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
|
||||
viewBinding.statsMaxSpeedValue.setText(parts.first);
|
||||
viewBinding.statsMaxSpeedUnit.setText(parts.second);
|
||||
}
|
||||
|
||||
// Set moving speed/pace
|
||||
{
|
||||
Speed speed = recordingData.getTrackStatistics().getAverageMovingSpeed();
|
||||
|
||||
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
|
||||
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
|
||||
viewBinding.statsMovingSpeedValue.setText(parts.first);
|
||||
viewBinding.statsMovingSpeedUnit.setText(parts.second);
|
||||
}
|
||||
|
||||
// Set altitude gain and loss
|
||||
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void updateTotalTime() {
|
||||
Duration totalTime = recordingData.getTrackStatistics().getTotalTime();
|
||||
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(totalTime));
|
||||
}
|
||||
|
||||
private void setLocationValues() {
|
||||
// Set speed/pace
|
||||
Speed speed = latestTrackPoint != null && latestTrackPoint.hasSpeed() ? latestTrackPoint.getSpeed() : null;
|
||||
|
||||
SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
|
||||
if (sensorDataSet != null && sensorDataSet.getCyclingDistanceSpeed() != null) {
|
||||
SensorDataCycling.DistanceSpeed data = sensorDataSet.getCyclingDistanceSpeed();
|
||||
if (data.hasValue() && data.isRecent()) {
|
||||
speed = data.getValue().getSpeed();
|
||||
}
|
||||
}
|
||||
|
||||
setSpeed(speed);
|
||||
|
||||
// Set distance
|
||||
{
|
||||
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), recordingData.getTrackStatistics().getTotalDistance(), preferenceMetricUnits);
|
||||
|
||||
viewBinding.statsDistanceValue.setText(parts.first);
|
||||
viewBinding.statsDistanceUnit.setText(parts.second);
|
||||
}
|
||||
// Set altitude
|
||||
viewBinding.statsAltitudeGroup.setVisibility(preferenceShowAltitude ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (preferenceShowAltitude) {
|
||||
// Current altitude
|
||||
Float altitude = null;
|
||||
int labelId = R.string.value_unknown;
|
||||
if (latestTrackPoint != null && latestTrackPoint.hasAltitude()) {
|
||||
altitude = (float) latestTrackPoint.getAltitude().toM();
|
||||
labelId = latestTrackPoint.getAltitude().getLabelId();
|
||||
}
|
||||
|
||||
Pair<String, String> parts = StringUtils.getAltitudeParts(getContext(), altitude, preferenceMetricUnits);
|
||||
viewBinding.statsAltitudeCurrentValue.setText(parts.first);
|
||||
viewBinding.statsAltitudeCurrentUnit.setText(parts.second);
|
||||
|
||||
viewBinding.statsAltitudeCurrentLabelEgm.setText(labelId);
|
||||
}
|
||||
|
||||
// Set coordinate
|
||||
viewBinding.statsCoordinateGroup.setVisibility(preferenceShowCoordinate ? View.VISIBLE : View.GONE);
|
||||
if (preferenceShowCoordinate) {
|
||||
String latitudeText = getContext().getString(R.string.value_unknown);
|
||||
String longitudeText = getContext().getString(R.string.value_unknown);
|
||||
if (latestTrackPoint != null && latestTrackPoint.hasLocation()) {
|
||||
latitudeText = StringUtils.formatCoordinate(latestTrackPoint.getLatitude());
|
||||
longitudeText = StringUtils.formatCoordinate(latestTrackPoint.getLongitude());
|
||||
}
|
||||
viewBinding.statsLatitudeValue.setText(latitudeText);
|
||||
viewBinding.statsLongitudeValue.setText(longitudeText);
|
||||
}
|
||||
}
|
||||
|
||||
private void setSpeed(Speed speed) {
|
||||
viewBinding.statsSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_speed : R.string.stats_pace);
|
||||
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
|
||||
viewBinding.statsSpeedValue.setText(parts.first);
|
||||
viewBinding.statsSpeedUnit.setText(parts.second);
|
||||
}
|
||||
|
||||
private void onRecordingDataChanged(TrackRecordingService.RecordingData recordingData) {
|
||||
String oldCategory = this.recordingData.getTrackCategory();
|
||||
String newCategory = recordingData.getTrackCategory();
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks.settings;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
@@ -35,15 +36,23 @@ public class ResetDialogPreference extends DialogPreference {
|
||||
|
||||
@Override
|
||||
public void onDialogClosed(boolean positiveResult) {
|
||||
if (positiveResult) {
|
||||
FragmentActivity activity = getActivity();
|
||||
if (!positiveResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
FragmentActivity activity = getActivity();
|
||||
|
||||
String preferenceKey = getArguments().getString(PreferenceDialogFragmentCompat.ARG_KEY);
|
||||
if (preferenceKey.equals(getString(R.string.settings_reset_key))) {
|
||||
PreferencesUtils.resetPreferences(activity, true);
|
||||
Toast.makeText(activity, R.string.settings_reset_done, Toast.LENGTH_SHORT).show();
|
||||
} else if (preferenceKey.equals(getString(R.string.settings_layout_reset_key))) {
|
||||
PreferencesUtils.resetCustomLayoutPreferences(activity);
|
||||
Toast.makeText(activity, R.string.settings_layout_reset_done, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
if (activity instanceof ResetCallback) {
|
||||
((ResetCallback) activity).onReset();
|
||||
}
|
||||
if (activity instanceof ResetCallback) {
|
||||
((ResetCallback) activity).onReset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +208,13 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
|
||||
}
|
||||
);
|
||||
|
||||
Preference customLayoutPreference = findPreference(getString(R.string.stats_custom_layout_key));
|
||||
customLayoutPreference.setOnPreferenceClickListener((preference) -> {
|
||||
Intent intent = new Intent(getContext(), SettingsCustomLayoutActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
});
|
||||
|
||||
announcementsFrequency = findPreference(getString(R.string.voice_frequency_key));
|
||||
announcementsSpeed = findPreference(getString(R.string.voice_speed_rate_key));
|
||||
updateVoiceAnnouncements();
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package de.dennisguse.opentracks.settings;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import de.dennisguse.opentracks.AbstractActivity;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.adapters.SettingsCustomLayoutAdapter;
|
||||
import de.dennisguse.opentracks.content.data.Layout;
|
||||
import de.dennisguse.opentracks.content.data.DataField;
|
||||
import de.dennisguse.opentracks.databinding.ActivitySettingsCustomLayoutBinding;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StatisticsUtils;
|
||||
|
||||
public class SettingsCustomLayoutActivity extends AbstractActivity implements SettingsCustomLayoutAdapter.SettingsCustomLayoutItemClickListener {
|
||||
|
||||
private ActivitySettingsCustomLayoutBinding viewBinding;
|
||||
private GridLayoutManager gridLayoutManager;
|
||||
private SettingsCustomLayoutAdapter adapterFieldsVisible;
|
||||
private SettingsCustomLayoutAdapter adapterFieldsHidden;
|
||||
private Layout layoutFieldsVisible;
|
||||
private Layout layoutFieldsHidden;
|
||||
private SharedPreferences sharedPreferences;
|
||||
private int numColumns;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
||||
|
||||
// Recycler view with visible stats.
|
||||
layoutFieldsVisible = StatisticsUtils.filterVisible(PreferencesUtils.getCustomLayout(sharedPreferences, this), true);
|
||||
adapterFieldsVisible = new SettingsCustomLayoutAdapter(this, this, layoutFieldsVisible);
|
||||
|
||||
numColumns = PreferencesUtils.getLayoutColumns(sharedPreferences, this);
|
||||
RecyclerView recyclerViewVisible = viewBinding.recyclerViewVisible;
|
||||
gridLayoutManager = new GridLayoutManager(this, numColumns);
|
||||
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
|
||||
@Override
|
||||
public int getSpanSize(int position) {
|
||||
if (adapterFieldsVisible.isItemWide(position)) {
|
||||
return numColumns;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
recyclerViewVisible.setLayoutManager(gridLayoutManager);
|
||||
recyclerViewVisible.setAdapter(adapterFieldsVisible);
|
||||
|
||||
ItemTouchHelper.SimpleCallback simpleCallback = new ItemTouchHelper.SimpleCallback(
|
||||
ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.START | ItemTouchHelper.END, 0) {
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
|
||||
int fromPosition = viewHolder.getAdapterPosition();
|
||||
int toPosition = target.getAdapterPosition();
|
||||
layoutFieldsVisible = adapterFieldsVisible.move(fromPosition, toPosition);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
}
|
||||
};
|
||||
|
||||
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(simpleCallback);
|
||||
itemTouchHelper.attachToRecyclerView(recyclerViewVisible);
|
||||
|
||||
// Spinner with items per row.
|
||||
ArrayAdapter<Integer> spinnerAdapter = new ArrayAdapter<>(this,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
IntStream.of(getResources().getIntArray(R.array.stats_custom_layout_fields_columns_per_row)).boxed().toArray(Integer[]::new));
|
||||
viewBinding.spinnerOptions.setAdapter(spinnerAdapter);
|
||||
viewBinding.spinnerOptions.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
numColumns = position + 1;
|
||||
gridLayoutManager.setSpanCount(numColumns);
|
||||
PreferencesUtils.setLayoutColumns(sharedPreferences, SettingsCustomLayoutActivity.this, position + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
|
||||
}
|
||||
});
|
||||
viewBinding.spinnerOptions.setSelection(PreferencesUtils.getLayoutColumns(sharedPreferences, SettingsCustomLayoutActivity.this) - 1);
|
||||
|
||||
// Recycler view with not visible stats.
|
||||
layoutFieldsHidden = StatisticsUtils.filterVisible(PreferencesUtils.getCustomLayout(sharedPreferences, this), false);
|
||||
adapterFieldsHidden = new SettingsCustomLayoutAdapter(this, this, layoutFieldsHidden);
|
||||
RecyclerView recyclerViewNotVisible = viewBinding.recyclerViewNotVisible;
|
||||
recyclerViewNotVisible.setLayoutManager(new LinearLayoutManager(this));
|
||||
recyclerViewNotVisible.setAdapter(adapterFieldsHidden);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
if (!layoutFieldsVisible.getFields().isEmpty() || !layoutFieldsHidden.getFields().isEmpty()) {
|
||||
Layout newLayout = new Layout(layoutFieldsVisible.getProfile());
|
||||
newLayout.addFields(layoutFieldsVisible.getFields());
|
||||
newLayout.addFields(layoutFieldsHidden.getFields());
|
||||
PreferencesUtils.setCustomLayout(sharedPreferences, this, newLayout);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
sharedPreferences = null;
|
||||
layoutFieldsVisible = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupActionBarBack(Toolbar toolbar) {
|
||||
super.setupActionBarBack(toolbar);
|
||||
toolbar.setTitle(R.string.menu_settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getRootView() {
|
||||
viewBinding = ActivitySettingsCustomLayoutBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSettingsCustomLayoutItemClicked(@NonNull DataField field) {
|
||||
if (field.isVisible()) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.generic_choose_an_option)
|
||||
.setItems(new String[]{getString(field.isPrimary() ? R.string.field_set_secondary : R.string.field_set_primary), getString(R.string.field_remove_from_layout)}, (dialog, which) -> {
|
||||
if (which == 0) {
|
||||
field.togglePrimary();
|
||||
} else {
|
||||
layoutFieldsVisible.removeField(field);
|
||||
field.toggleVisibility();
|
||||
layoutFieldsHidden.addField(field);
|
||||
}
|
||||
|
||||
adapterFieldsVisible.swapValues(layoutFieldsVisible);
|
||||
adapterFieldsHidden.swapValues(layoutFieldsHidden);
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
} else {
|
||||
layoutFieldsHidden.removeField(field);
|
||||
field.toggleVisibility();
|
||||
layoutFieldsVisible.addField(field);
|
||||
viewBinding.scrollView.fullScroll(ScrollView.FOCUS_UP);
|
||||
|
||||
adapterFieldsVisible.swapValues(layoutFieldsVisible);
|
||||
adapterFieldsHidden.swapValues(layoutFieldsHidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
public final class CsvConstants {
|
||||
private CsvConstants() {
|
||||
|
||||
}
|
||||
|
||||
public static final String LINE_SEPARATOR = ";";
|
||||
public static final String ITEM_SEPARATOR = ",";
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
|
||||
public class PreferencesOpenHelper {
|
||||
private final Context context;
|
||||
private final SharedPreferences sharedPreferences;
|
||||
|
||||
private static final int PREFERENCES_VERSION = 1;
|
||||
|
||||
private PreferencesOpenHelper(Context context, SharedPreferences sharedPreferences) {
|
||||
this.context = context;
|
||||
this.sharedPreferences = sharedPreferences;
|
||||
}
|
||||
|
||||
public static PreferencesOpenHelper newInstance(Context context, SharedPreferences sharedPreferences) {
|
||||
return new PreferencesOpenHelper(context, sharedPreferences);
|
||||
}
|
||||
|
||||
public void checkForUpgrade() {
|
||||
int lastVersion = PreferencesUtils.getInt(sharedPreferences, context, R.string.prefs_last_version_key, 0);
|
||||
if (PREFERENCES_VERSION > lastVersion) {
|
||||
onUpgrade();
|
||||
}
|
||||
}
|
||||
|
||||
private void onUpgrade() {
|
||||
PreferencesUtils.setInt(sharedPreferences, context, R.string.prefs_last_version_key, PREFERENCES_VERSION);
|
||||
switch (PREFERENCES_VERSION) {
|
||||
case 1:
|
||||
upgradeFrom0to1();
|
||||
}
|
||||
}
|
||||
|
||||
private void upgradeFrom0to1() {
|
||||
PreferencesUtils.setString(sharedPreferences, context, R.string.stats_custom_layout_fields_key, PreferencesUtils.buildDefaultLayout(context));
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,11 @@
|
||||
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.content.res.TypedArray;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -26,8 +28,16 @@ import androidx.annotation.VisibleForTesting;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.DataField;
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Layout;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
|
||||
/**
|
||||
@@ -44,7 +54,9 @@ public class PreferencesUtils {
|
||||
|
||||
@Deprecated //Should only be used to get a sharedPreference for more than one interaction!
|
||||
public static SharedPreferences getSharedPreferences(Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context);
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
PreferencesOpenHelper.newInstance(context, sharedPreferences).checkForUpgrade();
|
||||
return sharedPreferences;
|
||||
}
|
||||
|
||||
public static String getDefaultActivity(SharedPreferences sharedPreferences, Context context) {
|
||||
@@ -80,7 +92,7 @@ public class PreferencesUtils {
|
||||
return sharedPreferences.getBoolean(getKey(context, keyId), defaultValue);
|
||||
}
|
||||
|
||||
private static int getInt(SharedPreferences sharedPreferences, Context context, int keyId, int defaultValue) {
|
||||
static int getInt(SharedPreferences sharedPreferences, Context context, int keyId, int defaultValue) {
|
||||
try {
|
||||
return sharedPreferences.getInt(getKey(context, keyId), defaultValue);
|
||||
} catch (ClassCastException e) {
|
||||
@@ -123,6 +135,12 @@ public class PreferencesUtils {
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
static void setInt(SharedPreferences sharedPreferences, Context context, int keyId, int value) {
|
||||
Editor editor = sharedPreferences.edit();
|
||||
editor.putInt(getKey(context, keyId), value);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static boolean isMetricUnits(SharedPreferences sharedPreferences, Context context) {
|
||||
final String STATS_UNIT = context.getString(R.string.stats_units_default);
|
||||
return STATS_UNIT.equals(getString(sharedPreferences, context, R.string.stats_units_key, STATS_UNIT));
|
||||
@@ -196,16 +214,6 @@ public class PreferencesUtils {
|
||||
return getBoolean(sharedPreferences, context, R.string.stats_fullscreen_while_recording_key, DEFAULT);
|
||||
}
|
||||
|
||||
public static boolean isShowStatsAltitude(SharedPreferences sharedPreferences, Context context) {
|
||||
final boolean STATS_SHOW_ALTITUDE = context.getResources().getBoolean(R.bool.stats_show_altitude_default);
|
||||
return getBoolean(sharedPreferences, context, R.string.stats_show_grade_altitude_key, STATS_SHOW_ALTITUDE);
|
||||
}
|
||||
|
||||
public static boolean isStatsShowCoordinate(SharedPreferences sharedPreferences, Context context) {
|
||||
final boolean STATS_SHOW_COORDINATE = context.getResources().getBoolean(R.bool.stats_show_coordinate_default);
|
||||
return getBoolean(sharedPreferences, context, R.string.stats_show_coordinate_key, STATS_SHOW_COORDINATE);
|
||||
}
|
||||
|
||||
public static int getVoiceFrequency(SharedPreferences sharedPreferences, Context context) {
|
||||
final int VOICE_FREQUENCY_DEFAULT = Integer.parseInt(context.getResources().getString(R.string.voice_frequency_default));
|
||||
return getInt(sharedPreferences, context, R.string.voice_frequency_key, VOICE_FREQUENCY_DEFAULT);
|
||||
@@ -318,4 +326,78 @@ public class PreferencesUtils {
|
||||
public static boolean isDefaultExportDirectoryUri(SharedPreferences sharedPreferences, Context context) {
|
||||
return getDefaultExportDirectoryUri(sharedPreferences, context) != null;
|
||||
}
|
||||
|
||||
public static int getLayoutColumns(SharedPreferences sharedPreferences, Context context) {
|
||||
return getInt(sharedPreferences, context, R.string.stats_custom_layout_columns_key, context.getResources().getInteger(R.integer.stats_custom_layout_columns_default));
|
||||
}
|
||||
|
||||
public static void setLayoutColumns(SharedPreferences sharedPreferences, Context context, int columns) {
|
||||
setInt(sharedPreferences, context, R.string.stats_custom_layout_columns_key, columns);
|
||||
}
|
||||
|
||||
private static List<TypedArray> getMultiTypedArray(Context context, String key) {
|
||||
List<TypedArray> typedArrays = new ArrayList<>();
|
||||
|
||||
try {
|
||||
Class<R.array> resource = R.array.class;
|
||||
Field field;
|
||||
int i = 0;
|
||||
|
||||
do {
|
||||
field = resource.getField(key + context.getString(R.string.stats_custom_layout_fields_default_value_separator) + i);
|
||||
typedArrays.add(context.getResources().obtainTypedArray(field.getInt(null)));
|
||||
i++;
|
||||
} while (field != null);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, e.getMessage());
|
||||
}
|
||||
|
||||
return typedArrays;
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
static String buildDefaultLayout(Context context) {
|
||||
List<TypedArray> fieldsArrays = getMultiTypedArray(context, "stats_custom_layout_fields_default_value");
|
||||
return context.getString(R.string.default_activity_default) + CsvConstants.LINE_SEPARATOR
|
||||
+ fieldsArrays.stream().map(i -> i.getString(0) + CsvConstants.ITEM_SEPARATOR + i.getString(1)).collect(Collectors.joining(CsvConstants.LINE_SEPARATOR))
|
||||
+ CsvConstants.LINE_SEPARATOR;
|
||||
}
|
||||
|
||||
public static Layout getCustomLayout(SharedPreferences sharedPreferences, Context context) {
|
||||
String csvCustomLayout = getString(sharedPreferences, context, R.string.stats_custom_layout_fields_key, buildDefaultLayout(context));
|
||||
List<String> csvParts = Arrays.asList(csvCustomLayout.split(CsvConstants.LINE_SEPARATOR));
|
||||
Layout layout = new Layout(csvParts.get(0));
|
||||
for (int i = 1; i < csvParts.size(); i++) {
|
||||
String[] fieldParts = csvParts.get(i).split(CsvConstants.ITEM_SEPARATOR);
|
||||
layout.addField(fieldParts[0], DataField.getTitleByKey(context, fieldParts[0]), fieldParts[1].equals(DataField.YES_VALUE), fieldParts[2].equals(DataField.YES_VALUE), fieldParts[0].equals(context.getString(R.string.stats_custom_layout_coordinates_key)));
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
public static void setCustomLayout(SharedPreferences sharedPreferences, Context context, Layout layout) {
|
||||
List<DataField> fields = layout.getFields();
|
||||
if (fields.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String csv = layout.getProfile() + CsvConstants.LINE_SEPARATOR
|
||||
+ fields.stream().map(DataField::toCsv).collect(Collectors.joining(CsvConstants.LINE_SEPARATOR)) + CsvConstants.LINE_SEPARATOR;
|
||||
setString(sharedPreferences, context, R.string.stats_custom_layout_fields_key, csv);
|
||||
}
|
||||
|
||||
public static void resetCustomLayoutPreferences(Context context) {
|
||||
SharedPreferences settings = getSharedPreferences(context);
|
||||
if (settings.contains(context.getString(R.string.stats_custom_layout_fields_key))) {
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.remove(context.getString(R.string.stats_custom_layout_fields_key));
|
||||
editor.commit();
|
||||
}
|
||||
if (settings.contains(context.getString(R.string.stats_custom_layout_columns_key))) {
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.remove(context.getString(R.string.stats_custom_layout_columns_key));
|
||||
editor.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Layout;
|
||||
|
||||
public class StatisticsUtils {
|
||||
|
||||
public static String emptyValue(@NonNull Context context, @NonNull String statTitle) {
|
||||
if (PreferencesUtils.isKey(context, R.string.stats_custom_layout_total_time_key, statTitle) || PreferencesUtils.isKey(context, R.string.stats_custom_layout_moving_time_key, statTitle) || PreferencesUtils.isKey(context, R.string.stats_custom_layout_pace_key, statTitle) || PreferencesUtils.isKey(context, R.string.stats_custom_layout_average_moving_pace_key, statTitle) || PreferencesUtils.isKey(context, R.string.stats_custom_layout_average_pace_key, statTitle) || PreferencesUtils.isKey(context, R.string.stats_custom_layout_fastest_pace_key, statTitle)) {
|
||||
return context.getString(R.string.stats_empty_value_time);
|
||||
} else if (PreferencesUtils.isKey(context, R.string.stats_custom_layout_distance_key, statTitle) || PreferencesUtils.isKey(context, R.string.stats_custom_layout_speed_key, statTitle) || PreferencesUtils.isKey(context, R.string.stats_custom_layout_average_speed_key, statTitle) || PreferencesUtils.isKey(context, R.string.stats_custom_layout_max_speed_key, statTitle) || PreferencesUtils.isKey(context, R.string.stats_custom_layout_average_moving_speed_key, statTitle)) {
|
||||
return context.getString(R.string.stats_empty_value_float);
|
||||
} else if (PreferencesUtils.isKey(context, R.string.stats_custom_layout_coordinates_key, statTitle)) {
|
||||
return context.getString(R.string.stats_empty_value_coordinates);
|
||||
} else {
|
||||
return context.getString(R.string.stats_empty_value_integer);
|
||||
}
|
||||
}
|
||||
|
||||
public static Layout filterVisible(Layout layout, boolean visible) {
|
||||
Layout result = new Layout(layout.getProfile());
|
||||
result.addFields(layout.getFields().stream().filter(f -> f.isVisible() == visible).collect(Collectors.toList()));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -124,10 +124,22 @@ public class StringUtils {
|
||||
/**
|
||||
* Formats a coordinate
|
||||
*
|
||||
* @param context the context's object.
|
||||
* @param coordinate the coordinate
|
||||
*/
|
||||
public static String formatCoordinate(double coordinate) {
|
||||
return Location.convert(coordinate, Location.FORMAT_DEGREES) + COORDINATE_DEGREE;
|
||||
public static String formatCoordinate(Context context, double coordinate) {
|
||||
return context.getString(R.string.location_coordinate, Location.convert(coordinate, Location.FORMAT_DEGREES));
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a complete coordinate (latitude, longitude)
|
||||
*
|
||||
* @param context the context's object.
|
||||
* @param latitude the coordinate's latitude.
|
||||
* @param longitude the coordinate's longitude.
|
||||
*/
|
||||
public static String formatCoordinate(Context context, double latitude, double longitude) {
|
||||
return context.getString(R.string.location_latitude_longitude, Location.convert(latitude, Location.FORMAT_DEGREES), Location.convert(longitude, Location.FORMAT_DEGREES));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,6 +203,33 @@ public class StringUtils {
|
||||
return new Pair<>(context.getString(R.string.time, minutes, seconds), unitString);
|
||||
}
|
||||
|
||||
public static Pair<String, String> getHeartRateParts(Context context, Float heartrate_bpm) {
|
||||
String value = context.getString(R.string.value_none);
|
||||
if (heartrate_bpm != null) {
|
||||
value = StringUtils.formatDecimal(heartrate_bpm, 0);
|
||||
}
|
||||
|
||||
return new Pair<>(value, context.getString(R.string.sensor_unit_beats_per_minute));
|
||||
}
|
||||
|
||||
public static Pair<String, String> getCadenceParts(Context context, Float cadence_rpm) {
|
||||
String value = context.getString(R.string.value_none);
|
||||
if (cadence_rpm != null) {
|
||||
value = StringUtils.formatDecimal(cadence_rpm, 0);
|
||||
}
|
||||
|
||||
return new Pair<>(value, context.getString(R.string.sensor_unit_rounds_per_minute));
|
||||
}
|
||||
|
||||
public static Pair<String, String> getPowerParts(Context context, Float power_w) {
|
||||
String value = context.getString(R.string.value_none);
|
||||
if (power_w != null) {
|
||||
value = StringUtils.formatDecimal(power_w, 0);
|
||||
}
|
||||
|
||||
return new Pair<>(value, context.getString(R.string.sensor_unit_power));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a string for category.
|
||||
*
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package de.dennisguse.opentracks.viewmodels;
|
||||
|
||||
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 SensorDataModel {
|
||||
private int labelId;
|
||||
private final String sensorValue;
|
||||
private String sensorName;
|
||||
private int unitId;
|
||||
|
||||
public SensorDataModel(int labelId, int unitId, float sensorValue) {
|
||||
this.labelId = labelId;
|
||||
this.unitId = unitId;
|
||||
this.sensorValue = StringUtils.formatDecimal(sensorValue, 0);
|
||||
}
|
||||
|
||||
public SensorDataModel(SensorData<Float> sensorData) {
|
||||
this.sensorName = sensorData.getSensorNameOrAddress();
|
||||
this.sensorValue = sensorData.hasValue() && sensorData.isRecent() ? StringUtils.formatDecimal(sensorData.getValue(), 0) : null;
|
||||
if (sensorData instanceof SensorDataHeartRate) {
|
||||
this.labelId = R.string.sensor_state_heart_rate;
|
||||
this.unitId = R.string.sensor_unit_beats_per_minute;
|
||||
} else if (sensorData instanceof SensorDataCycling.Cadence) {
|
||||
this.labelId = R.string.sensor_state_cadence;
|
||||
this.unitId = R.string.sensor_unit_rounds_per_minute;
|
||||
} else if (sensorData instanceof SensorDataCyclingPower) {
|
||||
this.labelId = R.string.sensor_state_power;
|
||||
this.unitId = R.string.sensor_unit_power;
|
||||
}
|
||||
}
|
||||
|
||||
public int getLabelId() {
|
||||
return labelId;
|
||||
}
|
||||
|
||||
public int getUnitId() {
|
||||
return unitId;
|
||||
}
|
||||
|
||||
public String getSensorName() {
|
||||
return sensorName;
|
||||
}
|
||||
|
||||
public String getSensorValue() {
|
||||
return sensorValue;
|
||||
}
|
||||
|
||||
public boolean hasValue() {
|
||||
return sensorValue != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package de.dennisguse.opentracks.viewmodels;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.DataField;
|
||||
|
||||
public class StatisticData {
|
||||
private final DataField dataField;
|
||||
private final String value;
|
||||
private final String unit;
|
||||
private final String description;
|
||||
|
||||
public StatisticData(@NonNull DataField dataField, @NonNull Pair<String, String> valueAndUnit, String description) {
|
||||
this.dataField = dataField;
|
||||
this.value = valueAndUnit.first;
|
||||
this.unit = valueAndUnit.second;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public DataField getField() {
|
||||
return dataField;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public boolean hasValue() {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
public String getUnit() {
|
||||
return unit;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public boolean hasDescription() {
|
||||
return description != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package de.dennisguse.opentracks.viewmodels;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.DataField;
|
||||
import de.dennisguse.opentracks.content.data.Layout;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.stats.SensorStatistics;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
public class StatisticDataBuilder {
|
||||
|
||||
public static List<StatisticData> fromRecordingData(@NonNull Context context, @NonNull TrackRecordingService.RecordingData recordingData, @NonNull Layout layout, boolean metricUnits) {
|
||||
List<StatisticData> statisticDataList = layout.getFields().stream()
|
||||
.filter(DataField::isVisible)
|
||||
.map(field -> build(context, recordingData, field.getKey(), field.isPrimary(), metricUnits))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
statisticDataList.addAll(getSensorStatsDataIfNeeded(context, recordingData, statisticDataList, metricUnits));
|
||||
return statisticDataList;
|
||||
}
|
||||
|
||||
private static StatisticData build(@NonNull Context context, @NonNull TrackRecordingService.RecordingData recordingData, @NonNull String fieldKey, boolean isPrimary, boolean metricUnits) {
|
||||
final TrackPoint latestTrackPoint = recordingData.getLatestTrackPoint();
|
||||
final SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
|
||||
|
||||
String title = null;
|
||||
String description = null;
|
||||
Pair<String, String> valueAndUnit = null;
|
||||
boolean isWide = false;
|
||||
if (fieldKey.equals(context.getString(R.string.stats_custom_layout_total_time_key))) {
|
||||
valueAndUnit = new Pair<>(StringUtils.formatElapsedTime(recordingData.getTrackStatistics().getTotalTime()), null);
|
||||
title = context.getString(R.string.stats_total_time);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_moving_time_key))) {
|
||||
valueAndUnit = new Pair<>(StringUtils.formatElapsedTime(recordingData.getTrackStatistics().getMovingTime()), null);
|
||||
title = context.getString(R.string.stats_moving_time);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_distance_key))) {
|
||||
valueAndUnit = StringUtils.getDistanceParts(context, recordingData.getTrackStatistics().getTotalDistance(), metricUnits);
|
||||
title = context.getString(R.string.stats_distance);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_speed_key)) || fieldKey.equals(context.getString(R.string.stats_custom_layout_pace_key))) {
|
||||
boolean reportSpeed = fieldKey.equals("speed");
|
||||
title = fieldKey.equals("speed") ? context.getString(R.string.stats_speed) : context.getString(R.string.stats_pace);
|
||||
Speed speed = latestTrackPoint != null && latestTrackPoint.hasSpeed() ? latestTrackPoint.getSpeed() : null;
|
||||
if (sensorDataSet != null && sensorDataSet.getCyclingDistanceSpeed() != null && sensorDataSet.getCyclingDistanceSpeed().hasValue() && sensorDataSet.getCyclingDistanceSpeed().isRecent()) {
|
||||
valueAndUnit = StringUtils.getSpeedParts(context, sensorDataSet.getCyclingDistanceSpeed().getValue().getSpeed(), metricUnits, reportSpeed);
|
||||
description = context.getString(R.string.description_speed_source_sensor, sensorDataSet.getCyclingDistanceSpeed().getSensorNameOrAddress());
|
||||
} else {
|
||||
valueAndUnit = StringUtils.getSpeedParts(context, speed, metricUnits, reportSpeed);
|
||||
description = context.getString(R.string.description_speed_source_gps);
|
||||
}
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_average_moving_speed_key))) {
|
||||
valueAndUnit = StringUtils.getSpeedParts(context, recordingData.getTrackStatistics().getAverageMovingSpeed(), metricUnits, true);
|
||||
title = context.getString(R.string.stats_average_moving_speed);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_average_speed_key))) {
|
||||
valueAndUnit = StringUtils.getSpeedParts(context, recordingData.getTrackStatistics().getAverageSpeed(), metricUnits, true);
|
||||
title = context.getString(R.string.stats_average_speed);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_max_speed_key))) {
|
||||
valueAndUnit = StringUtils.getSpeedParts(context, recordingData.getTrackStatistics().getMaxSpeed(), metricUnits, true);
|
||||
title = context.getString(R.string.stats_max_speed);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_average_moving_pace_key))) {
|
||||
valueAndUnit = StringUtils.getSpeedParts(context, recordingData.getTrackStatistics().getAverageMovingSpeed(), metricUnits, false);
|
||||
title = context.getString(R.string.stats_average_moving_pace);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_average_pace_key))) {
|
||||
valueAndUnit = StringUtils.getSpeedParts(context, recordingData.getTrackStatistics().getAverageSpeed(), metricUnits, false);
|
||||
title = context.getString(R.string.stats_average_pace);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_fastest_pace_key))) {
|
||||
valueAndUnit = StringUtils.getSpeedParts(context, recordingData.getTrackStatistics().getMaxSpeed(), metricUnits, true);
|
||||
title = context.getString(R.string.stats_fastest_pace);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_altitude_key))) {
|
||||
Float altitude = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? (float) latestTrackPoint.getAltitude().toM() : null;
|
||||
title = context.getString(R.string.stats_altitude);
|
||||
description = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? context.getString(latestTrackPoint.getAltitude().getLabelId()) : null;
|
||||
valueAndUnit = StringUtils.getAltitudeParts(context, altitude, metricUnits);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_gain_key))) {
|
||||
valueAndUnit = StringUtils.getAltitudeParts(context, recordingData.getTrackStatistics().getTotalAltitudeGain(), metricUnits);
|
||||
title = context.getString(R.string.stats_gain);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_loss_key))) {
|
||||
valueAndUnit = StringUtils.getAltitudeParts(context, recordingData.getTrackStatistics().getTotalAltitudeLoss(), metricUnits);
|
||||
title = context.getString(R.string.stats_loss);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_coordinates_key))) {
|
||||
title = context.getString(R.string.stats_coordinates);
|
||||
isWide = true;
|
||||
if (latestTrackPoint != null && latestTrackPoint.hasLocation()) {
|
||||
valueAndUnit = new Pair<>(StringUtils.formatCoordinate(context, latestTrackPoint.getLatitude(), latestTrackPoint.getLongitude()), null);
|
||||
} else {
|
||||
valueAndUnit = new Pair<>(context.getString(R.string.value_none), null);
|
||||
}
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_heart_rate_key))) {
|
||||
title = context.getString(R.string.stats_sensors_heart_rate);
|
||||
if (sensorDataSet != null && sensorDataSet.getHeartRate() != null && sensorDataSet.getHeartRate().hasValue() && sensorDataSet.getHeartRate().isRecent()) {
|
||||
valueAndUnit = StringUtils.getHeartRateParts(context, sensorDataSet.getHeartRate().getValue());
|
||||
description = sensorDataSet.getHeartRate().getSensorNameOrAddress();
|
||||
} else {
|
||||
valueAndUnit = StringUtils.getHeartRateParts(context, null);
|
||||
description = context.getString(R.string.value_none);
|
||||
}
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_cadence_key))) {
|
||||
title = context.getString(R.string.stats_sensors_cadence);
|
||||
if (sensorDataSet != null && sensorDataSet.getCyclingCadence() != null && sensorDataSet.getCyclingCadence().hasValue() && sensorDataSet.getCyclingCadence().isRecent()) {
|
||||
valueAndUnit = StringUtils.getCadenceParts(context, sensorDataSet.getCyclingCadence().getValue());
|
||||
description = sensorDataSet.getCyclingCadence().getSensorNameOrAddress();
|
||||
} else {
|
||||
valueAndUnit = StringUtils.getCadenceParts(context, null);
|
||||
description = context.getString(R.string.value_none);
|
||||
}
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_power_key))) {
|
||||
title = context.getString(R.string.stats_sensors_power);
|
||||
if (sensorDataSet != null && sensorDataSet.getCyclingPower() != null && sensorDataSet.getCyclingPower().hasValue() && sensorDataSet.getCyclingPower().isRecent()) {
|
||||
valueAndUnit = StringUtils.getPowerParts(context, sensorDataSet.getCyclingPower().getValue());
|
||||
description = sensorDataSet.getCyclingPower().getSensorNameOrAddress();
|
||||
} else {
|
||||
valueAndUnit = StringUtils.getPowerParts(context, null);
|
||||
description = context.getString(R.string.value_none);
|
||||
}
|
||||
}
|
||||
|
||||
if (valueAndUnit == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new StatisticData(new DataField(fieldKey, title, true, isPrimary, isWide), valueAndUnit, description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a list of StatisticData with sensors connected but not in statisticDataList.
|
||||
*/
|
||||
private static List<StatisticData> getSensorStatsDataIfNeeded(Context context, TrackRecordingService.RecordingData recordingData, List<StatisticData> statisticDataList, boolean metricUnits) {
|
||||
List<StatisticData> sensorDataList = new ArrayList<>();
|
||||
SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
|
||||
if (sensorDataSet == null) {
|
||||
return sensorDataList;
|
||||
}
|
||||
if (statisticDataList.stream().noneMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_heart_rate))) && sensorDataSet.getHeartRate() != null && sensorDataSet.getHeartRate().hasValue() && sensorDataSet.getHeartRate().isRecent()) {
|
||||
sensorDataList.add(build(context, recordingData, "heart_rate", true, metricUnits));
|
||||
}
|
||||
if (statisticDataList.stream().noneMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_cadence))) && sensorDataSet.getCyclingCadence() != null && sensorDataSet.getCyclingCadence().hasValue() && sensorDataSet.getCyclingCadence().isRecent()) {
|
||||
sensorDataList.add(build(context, recordingData, "cadence", true, metricUnits));
|
||||
}
|
||||
if (statisticDataList.stream().noneMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_power))) && sensorDataSet.getCyclingPower() != null && sensorDataSet.getCyclingPower().hasValue() && sensorDataSet.getCyclingPower().isRecent()) {
|
||||
sensorDataList.add(build(context, recordingData, "power", true, metricUnits));
|
||||
}
|
||||
|
||||
return sensorDataList;
|
||||
}
|
||||
|
||||
public static List<StatisticData> fromSensorStatistics(@NonNull Context context, @NonNull SensorStatistics sensorStatistics) {
|
||||
List<StatisticData> sensorDataList = new ArrayList<>();
|
||||
|
||||
if (sensorStatistics.hasHeartRate()) {
|
||||
sensorDataList.add(
|
||||
new StatisticData(
|
||||
new DataField(context.getString(R.string.stats_custom_layout_heart_rate_key), context.getString(R.string.sensor_state_heart_rate_max), true, true, false),
|
||||
StringUtils.getHeartRateParts(context, sensorStatistics.getMaxHeartRate()),
|
||||
null
|
||||
)
|
||||
);
|
||||
sensorDataList.add(
|
||||
new StatisticData(
|
||||
new DataField(context.getString(R.string.stats_custom_layout_average_heart_rate_key), context.getString(R.string.sensor_state_heart_rate_avg), true, true, false),
|
||||
StringUtils.getHeartRateParts(context, sensorStatistics.getAvgHeartRate()),
|
||||
null
|
||||
)
|
||||
);
|
||||
}
|
||||
if (sensorStatistics.hasCadence()) {
|
||||
sensorDataList.add(
|
||||
new StatisticData(
|
||||
new DataField(context.getString(R.string.stats_custom_layout_cadence_key), context.getString(R.string.sensor_state_cadence_max), true, true, false),
|
||||
StringUtils.getCadenceParts(context, sensorStatistics.getMaxCadence()),
|
||||
null
|
||||
)
|
||||
);
|
||||
sensorDataList.add(
|
||||
new StatisticData(
|
||||
new DataField(context.getString(R.string.stats_custom_layout_average_cadence_key), context.getString(R.string.sensor_state_cadence_avg), true, true, false),
|
||||
StringUtils.getCadenceParts(context, sensorStatistics.getAvgCadence()),
|
||||
null
|
||||
)
|
||||
);
|
||||
}
|
||||
if (sensorStatistics.hasPower()) {
|
||||
sensorDataList.add(
|
||||
new StatisticData(
|
||||
new DataField(context.getString(R.string.stats_custom_layout_power_key), context.getString(R.string.sensor_state_power_avg), true, true, false),
|
||||
StringUtils.getPowerParts(context, sensorStatistics.getAvgPower()),
|
||||
null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return sensorDataList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.dennisguse.opentracks.viewmodels;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Layout;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
|
||||
public class StatisticsDataModel extends AndroidViewModel {
|
||||
|
||||
private MutableLiveData<List<StatisticData>> statsData;
|
||||
|
||||
public StatisticsDataModel(@NonNull Application application) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
public MutableLiveData<List<StatisticData>> getStatsData() {
|
||||
if (statsData == null) {
|
||||
statsData = new MutableLiveData<>();
|
||||
}
|
||||
return statsData;
|
||||
}
|
||||
|
||||
public void update(TrackRecordingService.RecordingData recordingData, Layout layout, boolean metricUnit) {
|
||||
new Thread(() -> {
|
||||
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(getApplication(), recordingData, layout, metricUnit);
|
||||
statsData.postValue(statisticDataList);
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user