From 9c4c8131f5068628327a629dea9b08505c912ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rom=C3=A1n=20Mart=C3=ADnez?= Date: Sat, 19 Jun 2021 21:29:06 +0200 Subject: [PATCH] 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. --- doc/data-structure.txt | 23 +- .../opentracks/EspressoCategoryUnitTest.java | 191 ------- .../opentracks/EspressoCustomLayoutTest.java | 108 ++++ .../opentracks/util/PreferencesUtilsTest.java | 134 ++++- .../opentracks/util/StatisticsUtilsTest.java | 68 +++ .../viewmodels/StatisticDataBuilderTest.java | 195 ++++++++ src/main/AndroidManifest.xml | 1 + .../adapters/CustomLayoutFieldType.java | 16 + .../opentracks/adapters/SensorsAdapter.java | 91 ---- .../adapters/SettingsCustomLayoutAdapter.java | 118 +++++ .../adapters/StatisticsAdapter.java | 140 ++++++ .../opentracks/content/data/DataField.java | 101 ++++ .../opentracks/content/data/Layout.java | 42 ++ .../fragments/MarkerDetailFragment.java | 4 +- .../fragments/StatisticsRecordedFragment.java | 30 +- .../StatisticsRecordingFragment.java | 217 ++------ .../settings/ResetDialogPreference.java | 19 +- .../opentracks/settings/SettingsActivity.java | 7 + .../SettingsCustomLayoutActivity.java | 170 +++++++ .../opentracks/util/CsvConstants.java | 10 + .../util/PreferencesOpenHelper.java | 41 ++ .../opentracks/util/PreferencesUtils.java | 106 +++- .../opentracks/util/StatisticsUtils.java | 31 ++ .../opentracks/util/StringUtils.java | 43 +- .../viewmodels/SensorDataModel.java | 56 --- .../opentracks/viewmodels/StatisticData.java | 45 ++ .../viewmodels/StatisticDataBuilder.java | 205 ++++++++ .../viewmodels/StatisticsDataModel.java | 35 ++ .../ic_baseline_drag_indicator_24.xml | 10 + .../drawable/ic_baseline_visibility_24.xml | 10 + .../ic_baseline_visibility_off_24.xml | 10 + .../activity_settings_custom_layout.xml | 60 +++ .../res/layout/aggregated_stats_list_item.xml | 12 +- src/main/res/layout/custom_stats_item.xml | 49 ++ src/main/res/layout/statistics_recorded.xml | 24 +- src/main/res/layout/statistics_recording.xml | 464 +----------------- src/main/res/layout/stats_item.xml | 47 ++ ...ensor_item.xml => stats_recorded_item.xml} | 45 +- src/main/res/values-ar/strings.xml | 1 - src/main/res/values-b+en+001/strings.xml | 1 - src/main/res/values-b+es+419/strings.xml | 1 - src/main/res/values-bg/strings.xml | 1 - src/main/res/values-ca/strings.xml | 1 - src/main/res/values-cs/strings.xml | 1 - src/main/res/values-da/strings.xml | 1 - src/main/res/values-de/strings.xml | 1 - src/main/res/values-el/strings.xml | 1 - src/main/res/values-es/strings.xml | 1 - src/main/res/values-et/strings.xml | 1 - src/main/res/values-eu/strings.xml | 1 - src/main/res/values-fa/strings.xml | 1 - src/main/res/values-fi/strings.xml | 1 - src/main/res/values-fr-rCA/strings.xml | 1 - src/main/res/values-fr-rCH/strings.xml | 1 - src/main/res/values-fr/strings.xml | 1 - src/main/res/values-gl/strings.xml | 1 - src/main/res/values-hi/strings.xml | 1 - src/main/res/values-hr/strings.xml | 1 - src/main/res/values-hu/strings.xml | 1 - src/main/res/values-in/strings.xml | 1 - src/main/res/values-it/strings.xml | 1 - src/main/res/values-iw/strings.xml | 1 - src/main/res/values-ja/strings.xml | 1 - src/main/res/values-ko/strings.xml | 1 - src/main/res/values-lt/strings.xml | 1 - src/main/res/values-lv/strings.xml | 1 - src/main/res/values-mo/strings.xml | 1 - src/main/res/values-ms/strings.xml | 1 - src/main/res/values-nb/strings.xml | 1 - src/main/res/values-nl/strings.xml | 1 - src/main/res/values-pl/strings.xml | 1 - src/main/res/values-pt-rBR/strings.xml | 1 - src/main/res/values-pt-rPT/strings.xml | 1 - src/main/res/values-pt/strings.xml | 1 - src/main/res/values-ro/strings.xml | 1 - src/main/res/values-ru/strings.xml | 1 - src/main/res/values-sk/strings.xml | 1 - src/main/res/values-sl/strings.xml | 1 - src/main/res/values-sr/strings.xml | 1 - src/main/res/values-sv/strings.xml | 1 - src/main/res/values-th/strings.xml | 1 - src/main/res/values-tl/strings.xml | 1 - src/main/res/values-tr/strings.xml | 1 - src/main/res/values-uk/strings.xml | 1 - src/main/res/values-vi/strings.xml | 1 - src/main/res/values-zh-rHK/strings.xml | 1 - src/main/res/values-zh-rTW/strings.xml | 1 - src/main/res/values-zh/strings.xml | 1 - src/main/res/values/settings.xml | 135 ++++- src/main/res/values/settings_deprecated.xml | 6 + src/main/res/values/strings.xml | 27 +- src/main/res/values/styles.xml | 55 ++- src/main/res/xml/settings.xml | 21 +- 93 files changed, 2146 insertions(+), 1126 deletions(-) delete mode 100644 src/androidTest/java/de/dennisguse/opentracks/EspressoCategoryUnitTest.java create mode 100644 src/androidTest/java/de/dennisguse/opentracks/EspressoCustomLayoutTest.java create mode 100644 src/androidTest/java/de/dennisguse/opentracks/util/StatisticsUtilsTest.java create mode 100644 src/androidTest/java/de/dennisguse/opentracks/viewmodels/StatisticDataBuilderTest.java create mode 100644 src/main/java/de/dennisguse/opentracks/adapters/CustomLayoutFieldType.java delete mode 100644 src/main/java/de/dennisguse/opentracks/adapters/SensorsAdapter.java create mode 100644 src/main/java/de/dennisguse/opentracks/adapters/SettingsCustomLayoutAdapter.java create mode 100644 src/main/java/de/dennisguse/opentracks/adapters/StatisticsAdapter.java create mode 100644 src/main/java/de/dennisguse/opentracks/content/data/DataField.java create mode 100644 src/main/java/de/dennisguse/opentracks/content/data/Layout.java create mode 100644 src/main/java/de/dennisguse/opentracks/settings/SettingsCustomLayoutActivity.java create mode 100644 src/main/java/de/dennisguse/opentracks/util/CsvConstants.java create mode 100644 src/main/java/de/dennisguse/opentracks/util/PreferencesOpenHelper.java create mode 100644 src/main/java/de/dennisguse/opentracks/util/StatisticsUtils.java delete mode 100644 src/main/java/de/dennisguse/opentracks/viewmodels/SensorDataModel.java create mode 100644 src/main/java/de/dennisguse/opentracks/viewmodels/StatisticData.java create mode 100644 src/main/java/de/dennisguse/opentracks/viewmodels/StatisticDataBuilder.java create mode 100644 src/main/java/de/dennisguse/opentracks/viewmodels/StatisticsDataModel.java create mode 100644 src/main/res/drawable/ic_baseline_drag_indicator_24.xml create mode 100644 src/main/res/drawable/ic_baseline_visibility_24.xml create mode 100644 src/main/res/drawable/ic_baseline_visibility_off_24.xml create mode 100644 src/main/res/layout/activity_settings_custom_layout.xml create mode 100644 src/main/res/layout/custom_stats_item.xml create mode 100644 src/main/res/layout/stats_item.xml rename src/main/res/layout/{sensor_item.xml => stats_recorded_item.xml} (53%) diff --git a/doc/data-structure.txt b/doc/data-structure.txt index d43eb8301..73c8d587d 100644 --- a/doc/data-structure.txt +++ b/doc/data-structure.txt @@ -43,7 +43,6 @@ class TrackPoint { - sensor_power } - class Marker { - id - trackId @@ -58,6 +57,26 @@ class Marker { - photoUrl } +class DataField { + - key + - title + - isVisible + - isPrimary + - isWide +} + +class StatisticData { + - value + - unit + - description +} +StatisticData o-- DataField + +class Layout { + - profile +} +Layout "1" o-- "many" DataField + note left of TrackPoint As of OpenTracks version 3.15.0, all times are using device time. Before that TrackPoint.time contained GPS time (determined by GPS hardware). @@ -77,4 +96,4 @@ Trackpoints of Type.SEGMENT_START_MANUAL or Type.SEGMENT_END_MANUAL do not conta Tracks recorded prior to OpenTracks version 3.15.0, do neither begin with a Type.SEGMENT_START_MANUAL nor end with a Type.SEGMENT_END_MANUAL. end note -@enduml \ No newline at end of file +@enduml diff --git a/src/androidTest/java/de/dennisguse/opentracks/EspressoCategoryUnitTest.java b/src/androidTest/java/de/dennisguse/opentracks/EspressoCategoryUnitTest.java deleted file mode 100644 index aa0a7f9e4..000000000 --- a/src/androidTest/java/de/dennisguse/opentracks/EspressoCategoryUnitTest.java +++ /dev/null @@ -1,191 +0,0 @@ -package de.dennisguse.opentracks; - - -import static androidx.test.espresso.Espresso.onData; -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.action.ViewActions.scrollTo; -import static androidx.test.espresso.assertion.ViewAssertions.matches; -import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.withClassName; -import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static androidx.test.espresso.matcher.ViewMatchers.withText; -import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.anything; -import static org.hamcrest.Matchers.is; - -import android.content.Context; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewParent; - -import androidx.test.espresso.DataInteraction; -import androidx.test.espresso.UiController; -import androidx.test.espresso.ViewAction; -import androidx.test.espresso.ViewInteraction; -import androidx.test.ext.junit.rules.ActivityScenarioRule; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.LargeTest; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.rule.GrantPermissionRule; - -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.hamcrest.TypeSafeMatcher; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestRule; -import org.junit.runner.RunWith; - -/** - * Espresso test that checks the unit for activity category selected. - * Also it checks that unit is properly when user change from a pace activity to an speed one. - */ -@LargeTest -@RunWith(AndroidJUnit4.class) -public class EspressoCategoryUnitTest { - - @Rule - public ActivityScenarioRule mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class); - - @Rule - public TestRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION); - - private final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - - @Rule - public TestRule mLocaleRule = new PreferenceMetricUnitRule(context, true); - - @Test - public void changeBetweenCategoryUpdateProperlyUnitTest() { - // Start record. - ViewInteraction appCompatImageButton = onView( - allOf(withId(R.id.controller_record), withContentDescription("Record"), - childAtPosition( - allOf(withId(R.id.controller_container), - childAtPosition( - withId(R.id.controller_fragment), - 0)), - 0), - isDisplayed())); - appCompatImageButton.perform(click()); - - { - // Change to running activity (pace activity). - ViewInteraction appCompatImageView = onView( - allOf(withId(R.id.stats_activity_type_icon), - childAtPosition( - childAtPosition( - withClassName(is("android.widget.ScrollView")), - 0), - 7))); - appCompatImageView.perform(scrollTo(), click()); - - DataInteraction imageView = onData(anything()) - .inAdapterView(allOf(withId(R.id.choose_activity_type_grid_view), - childAtPosition( - withClassName(is("android.widget.LinearLayout")), - 0))) - .atPosition(1); - imageView.perform(click()); - } - - // Pace (min/km) in all fields. - { - ViewInteraction textView = onView(allOf(withId(R.id.stats_speed_unit))); - textView.perform(waitFor(2000)); - textView.check(matches(withText("min/km"))); - } - { - ViewInteraction textView = onView(allOf(withId(R.id.stats_average_speed_unit))); - textView.check(matches(withText("min/km"))); - } - { - ViewInteraction textView = onView(allOf(withId(R.id.stats_max_speed_unit))); - textView.check(matches(withText("min/km"))); - } - { - ViewInteraction textView = onView(allOf(withId(R.id.stats_moving_speed_unit))); - textView.check(matches(withText("min/km"))); - } - - { - // Change to biking activity (speed activity). - ViewInteraction appCompatImageView = onView( - allOf(withId(R.id.stats_activity_type_icon), - childAtPosition( - childAtPosition( - withClassName(is("android.widget.ScrollView")), - 0), - 7))); - appCompatImageView.perform(scrollTo(), click()); - - DataInteraction imageView = onData(anything()) - .inAdapterView(allOf(withId(R.id.choose_activity_type_grid_view), - childAtPosition( - withClassName(is("android.widget.LinearLayout")), - 0))) - .atPosition(9); - imageView.perform(click()); - } - - // Speed (km/h) in all fields. - { - ViewInteraction textView = onView(allOf(withId(R.id.stats_speed_unit))); - textView.perform(waitFor(2000)); - textView.check(matches(withText("km/h"))); - } - { - ViewInteraction textView = onView(allOf(withId(R.id.stats_average_speed_unit))); - textView.check(matches(withText("km/h"))); - } - { - ViewInteraction textView = onView(allOf(withId(R.id.stats_max_speed_unit))); - textView.check(matches(withText("km/h"))); - } - { - ViewInteraction textView = onView(allOf(withId(R.id.stats_moving_speed_unit))); - textView.check(matches(withText("km/h"))); - } - } - - private static Matcher childAtPosition( - final Matcher parentMatcher, final int position) { - - return new TypeSafeMatcher<>() { - @Override - public void describeTo(Description description) { - description.appendText("Child at position " + position + " in parent "); - parentMatcher.describeTo(description); - } - - @Override - public boolean matchesSafely(View view) { - ViewParent parent = view.getParent(); - return parent instanceof ViewGroup && parentMatcher.matches(parent) - && view.equals(((ViewGroup) parent).getChildAt(position)); - } - }; - } - - private static ViewAction waitFor(final long duration_ms) { - return new ViewAction() { - - @Override - public String getDescription() { - return "Wait for milliseconds."; - } - - @Override - public Matcher getConstraints() { - return isDisplayed(); - } - - @Override - public void perform(UiController uiController, final View view) { - uiController.loopMainThreadForAtLeast(duration_ms); - } - }; - } -} diff --git a/src/androidTest/java/de/dennisguse/opentracks/EspressoCustomLayoutTest.java b/src/androidTest/java/de/dennisguse/opentracks/EspressoCustomLayoutTest.java new file mode 100644 index 000000000..ccfae0130 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/EspressoCustomLayoutTest.java @@ -0,0 +1,108 @@ +package de.dennisguse.opentracks; + + +import androidx.recyclerview.widget.RecyclerView; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.espresso.ViewAssertion; +import androidx.test.espresso.ViewInteraction; +import androidx.test.filters.LargeTest; +import androidx.test.rule.ActivityTestRule; +import androidx.test.rule.GrantPermissionRule; +import androidx.test.runner.AndroidJUnit4; + +import android.content.Context; +import android.content.SharedPreferences; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.*; +import static androidx.test.espresso.matcher.ViewMatchers.*; + +import de.dennisguse.opentracks.content.data.DataField; +import de.dennisguse.opentracks.content.data.Layout; +import de.dennisguse.opentracks.util.PreferencesUtils; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.allOf; + +@LargeTest +@RunWith(AndroidJUnit4.class) +public class EspressoCustomLayoutTest { + + private final Context context = ApplicationProvider.getApplicationContext(); + + @Rule + public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(TrackListActivity.class); + + @Rule + public GrantPermissionRule mGrantPermissionRule = + GrantPermissionRule.grant( + "android.permission.ACCESS_FINE_LOCATION"); + + @Test + public void customLayoutTest() { + ViewInteraction appCompatImageButton = onView( + allOf(withId(R.id.controller_record), withContentDescription("Record"), + childAtPosition( + allOf(withId(R.id.controller_container), + childAtPosition( + withId(R.id.controller_fragment), + 0)), + 0), + isDisplayed())); + appCompatImageButton.perform(click()); + + SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); + Layout layout = PreferencesUtils.getCustomLayout(sharedPreferences, context); + + onView(withId(R.id.stats_recycler_view)).check(new RecyclerViewItemCountAssertion((int) layout.getFields().stream().filter(DataField::isVisible).count())); + } + + private static class RecyclerViewItemCountAssertion implements ViewAssertion { + private final int expectedCount; + + public RecyclerViewItemCountAssertion(int expectedCount) { + this.expectedCount = expectedCount; + } + + @Override + public void check(View view, NoMatchingViewException noViewFoundException) { + if (noViewFoundException != null) { + throw noViewFoundException; + } + + RecyclerView recyclerView = (RecyclerView) view; + RecyclerView.Adapter adapter = recyclerView.getAdapter(); + assertThat(adapter.getItemCount(), is(expectedCount)); + } + } + + private static Matcher childAtPosition( + final Matcher parentMatcher, final int position) { + + return new TypeSafeMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("Child at position " + position + " in parent "); + parentMatcher.describeTo(description); + } + + @Override + public boolean matchesSafely(View view) { + ViewParent parent = view.getParent(); + return parent instanceof ViewGroup && parentMatcher.matches(parent) + && view.equals(((ViewGroup) parent).getChildAt(position)); + } + }; + } +} diff --git a/src/androidTest/java/de/dennisguse/opentracks/util/PreferencesUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/util/PreferencesUtilsTest.java index 886cf2e6e..a3a9cdb54 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/util/PreferencesUtilsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/util/PreferencesUtilsTest.java @@ -10,9 +10,14 @@ import org.junit.Test; import org.junit.runner.RunWith; import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.content.data.DataField; +import de.dennisguse.opentracks.content.data.Layout; import de.dennisguse.opentracks.io.file.TrackFileFormat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; @RunWith(AndroidJUnit4.class) public class PreferencesUtilsTest { @@ -64,4 +69,131 @@ public class PreferencesUtilsTest { // then assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat); } -} \ No newline at end of file + + @Test + public void testGetCustomLayout_default() { + // given + SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.clear(); + editor.commit(); + + // when + Layout layout = PreferencesUtils.getCustomLayout(sharedPreferences, context); + + // then + assertTrue(layout.getFields().size() > 0); + assertEquals(layout.getProfile(), context.getString(R.string.default_activity_default)); + assertTrue(layout.getFields().stream().anyMatch(DataField::isVisible)); + } + + @Test + public void testGetCustomLayout_1() { + // given + SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString( + context.getString(R.string.stats_custom_layout_fields_key), + "run;" + + context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_distance_key) + ",1,0;" + + context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,1;" + + context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;"); + editor.apply(); + + // when + Layout layout = PreferencesUtils.getCustomLayout(sharedPreferences, context); + + // then + assertEquals(layout.getFields().size(), 4); + assertEquals(layout.getProfile(), "run"); + + assertEquals(layout.getFields().get(0).getKey(), context.getString(R.string.stats_custom_layout_moving_time_key)); + assertTrue(layout.getFields().get(0).isVisible()); + assertTrue(layout.getFields().get(0).isPrimary()); + + assertEquals(layout.getFields().get(1).getKey(), context.getString(R.string.stats_custom_layout_distance_key)); + assertTrue(layout.getFields().get(1).isVisible()); + assertFalse(layout.getFields().get(1).isPrimary()); + + assertEquals(layout.getFields().get(2).getKey(), context.getString(R.string.stats_custom_layout_average_moving_speed_key)); + assertFalse(layout.getFields().get(2).isVisible()); + assertTrue(layout.getFields().get(2).isPrimary()); + + assertEquals(layout.getFields().get(3).getKey(), context.getString(R.string.stats_custom_layout_speed_key)); + assertFalse(layout.getFields().get(3).isVisible()); + assertFalse(layout.getFields().get(3).isPrimary()); + } + + @Test + public void testGetCustomLayout_coordinatesIsWide() { + // given + SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString( + context.getString(R.string.stats_custom_layout_fields_key), + "walking;" + + context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_distance_key) + ",1,0;" + + context.getString(R.string.stats_custom_layout_coordinates_key) + ",0,1;" + + context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;"); + editor.apply(); + + // when + Layout layout = PreferencesUtils.getCustomLayout(sharedPreferences, context); + + // then + assertEquals(layout.getFields().size(), 4); + assertEquals(layout.getProfile(), "walking"); + + assertEquals(layout.getFields().get(0).getKey(), context.getString(R.string.stats_custom_layout_moving_time_key)); + assertTrue(layout.getFields().get(0).isVisible()); + assertTrue(layout.getFields().get(0).isPrimary()); + + assertEquals(layout.getFields().get(1).getKey(), context.getString(R.string.stats_custom_layout_distance_key)); + assertTrue(layout.getFields().get(1).isVisible()); + assertFalse(layout.getFields().get(1).isPrimary()); + + assertEquals(layout.getFields().get(2).getKey(), context.getString(R.string.stats_custom_layout_coordinates_key)); + assertFalse(layout.getFields().get(2).isVisible()); + assertTrue(layout.getFields().get(2).isPrimary()); + assertTrue(layout.getFields().get(2).isWide()); + + assertEquals(layout.getFields().get(3).getKey(), context.getString(R.string.stats_custom_layout_speed_key)); + assertFalse(layout.getFields().get(3).isVisible()); + assertFalse(layout.getFields().get(3).isPrimary()); + } + + @Test + public void testSetCustomLayout() { + // given + SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); + Layout layoutSrc = new Layout("road cycling"); + layoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_moving_time_key), context.getString(R.string.stats_moving_time), true, true, false)); + layoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_distance_key), context.getString(R.string.stats_distance), true, false, false)); + layoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_average_moving_speed_key), context.getString(R.string.stats_average_moving_speed), false, true, false)); + layoutSrc.addField(new DataField(context.getString(R.string.stats_custom_layout_speed_key), context.getString(R.string.stats_speed), false, false, false)); + + // when + PreferencesUtils.setCustomLayout(sharedPreferences, context, layoutSrc); + + // then + String csv = sharedPreferences.getString(context.getString(R.string.stats_custom_layout_fields_key), null); + assertNotNull(csv); + assertEquals(csv, + "road cycling;" + + context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_distance_key) + ",1,0;" + + context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,1;" + + context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;"); + + Layout layoutDst = PreferencesUtils.getCustomLayout(sharedPreferences, context); + assertEquals(layoutSrc.getProfile(), layoutDst.getProfile()); + assertEquals(layoutSrc.getFields().size(), layoutDst.getFields().size()); + for (int i = 0; i < layoutSrc.getFields().size(); i++) { + assertEquals(layoutSrc.getFields().get(i).getKey(), layoutDst.getFields().get(i).getKey()); + assertEquals(layoutSrc.getFields().get(i).isVisible(), layoutDst.getFields().get(i).isVisible()); + assertEquals(layoutSrc.getFields().get(i).isPrimary(), layoutDst.getFields().get(i).isPrimary()); + } + } +} diff --git a/src/androidTest/java/de/dennisguse/opentracks/util/StatisticsUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/util/StatisticsUtilsTest.java new file mode 100644 index 000000000..b7dd74e38 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/util/StatisticsUtilsTest.java @@ -0,0 +1,68 @@ +package de.dennisguse.opentracks.util; + +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import junit.framework.TestCase; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.content.data.DataField; +import de.dennisguse.opentracks.content.data.Layout; + +@RunWith(AndroidJUnit4.class) +public class StatisticsUtilsTest extends TestCase { + + private final Context context = ApplicationProvider.getApplicationContext(); + + @Test + public void testEmptyValue() { + assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_distance_key))); + assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_total_time_key))); + assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_speed_key))); + assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_moving_time_key))); + assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_speed_key))); + assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_max_speed_key))); + assertEquals(context.getString(R.string.stats_empty_value_float), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_moving_speed_key))); + assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_altitude_key))); + assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_gain_key))); + assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_loss_key))); + assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_pace_key))); + assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_moving_pace_key))); + assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_average_pace_key))); + assertEquals(context.getString(R.string.stats_empty_value_time), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_fastest_pace_key))); + assertEquals(context.getString(R.string.stats_empty_value_coordinates), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_coordinates_key))); + assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_heart_rate_key))); + assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_cadence_key))); + assertEquals(context.getString(R.string.stats_empty_value_integer), StatisticsUtils.emptyValue(context, context.getString(R.string.stats_custom_layout_power_key))); + } + + @Test + public void testFilterVisible() { + // given + Layout layout = new Layout("profile"); + layout.addField(new DataField("key1", "title1", true, true, false)); + layout.addField(new DataField("key2", "title2", false, true, false)); + layout.addField(new DataField("key3", "title3", true, true, false)); + layout.addField(new DataField("key4", "title4", false, true, false)); + layout.addField(new DataField("key5", "title5", true, true, false)); + + // when + Layout resultTrue = StatisticsUtils.filterVisible(layout, true); + Layout resultFalse = StatisticsUtils.filterVisible(layout, false); + + // then + assertEquals(resultTrue.getFields().size(), 3); + assertTrue(resultTrue.getFields().stream().anyMatch(f -> f.getKey().equals("key1"))); + assertTrue(resultTrue.getFields().stream().anyMatch(f -> f.getKey().equals("key3"))); + assertTrue(resultTrue.getFields().stream().anyMatch(f -> f.getKey().equals("key5"))); + + assertEquals(resultFalse.getFields().size(), 2); + assertTrue(resultFalse.getFields().stream().anyMatch(f -> f.getKey().equals("key2"))); + assertTrue(resultFalse.getFields().stream().anyMatch(f -> f.getKey().equals("key4"))); + } +} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/viewmodels/StatisticDataBuilderTest.java b/src/androidTest/java/de/dennisguse/opentracks/viewmodels/StatisticDataBuilderTest.java new file mode 100644 index 000000000..d36e6126d --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/viewmodels/StatisticDataBuilderTest.java @@ -0,0 +1,195 @@ +package de.dennisguse.opentracks.viewmodels; + +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; + +import junit.framework.TestCase; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.time.Duration; +import java.util.List; + +import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.content.data.Distance; +import de.dennisguse.opentracks.content.data.Layout; +import de.dennisguse.opentracks.content.data.Track; +import de.dennisguse.opentracks.services.TrackRecordingService; +import de.dennisguse.opentracks.stats.SensorStatistics; +import de.dennisguse.opentracks.stats.TrackStatistics; + +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class StatisticDataBuilderTest extends TestCase { + + private final Context context = ApplicationProvider.getApplicationContext(); + + @Mock + private Track trackMock; + + @Mock + private TrackStatistics trackStatisticsMock; + + @Mock + private TrackRecordingService.RecordingData recordingDataMock; + + @Mock + private SensorStatistics sensorStatisticsMock; + + @Test + public void testFromRecordingData() { + when(trackMock.getTrackStatistics()).thenReturn(trackStatisticsMock); + when(trackStatisticsMock.getTotalTime()).thenReturn(Duration.ofMillis(0)); + when(trackStatisticsMock.getMovingTime()).thenReturn(Duration.ofMillis(0)); + when(trackStatisticsMock.getTotalDistance()).thenReturn(Distance.of(0)); + when(recordingDataMock.getTrackStatistics()).thenReturn(trackStatisticsMock); + + // given + Layout layout = new Layout(context.getString(R.string.default_activity_default)); + layout.addField(context.getString(R.string.stats_custom_layout_total_time_key), context.getString(R.string.stats_total_time), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_moving_time_key), context.getString(R.string.stats_moving_time), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_distance_key), context.getString(R.string.stats_distance), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_speed_key), context.getString(R.string.stats_speed), false, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_max_speed_key), context.getString(R.string.stats_max_speed), false, true, false); + + // when + List statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, true); + + // then + assertEquals(statisticDataList.size(), 3); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_total_time)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_moving_time)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_distance)))); + } + + @Test + public void testFromRecordingData_allFields() { + when(trackMock.getTrackStatistics()).thenReturn(trackStatisticsMock); + when(trackStatisticsMock.getTotalTime()).thenReturn(Duration.ofMillis(0)); + when(trackStatisticsMock.getMovingTime()).thenReturn(Duration.ofMillis(0)); + when(trackStatisticsMock.getTotalDistance()).thenReturn(Distance.of(0)); + when(recordingDataMock.getTrackStatistics()).thenReturn(trackStatisticsMock); + + // given + Layout layout = new Layout(context.getString(R.string.default_activity_default)); + layout.addField(context.getString(R.string.stats_custom_layout_total_time_key), context.getString(R.string.stats_total_time), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_moving_time_key), context.getString(R.string.stats_moving_time), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_distance_key), context.getString(R.string.stats_distance), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_speed_key), context.getString(R.string.stats_speed), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_average_moving_speed_key), context.getString(R.string.stats_average_moving_speed), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_average_speed_key), context.getString(R.string.stats_average_speed), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_max_speed_key), context.getString(R.string.stats_max_speed), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_pace_key), context.getString(R.string.stats_pace), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_average_moving_pace_key), context.getString(R.string.stats_average_moving_pace), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_average_pace_key), context.getString(R.string.stats_average_pace), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_fastest_pace_key), context.getString(R.string.stats_fastest_pace), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_altitude_key), context.getString(R.string.stats_altitude), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_gain_key), context.getString(R.string.stats_gain), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_loss_key), context.getString(R.string.stats_loss), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_coordinates_key), context.getString(R.string.stats_coordinates), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_heart_rate_key), context.getString(R.string.stats_sensors_heart_rate), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_cadence_key), context.getString(R.string.stats_sensors_cadence), true, true, false); + layout.addField(context.getString(R.string.stats_custom_layout_power_key), context.getString(R.string.stats_sensors_power), true, true, false); + + // when + List statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, true); + + // then + assertEquals(statisticDataList.size(), 18); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_total_time)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_moving_time)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_distance)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_speed)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_average_moving_speed)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_average_speed)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_max_speed)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_pace)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_average_moving_pace)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_average_pace)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_fastest_pace)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_altitude)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_gain)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_loss)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_coordinates)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_heart_rate)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_cadence)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_power)))); + } + + @Test + public void testFromSensorStatistics_onlyHeartRate() { + when(sensorStatisticsMock.hasHeartRate()).thenReturn(true); + when(sensorStatisticsMock.getMaxHeartRate()).thenReturn(200f); + when(sensorStatisticsMock.getAvgHeartRate()).thenReturn(150f); + when(sensorStatisticsMock.hasCadence()).thenReturn(false); + when(sensorStatisticsMock.hasPower()).thenReturn(false); + + // when + List statisticDataList = StatisticDataBuilder.fromSensorStatistics(context, sensorStatisticsMock); + + // then + assertEquals(statisticDataList.size(), 2); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_heart_rate_max)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_heart_rate_avg)))); + } + + @Test + public void testFromSensorStatistics_onlyCadence() { + when(sensorStatisticsMock.hasHeartRate()).thenReturn(false); + when(sensorStatisticsMock.hasCadence()).thenReturn(true); + when(sensorStatisticsMock.getAvgCadence()).thenReturn(90f); + when(sensorStatisticsMock.getMaxCadence()).thenReturn(110f); + when(sensorStatisticsMock.hasPower()).thenReturn(false); + + // when + List statisticDataList = StatisticDataBuilder.fromSensorStatistics(context, sensorStatisticsMock); + + // then + assertEquals(statisticDataList.size(), 2); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_cadence_max)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_cadence_avg)))); + } + + @Test + public void testFromSensorStatistics_onlyPower() { + when(sensorStatisticsMock.hasHeartRate()).thenReturn(false); + when(sensorStatisticsMock.hasCadence()).thenReturn(false); + when(sensorStatisticsMock.hasPower()).thenReturn(true); + when(sensorStatisticsMock.getAvgPower()).thenReturn(300f); + + // when + List statisticDataList = StatisticDataBuilder.fromSensorStatistics(context, sensorStatisticsMock); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_power_avg)))); + + // then + assertEquals(statisticDataList.size(), 1); + } + + @Test + public void testFromSensorStatistics() { + when(sensorStatisticsMock.hasHeartRate()).thenReturn(true); + when(sensorStatisticsMock.getMaxHeartRate()).thenReturn(200f); + when(sensorStatisticsMock.getAvgHeartRate()).thenReturn(150f); + when(sensorStatisticsMock.hasCadence()).thenReturn(true); + when(sensorStatisticsMock.getAvgCadence()).thenReturn(90f); + when(sensorStatisticsMock.getMaxCadence()).thenReturn(110f); + when(sensorStatisticsMock.hasPower()).thenReturn(true); + when(sensorStatisticsMock.getAvgPower()).thenReturn(300f); + + // when + List statisticDataList = StatisticDataBuilder.fromSensorStatistics(context, sensorStatisticsMock); + + // then + assertEquals(statisticDataList.size(), 5); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_heart_rate_max)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_heart_rate_avg)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_cadence_max)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_cadence_avg)))); + assertTrue(statisticDataList.stream().anyMatch(i -> i.getField().getTitle().equals(context.getString(R.string.sensor_state_power_avg)))); + } +} diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 4e7c8b3c8..c4ffd9225 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -316,6 +316,7 @@ limitations under the License. + { - - private List 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 swapData(List 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())); - } - } -} diff --git a/src/main/java/de/dennisguse/opentracks/adapters/SettingsCustomLayoutAdapter.java b/src/main/java/de/dennisguse/opentracks/adapters/SettingsCustomLayoutAdapter.java new file mode 100644 index 000000000..d9cb9fb44 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/adapters/SettingsCustomLayoutAdapter.java @@ -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 { + + 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 optionalField = layout.getFields().stream().filter(f -> f.getKey().equals(statTitle)).findFirst(); + optionalField.ifPresent(itemClickListener::onSettingsCustomLayoutItemClicked); + } + } + + public interface SettingsCustomLayoutItemClickListener { + void onSettingsCustomLayoutItemClicked(@NonNull DataField field); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/adapters/StatisticsAdapter.java b/src/main/java/de/dennisguse/opentracks/adapters/StatisticsAdapter.java new file mode 100644 index 000000000..8062c8e40 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/adapters/StatisticsAdapter.java @@ -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 { + + private List 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 swapData(List 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); + } + } + } +} diff --git a/src/main/java/de/dennisguse/opentracks/content/data/DataField.java b/src/main/java/de/dennisguse/opentracks/content/data/DataField.java new file mode 100644 index 000000000..1d95a2640 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/content/data/DataField.java @@ -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); + } + } +} diff --git a/src/main/java/de/dennisguse/opentracks/content/data/Layout.java b/src/main/java/de/dennisguse/opentracks/content/data/Layout.java new file mode 100644 index 000000000..fdc7d7701 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/content/data/Layout.java @@ -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 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 dataFields) { + this.dataFields.addAll(dataFields); + } + + public void removeField(DataField dataField) { + dataFields.remove(dataField); + } + + public List 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; + } +} diff --git a/src/main/java/de/dennisguse/opentracks/fragments/MarkerDetailFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/MarkerDetailFragment.java index fae1fca34..1d5663e38 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/MarkerDetailFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/MarkerDetailFragment.java @@ -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); } diff --git a/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordedFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordedFragment.java index 465e1775e..5b74ed2c5 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordedFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordedFragment.java @@ -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 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 sensorDataList = StatisticDataBuilder.fromSensorStatistics(getContext(), sensorStatistics); if (sensorDataList.size() > 0) { sensorsAdapter.swapData(sensorDataList); } diff --git a/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordingFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordingFragment.java index e67281f54..c3022465b 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordingFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordingFragment.java @@ -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> 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 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 parts = StringUtils.getAltitudeParts(getContext(), recordingData.getTrackStatistics().getTotalAltitudeGain(), preferenceMetricUnits); - viewBinding.statsAltitudeGainValue.setText(parts.first); - viewBinding.statsAltitudeGainUnit.setText(parts.second); - } - { - Pair 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 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 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 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 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 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 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(); diff --git a/src/main/java/de/dennisguse/opentracks/settings/ResetDialogPreference.java b/src/main/java/de/dennisguse/opentracks/settings/ResetDialogPreference.java index eca722534..6c30b6b7e 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/ResetDialogPreference.java +++ b/src/main/java/de/dennisguse/opentracks/settings/ResetDialogPreference.java @@ -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(); } } } diff --git a/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java b/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java index 3cf9f6f8a..1aa545404 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java +++ b/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java @@ -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(); diff --git a/src/main/java/de/dennisguse/opentracks/settings/SettingsCustomLayoutActivity.java b/src/main/java/de/dennisguse/opentracks/settings/SettingsCustomLayoutActivity.java new file mode 100644 index 000000000..1f564b4d1 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/settings/SettingsCustomLayoutActivity.java @@ -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 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); + } + } +} diff --git a/src/main/java/de/dennisguse/opentracks/util/CsvConstants.java b/src/main/java/de/dennisguse/opentracks/util/CsvConstants.java new file mode 100644 index 000000000..2835b5aae --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/util/CsvConstants.java @@ -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 = ","; +} diff --git a/src/main/java/de/dennisguse/opentracks/util/PreferencesOpenHelper.java b/src/main/java/de/dennisguse/opentracks/util/PreferencesOpenHelper.java new file mode 100644 index 000000000..87e986525 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/util/PreferencesOpenHelper.java @@ -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)); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/util/PreferencesUtils.java b/src/main/java/de/dennisguse/opentracks/util/PreferencesUtils.java index 365a838a7..7188d3665 100644 --- a/src/main/java/de/dennisguse/opentracks/util/PreferencesUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/PreferencesUtils.java @@ -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 getMultiTypedArray(Context context, String key) { + List typedArrays = new ArrayList<>(); + + try { + Class 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 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 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 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(); + } + } } diff --git a/src/main/java/de/dennisguse/opentracks/util/StatisticsUtils.java b/src/main/java/de/dennisguse/opentracks/util/StatisticsUtils.java new file mode 100644 index 000000000..0c3848de0 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/util/StatisticsUtils.java @@ -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; + } +} diff --git a/src/main/java/de/dennisguse/opentracks/util/StringUtils.java b/src/main/java/de/dennisguse/opentracks/util/StringUtils.java index 5572bfac5..6a4b1e697 100644 --- a/src/main/java/de/dennisguse/opentracks/util/StringUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/StringUtils.java @@ -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 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 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 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. * diff --git a/src/main/java/de/dennisguse/opentracks/viewmodels/SensorDataModel.java b/src/main/java/de/dennisguse/opentracks/viewmodels/SensorDataModel.java deleted file mode 100644 index aa5a350e3..000000000 --- a/src/main/java/de/dennisguse/opentracks/viewmodels/SensorDataModel.java +++ /dev/null @@ -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 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; - } -} diff --git a/src/main/java/de/dennisguse/opentracks/viewmodels/StatisticData.java b/src/main/java/de/dennisguse/opentracks/viewmodels/StatisticData.java new file mode 100644 index 000000000..db703d26b --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/viewmodels/StatisticData.java @@ -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 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; + } +} diff --git a/src/main/java/de/dennisguse/opentracks/viewmodels/StatisticDataBuilder.java b/src/main/java/de/dennisguse/opentracks/viewmodels/StatisticDataBuilder.java new file mode 100644 index 000000000..40318dc83 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/viewmodels/StatisticDataBuilder.java @@ -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 fromRecordingData(@NonNull Context context, @NonNull TrackRecordingService.RecordingData recordingData, @NonNull Layout layout, boolean metricUnits) { + List 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 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 getSensorStatsDataIfNeeded(Context context, TrackRecordingService.RecordingData recordingData, List statisticDataList, boolean metricUnits) { + List 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 fromSensorStatistics(@NonNull Context context, @NonNull SensorStatistics sensorStatistics) { + List 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; + } +} diff --git a/src/main/java/de/dennisguse/opentracks/viewmodels/StatisticsDataModel.java b/src/main/java/de/dennisguse/opentracks/viewmodels/StatisticsDataModel.java new file mode 100644 index 000000000..e6ebde83e --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/viewmodels/StatisticsDataModel.java @@ -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> statsData; + + public StatisticsDataModel(@NonNull Application application) { + super(application); + } + + public MutableLiveData> getStatsData() { + if (statsData == null) { + statsData = new MutableLiveData<>(); + } + return statsData; + } + + public void update(TrackRecordingService.RecordingData recordingData, Layout layout, boolean metricUnit) { + new Thread(() -> { + List statisticDataList = StatisticDataBuilder.fromRecordingData(getApplication(), recordingData, layout, metricUnit); + statsData.postValue(statisticDataList); + }).start(); + } +} diff --git a/src/main/res/drawable/ic_baseline_drag_indicator_24.xml b/src/main/res/drawable/ic_baseline_drag_indicator_24.xml new file mode 100644 index 000000000..6dcde80e4 --- /dev/null +++ b/src/main/res/drawable/ic_baseline_drag_indicator_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/main/res/drawable/ic_baseline_visibility_24.xml b/src/main/res/drawable/ic_baseline_visibility_24.xml new file mode 100644 index 000000000..a3e222a2d --- /dev/null +++ b/src/main/res/drawable/ic_baseline_visibility_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/main/res/drawable/ic_baseline_visibility_off_24.xml b/src/main/res/drawable/ic_baseline_visibility_off_24.xml new file mode 100644 index 000000000..92c48569f --- /dev/null +++ b/src/main/res/drawable/ic_baseline_visibility_off_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/main/res/layout/activity_settings_custom_layout.xml b/src/main/res/layout/activity_settings_custom_layout.xml new file mode 100644 index 000000000..7061a9a61 --- /dev/null +++ b/src/main/res/layout/activity_settings_custom_layout.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/aggregated_stats_list_item.xml b/src/main/res/layout/aggregated_stats_list_item.xml index 1975cb0fd..732fb83b7 100644 --- a/src/main/res/layout/aggregated_stats_list_item.xml +++ b/src/main/res/layout/aggregated_stats_list_item.xml @@ -42,7 +42,7 @@ - - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/statistics_recorded.xml b/src/main/res/layout/statistics_recorded.xml index 83aef9826..a840ca5f6 100644 --- a/src/main/res/layout/statistics_recorded.xml +++ b/src/main/res/layout/statistics_recorded.xml @@ -88,7 +88,7 @@ - + style="@style/StatsScrollView" + android:layout_width="match_parent" + android:layout_height="wrap_content"> - + android:layout_height="match_parent" + app:layout_constraintEnd_toStartOf="parent" + app:layout_constraintStart_toEndOf="parent" + app:layout_constraintTop_toBottomOf="parent" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + diff --git a/src/main/res/layout/stats_item.xml b/src/main/res/layout/stats_item.xml new file mode 100644 index 000000000..ec5629f81 --- /dev/null +++ b/src/main/res/layout/stats_item.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/sensor_item.xml b/src/main/res/layout/stats_recorded_item.xml similarity index 53% rename from src/main/res/layout/sensor_item.xml rename to src/main/res/layout/stats_recorded_item.xml index b2c80e31c..80d8d5b0f 100644 --- a/src/main/res/layout/sensor_item.xml +++ b/src/main/res/layout/stats_recorded_item.xml @@ -1,42 +1,47 @@ - + android:paddingBottom="16dp" + android:paddingTop="16dp"> - - + app:layout_constraintBottom_toBottomOf="@id/stats_value" /> + + \ No newline at end of file diff --git a/src/main/res/values-ar/strings.xml b/src/main/res/values-ar/strings.xml index d430c438b..0fd48c5a3 100644 --- a/src/main/res/values-ar/strings.xml +++ b/src/main/res/values-ar/strings.xml @@ -217,7 +217,6 @@ limitations under the License. البلوتوث - خط العرض/خط الطول المعدل المفضل وحدات القياس البريطانية (ميل، قدم) وحدات القياس المترية (كم، م) diff --git a/src/main/res/values-b+en+001/strings.xml b/src/main/res/values-b+en+001/strings.xml index 714d63b12..062e0e5bd 100644 --- a/src/main/res/values-b+en+001/strings.xml +++ b/src/main/res/values-b+en+001/strings.xml @@ -176,7 +176,6 @@ Reset now? All settings have been reverted to default values Bluetooth - Latitude/Longitude Preferred rate Imperial (miles, ft) Metric (km, m) diff --git a/src/main/res/values-b+es+419/strings.xml b/src/main/res/values-b+es+419/strings.xml index bde314f9b..bbbed4b5b 100644 --- a/src/main/res/values-b+es+419/strings.xml +++ b/src/main/res/values-b+es+419/strings.xml @@ -178,7 +178,6 @@ ¿Restablecer ahora? Se restablecieron los valores predeterminados de todas las opciones de configuración Bluetooth - Latitud/longitud Tasa preferida Imperiales (millas, pies) Métricas (km, m) diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index 3abb68f4d..ae3c014c8 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -213,7 +213,6 @@ limitations under the License. Bluetooth - Ширина и дължина Предпочитана мярка Имперски (мил, фт) Метрични (км, м) diff --git a/src/main/res/values-ca/strings.xml b/src/main/res/values-ca/strings.xml index a9516544d..f2ea4a723 100644 --- a/src/main/res/values-ca/strings.xml +++ b/src/main/res/values-ca/strings.xml @@ -213,7 +213,6 @@ limitations under the License. Bluetooth - Latitud/Longitud Mesura preferida Sistema anglosaxó (milles, peus) Sistema mètric (km, m) diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index e8579cb0c..562e070b7 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -280,7 +280,6 @@ Pokud zařízení GPS hlásí nepřesná data (např. Polohu, rychlost, nadmořs Připojeno k senzoru %1$s Odpojeno od %1$s - Zeměpisná šířka/délka Preferovaná rychlost Imperiální jednotky (míle, stopy) Metrické jednotky (km, m) diff --git a/src/main/res/values-da/strings.xml b/src/main/res/values-da/strings.xml index 8969b11f6..aa92a7ff4 100644 --- a/src/main/res/values-da/strings.xml +++ b/src/main/res/values-da/strings.xml @@ -282,7 +282,6 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj Tilsluttet %1$s Frakoblet %1$s - Breddegrad/længdegrad Foretrukkenfartenhed Britisk (mil, ft) Metrisk (km, m) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 2307c02c7..dfc635a50 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -284,7 +284,6 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka Verbunden mit %1$s Verbindung getrennt von %1$s - Breiten-/Längengrad Bevorzugtes Maß Englisch (mi, ft) Metrisch (km, m) diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index c71284891..32233fa61 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -213,7 +213,6 @@ limitations under the License. Bluetooth - Πλάτος/Μήκος Προτιμώμενος ρυθμός Αγγλοσαξονικό (μίλια, πόδια) Μετρικό (χλμ, μέτρα) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index f80e9aee3..2e25414cb 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -279,7 +279,6 @@ Si el dispositivo GPS reporta datos inexactos (por ejemplo, ubicación, velocida Conectado a %1$s Desconectado de %1$s - Latitud/Longitud Ritmo preferido Imperial (millas, pies) Métrica (km, m) diff --git a/src/main/res/values-et/strings.xml b/src/main/res/values-et/strings.xml index 54c17f714..95e3e6668 100644 --- a/src/main/res/values-et/strings.xml +++ b/src/main/res/values-et/strings.xml @@ -279,7 +279,6 @@ Kui GPS-seade teatab ebatäpsetest andmetest (nt asukoht, kiirus, kõrgus), ei s Ühendatud anduriga %1$s Lahti ühendatud %1$s - Laiuskraad/pikkuskraad Eelistatud määr Inglise mõõtühikud (miili, jalga) Meetermõõdustik (km, m) diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml index 373e280ee..fbe4133b5 100644 --- a/src/main/res/values-eu/strings.xml +++ b/src/main/res/values-eu/strings.xml @@ -280,7 +280,6 @@ GPS gailuak datu okerrak salatzen baditu (adibidez, kokapena, abiadura, kota), O Honekin konektatuta: %1$s Deskonektatuta %1$s - Latitudea/Longitudea Erritmo hobetsia Inperiala (mi, ft) Metrikoa (km, m) diff --git a/src/main/res/values-fa/strings.xml b/src/main/res/values-fa/strings.xml index 656fc8468..65e44d9b7 100644 --- a/src/main/res/values-fa/strings.xml +++ b/src/main/res/values-fa/strings.xml @@ -213,7 +213,6 @@ limitations under the License. بلوتوث - عرض جغرافیایی/ طول جغرافیایی اولویت سنجه سرعت انگلیسی (مایل، فوت) متریک (کیلومتر، متر) diff --git a/src/main/res/values-fi/strings.xml b/src/main/res/values-fi/strings.xml index 8feb87ff9..945b4aa65 100644 --- a/src/main/res/values-fi/strings.xml +++ b/src/main/res/values-fi/strings.xml @@ -284,7 +284,6 @@ limitations under the License. Yhdistetty: %1$s Yhteys katkaistu: %1$s - Leveys-/pituusaste Haluttu nopeus Brittiläinen järj. (maili, jalka) diff --git a/src/main/res/values-fr-rCA/strings.xml b/src/main/res/values-fr-rCA/strings.xml index 8bc685648..ed4b42c09 100644 --- a/src/main/res/values-fr-rCA/strings.xml +++ b/src/main/res/values-fr-rCA/strings.xml @@ -176,7 +176,6 @@ Réinitialiser maintenant? Les valeurs par défaut de tous les paramètres ont bien été rétablies Bluetooth - Latitude/longitude Mode de calcul souhaité Unités impériales (mi, pi) Unités métriques (km, m) diff --git a/src/main/res/values-fr-rCH/strings.xml b/src/main/res/values-fr-rCH/strings.xml index a7caadb5c..7ed2579c8 100644 --- a/src/main/res/values-fr-rCH/strings.xml +++ b/src/main/res/values-fr-rCH/strings.xml @@ -176,7 +176,6 @@ Réinitialiser maintenant ? Les valeurs par défaut de tous les paramètres ont bien été rétablies. Bluetooth - Latitude/Longitude Mode de calcul souhaité Unités impériales (mi, pi) Unités métriques (km, m) diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index b91672558..7f07c40f3 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -279,7 +279,6 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio Connecté à %1$s Déconnecté de %1$s - Latitude/Longitude Mode de calcul souhaité Unités impériales (mi, pi) Unités métriques (km, m) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index d04c01184..346fbc23e 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -294,7 +294,6 @@ Se te detés ou estás en interiores, non se gardarán datos do sensor (mais ser Desconectado de %1$s Circunferencia da roda (mm) - Latitude/lonxitude Indicador preferido Imperiais (millas, pés) Métricas (km, m) diff --git a/src/main/res/values-hi/strings.xml b/src/main/res/values-hi/strings.xml index c825ca891..b86d0506e 100644 --- a/src/main/res/values-hi/strings.xml +++ b/src/main/res/values-hi/strings.xml @@ -213,7 +213,6 @@ limitations under the License. Bluetooth - अक्षांश/देशांतर पसंदीदा दर इंपीरियल (मील, फुट) मेट्रिक (किमी, मी) diff --git a/src/main/res/values-hr/strings.xml b/src/main/res/values-hr/strings.xml index 32d63e7fe..77e7275f1 100644 --- a/src/main/res/values-hr/strings.xml +++ b/src/main/res/values-hr/strings.xml @@ -214,7 +214,6 @@ limitations under the License. Bluetooth - Geografska širina/duljina Preferirana stopa Imperijalne (milje, stope) Metričke (km, m) diff --git a/src/main/res/values-hu/strings.xml b/src/main/res/values-hu/strings.xml index 74c963ca2..2a3986b98 100644 --- a/src/main/res/values-hu/strings.xml +++ b/src/main/res/values-hu/strings.xml @@ -213,7 +213,6 @@ limitations under the License. Bluetooth - Szélesség/hosszúság Mérték választása Angolszász (mérföld, láb) Metrikus (km, m) diff --git a/src/main/res/values-in/strings.xml b/src/main/res/values-in/strings.xml index 9a5dba672..46bb75bdf 100644 --- a/src/main/res/values-in/strings.xml +++ b/src/main/res/values-in/strings.xml @@ -175,7 +175,6 @@ Setel ulang sekarang? Semua setelan telah dikembalikan ke nilai default Bluetooth - Lintang/Bujur Laju yang dipilih Imperial (mil, kaki) Metrik (km, m) diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index cb00be5d9..32fcd14ab 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -255,7 +255,6 @@ Infatti, un\'applicazione deve supportare ACTION_VIEW con MIME applica Bluetooth - Latitudine/longitudine Velocità preferita Imperiale (mi, ft) Metrico (km, m) diff --git a/src/main/res/values-iw/strings.xml b/src/main/res/values-iw/strings.xml index db71144bf..0c0a3cd8a 100644 --- a/src/main/res/values-iw/strings.xml +++ b/src/main/res/values-iw/strings.xml @@ -178,7 +178,6 @@ לאפס כעת? כל ההגדרות הוחזרו לערכי ברירת המחדל Bluetooth - קו רוחב/קו אורך קצב מועדף אימפריאלי (מייל, רגל) מטרי (ק\"מ, מ\') diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index e0add8019..522a9a1b0 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -212,7 +212,6 @@ limitations under the License. Bluetooth - 緯度/経度 計測方法 ヤード・ポンド法(マイル、フィート) メートル法(km、m) diff --git a/src/main/res/values-ko/strings.xml b/src/main/res/values-ko/strings.xml index cffc2f2bf..da6003761 100644 --- a/src/main/res/values-ko/strings.xml +++ b/src/main/res/values-ko/strings.xml @@ -212,7 +212,6 @@ limitations under the License. 블루투스 - 위도/경도 선호하는 속도 야드파운드법(마일, 피트) 미터법(km,m) diff --git a/src/main/res/values-lt/strings.xml b/src/main/res/values-lt/strings.xml index f72c9c7bd..e4a0964d1 100644 --- a/src/main/res/values-lt/strings.xml +++ b/src/main/res/values-lt/strings.xml @@ -214,7 +214,6 @@ limitations under the License. Bluetooth - Platuma ir ilguma Pageidaujamas greitis Imperinės (myl., pėdos) Metrinės (km, m) diff --git a/src/main/res/values-lv/strings.xml b/src/main/res/values-lv/strings.xml index 5a2344c31..846eff2ab 100644 --- a/src/main/res/values-lv/strings.xml +++ b/src/main/res/values-lv/strings.xml @@ -214,7 +214,6 @@ limitations under the License. Bluetooth - Platums/garums Vēlamais rādītājs Standarta (jūdzes, pēdas) Metriskās (km, m) diff --git a/src/main/res/values-mo/strings.xml b/src/main/res/values-mo/strings.xml index 6f18d1d70..ddfccb1d9 100644 --- a/src/main/res/values-mo/strings.xml +++ b/src/main/res/values-mo/strings.xml @@ -177,7 +177,6 @@ Resetați acum? Toate setările au revenit la valorile prestabilite Bluetooth - Latitudine/longitudine Rata de preferat Imperial (mi, ft) Metric (km, m) diff --git a/src/main/res/values-ms/strings.xml b/src/main/res/values-ms/strings.xml index d4f6ddcd9..51fcc4056 100644 --- a/src/main/res/values-ms/strings.xml +++ b/src/main/res/values-ms/strings.xml @@ -212,7 +212,6 @@ limitations under the License. Bluetooth - Latitud/Longitud Kadar pilihan Imperial (bt, kaki) Metrik (km, m) diff --git a/src/main/res/values-nb/strings.xml b/src/main/res/values-nb/strings.xml index 090e16866..814cf259e 100644 --- a/src/main/res/values-nb/strings.xml +++ b/src/main/res/values-nb/strings.xml @@ -213,7 +213,6 @@ limitations under the License. Bluetooth - Bredde- og lengdegrad Foretrukket måling Britisk (mi, ft) Metrisk (km, m) diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index f549e71e2..65f317d82 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -279,7 +279,6 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid Verbonden met %1$s Ontkoppeld van %1$s - Breedte-/lengtegraad Voorkeursverhouding Engels (mi, ft) Metrisch (km, m) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 8e0393d51..f877c541c 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -242,7 +242,6 @@ limitations under the License. Rozłaczony z %1$s Obwód koła (mm) - Długości i szerokości geograficznej Wybór tempo/prędkość Imperialne (mile, stopy) Metryczne (km, m) diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index fb3bd91eb..494bce151 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -176,7 +176,6 @@ Redefinir agora? Todas as configurações foram revertidas para os valores padrão Bluetooth - Latitude/longitude Taxa preferencial Imperial (milhas, pés) Métrica (km, m) diff --git a/src/main/res/values-pt-rPT/strings.xml b/src/main/res/values-pt-rPT/strings.xml index db478464d..12f6d2f6c 100644 --- a/src/main/res/values-pt-rPT/strings.xml +++ b/src/main/res/values-pt-rPT/strings.xml @@ -176,7 +176,6 @@ Repor agora? Todas as definições foram revertidas para os valores predefinidos Bluetooth - Latitude/Longitude Avaliação preferida Imperial (milhas, pés) Métrico (km, m) diff --git a/src/main/res/values-pt/strings.xml b/src/main/res/values-pt/strings.xml index e52cb3e1a..3bfd5c9a3 100644 --- a/src/main/res/values-pt/strings.xml +++ b/src/main/res/values-pt/strings.xml @@ -213,7 +213,6 @@ limitations under the License. Bluetooth - Latitude/longitude Taxa preferencial Imperial (milhas, pés) Métrica (km, m) diff --git a/src/main/res/values-ro/strings.xml b/src/main/res/values-ro/strings.xml index 1958df2d8..d09e06841 100644 --- a/src/main/res/values-ro/strings.xml +++ b/src/main/res/values-ro/strings.xml @@ -214,7 +214,6 @@ limitations under the License. Bluetooth - Latitudine/longitudine Rata de preferat Imperial (mi, ft) Metrică (km, m) diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 385dad7c9..f5b179440 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -215,7 +215,6 @@ limitations under the License. Bluetooth - Широта/долгота Предпочтение Британская (мили, футы) Метрическая (км, м) diff --git a/src/main/res/values-sk/strings.xml b/src/main/res/values-sk/strings.xml index bb2063136..6d18c9c77 100644 --- a/src/main/res/values-sk/strings.xml +++ b/src/main/res/values-sk/strings.xml @@ -214,7 +214,6 @@ limitations under the License. Bluetooth - Zemepis. šírka/dĺžka Preferovaná hodnota Imperiálne jednotky (mi, ft) Metrické jednotky (km, m) diff --git a/src/main/res/values-sl/strings.xml b/src/main/res/values-sl/strings.xml index 600f4ffb5..601183177 100644 --- a/src/main/res/values-sl/strings.xml +++ b/src/main/res/values-sl/strings.xml @@ -215,7 +215,6 @@ limitations under the License. Bluetooth - Zemlj. širina/dolžina Pred. enot. za hitr. Imperialni sistem (mi, ft) Metrični sistem (km, m) diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index 18d3217ce..e3d1d1822 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -214,7 +214,6 @@ limitations under the License. Bluetooth - Географска шир./дуж. Жељени темпо Империјалне (ми, ст) Метричке (км, м) diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index af0869d8d..cd4a57dbc 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -213,7 +213,6 @@ limitations under the License. Bluetooth - Latitud/longitud Föredragen mätning Imperial (mi, ft) Metrisk (km, m) diff --git a/src/main/res/values-th/strings.xml b/src/main/res/values-th/strings.xml index d2da63a4a..b19f17776 100644 --- a/src/main/res/values-th/strings.xml +++ b/src/main/res/values-th/strings.xml @@ -212,7 +212,6 @@ limitations under the License. บลูทูธ - ละติจูด/ลองจิจูด อัตราที่ต้องการ หน่วยอังกฤษ (ไมล์, ฟุต) หน่วยเมตริก (กม., เมตร) diff --git a/src/main/res/values-tl/strings.xml b/src/main/res/values-tl/strings.xml index 4b9aea107..556aaf66b 100644 --- a/src/main/res/values-tl/strings.xml +++ b/src/main/res/values-tl/strings.xml @@ -213,7 +213,6 @@ limitations under the License. Bluetooth - Latitude/Longtitude Gustong rate Imperial (mi, ft) Metric (km, m) diff --git a/src/main/res/values-tr/strings.xml b/src/main/res/values-tr/strings.xml index aaaf198de..8c0cc480e 100644 --- a/src/main/res/values-tr/strings.xml +++ b/src/main/res/values-tr/strings.xml @@ -213,7 +213,6 @@ limitations under the License. Bluetooth - Enlem/Boylam Tercih edilen oran İngiliz (mil, fit) Metrik (km, m) diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 7d6f28322..7d7df4e53 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -215,7 +215,6 @@ limitations under the License. Bluetooth - Широта/довгота Система вимірювання Британська (милі, фути) Метрична (км, м) diff --git a/src/main/res/values-vi/strings.xml b/src/main/res/values-vi/strings.xml index 6906c5407..6dce5aa21 100644 --- a/src/main/res/values-vi/strings.xml +++ b/src/main/res/values-vi/strings.xml @@ -212,7 +212,6 @@ limitations under the License. Bluetooth - Vĩ độ/Kinh độ Tốc độ ưa thích Hệ đo lường Anh (mi, ft) Hệ mét (km, m) diff --git a/src/main/res/values-zh-rHK/strings.xml b/src/main/res/values-zh-rHK/strings.xml index 773df2d55..395437629 100644 --- a/src/main/res/values-zh-rHK/strings.xml +++ b/src/main/res/values-zh-rHK/strings.xml @@ -175,7 +175,6 @@ 確定要立即重設? 所有設定已還原為預設值 藍牙 - 緯度/經度 偏好速率 英制 (英里、英尺) 公制 (公里、公尺) diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index ab8539761..6a8af800c 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -175,7 +175,6 @@ 確定要立即重設? 所有設定已還原為預設值 藍牙 - 緯度/經度 偏好速率 英制 (英里、英尺) 公制 (公里、公尺) diff --git a/src/main/res/values-zh/strings.xml b/src/main/res/values-zh/strings.xml index 707ee9b15..e287c5f4a 100644 --- a/src/main/res/values-zh/strings.xml +++ b/src/main/res/values-zh/strings.xml @@ -227,7 +227,6 @@ limitations under the License. 蓝牙 车轮周长 (mm) - 纬度/经度 首选速率 英制(英里、英尺) 公制(公里、米) diff --git a/src/main/res/values/settings.xml b/src/main/res/values/settings.xml index 4e130b4b3..a4e87c5d7 100644 --- a/src/main/res/values/settings.xml +++ b/src/main/res/values/settings.xml @@ -1,12 +1,6 @@ - statsShowCoordinate - true - - statsShowGradeElevation - true - trackdetail_show_on_lockscreen_while_recording false @@ -17,6 +11,7 @@ false settingsReset + settingsLayoutReset bluetoothSensor bluetoothCyclingCadenceSensor @@ -278,4 +273,130 @@ @string/settings_night_mode_option_day @string/settings_night_mode_option_night - \ No newline at end of file + + + statsCustomLayoutKey + + + statsCustomLayoutColumnsKey + 2 + + + statsCustomLayoutFieldsKey + + distance + total_time + speed + moving_time + average_speed + max_speed + average_moving_speed + altitude + gain + loss + pace + average_moving_pace + average_pace + fastest_pace + coordinates + heart_rate + average_heart_rate + cadence + average_cadence + power + + _ + + @string/stats_custom_layout_distance_key + 1,1,0 + + + @string/stats_custom_layout_total_time_key + 1,1,0 + + + @string/stats_custom_layout_speed_key + 1,1,0 + + + @string/stats_custom_layout_moving_time_key + 1,1,0 + + + @string/stats_custom_layout_average_speed_key + 1,0,0 + + + @string/stats_custom_layout_max_speed_key + 1,0,0 + + + @string/stats_custom_layout_average_moving_speed_key + 1,0,0 + + + @string/stats_custom_layout_altitude_key + 1,0,0 + + + @string/stats_custom_layout_gain_key + 1,0,0 + + + @string/stats_custom_layout_loss_key + 1,0,0 + + + @string/stats_custom_layout_pace_key + 0,0,0 + + + @string/stats_custom_layout_average_moving_pace_key + 0,0,0 + + + @string/stats_custom_layout_average_pace_key + 0,0,0 + + + @string/stats_custom_layout_fastest_pace_key + 0,0,0 + + + @string/stats_custom_layout_coordinates_key + 0,0,1 + + + @string/stats_custom_layout_heart_rate_key + 0,0,0 + + + @string/stats_custom_layout_cadence_key + 0,0,0 + + + @string/stats_custom_layout_power_key + 0,0,0 + + + + 0 + 0.0 + 00:00 + 0.0, 0.0 + + + + 1 + 2 + 3 + + + lastVersionKey + + diff --git a/src/main/res/values/settings_deprecated.xml b/src/main/res/values/settings_deprecated.xml index 8455e60e0..e0eaa2327 100644 --- a/src/main/res/values/settings_deprecated.xml +++ b/src/main/res/values/settings_deprecated.xml @@ -44,6 +44,12 @@ true + statsShowCoordinate + true + + statsShowGradeElevation + true + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 98459e458..ad0ea7333 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -196,6 +196,8 @@ limitations under the License. Time Total distance: %1$.2f km (%2$.1f mi) Total time: %1$s + Sensor: %1$s + GPS with photos without photos @@ -236,6 +238,7 @@ limitations under the License. Completed Click back again to cancel + Choose an option Starting GPS Waiting for a better GPS signal @@ -332,6 +335,8 @@ limitations under the License. By distance By time + Customize your recording layout + Data fields per row Show statistics on lockscreen While recording, show statistics without unlocking the device. Keep screen on @@ -356,8 +361,15 @@ limitations under the License. Day Night - + Reset + + Reset Custom Layout + All layout settings will be reverted to the default values. This will not delete any tracks on the device. + Reset your layouts? + All layout settings have been reverted to the default values + + Reset All Settings All settings will be reverted to the default values. This will not delete any tracks on the device. Reset now? All settings have been reverted to the default values @@ -373,7 +385,6 @@ limitations under the License. Wheel circumference (mm) - Latitude/Longitude Preferred rate Imperial (mi, ft) Metric (km, m) @@ -392,6 +403,7 @@ limitations under the License. Fastest pace Gain Loss + Latitude/Longitude Latitude Longitude Max speed @@ -401,6 +413,9 @@ limitations under the License. Distance Total time Split interval + Heart Rate + Cadence + Power Track %1$d Location accuracy: %1$s @@ -436,6 +451,9 @@ limitations under the License. mph min/km min/mi + + %1$s° + %1$s, %2$s Adapt for accuracy Adapt for battery life @@ -553,4 +571,9 @@ limitations under the License. Instant post-workout export Track export directory + Customize Recording Layout + + Primary + Secondary + Remove diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml index ae6f89292..76414cff5 100644 --- a/src/main/res/values/styles.xml +++ b/src/main/res/values/styles.xml @@ -100,12 +100,16 @@ limitations under the License. - + - + + - + + + + + + diff --git a/src/main/res/xml/settings.xml b/src/main/res/xml/settings.xml index 99d1a208a..6560e18e1 100644 --- a/src/main/res/xml/settings.xml +++ b/src/main/res/xml/settings.xml @@ -43,14 +43,9 @@ limitations under the License. - - + + + android:title="@string/settings_all_reset" />