diff --git a/src/androidTest/java/de/dennisguse/opentracks/content/data/LayoutTest.java b/src/androidTest/java/de/dennisguse/opentracks/content/data/LayoutTest.java new file mode 100644 index 000000000..7b5521a35 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/content/data/LayoutTest.java @@ -0,0 +1,154 @@ +package de.dennisguse.opentracks.content.data; + +import android.content.Context; +import android.content.res.Resources; + +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 java.util.List; + +import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.settings.PreferencesUtils; + +@RunWith(AndroidJUnit4.class) +public class LayoutTest extends TestCase { + + private final Context context = ApplicationProvider.getApplicationContext(); + private final Resources resources = ApplicationProvider.getApplicationContext().getResources(); + + @Test + public void testFromCsv() { + // given a csv line + String csv = "running;2;" + context.getString(R.string.stats_custom_layout_speed_key) + ",1,1,0;" + context.getString(R.string.stats_custom_layout_distance_key) + ",1,0,0;" + context.getString(R.string.stats_custom_layout_altitude_key) + ",0,1,0;" + context.getString(R.string.stats_custom_layout_gain_key) + ",0,0,0;"; + + // when create a layout from CSV line + Layout layout = Layout.fromCsv(csv, resources); + List dataFieldList = layout.getFields(); + + // then layout and data fields are built correctly + assertEquals(layout.getName(), "running"); + assertEquals(layout.getColumnsPerRow(), 2); + assertEquals(dataFieldList.size(), 4); + assertEquals(dataFieldList.get(0).getKey(), context.getString(R.string.stats_custom_layout_speed_key)); + assertEquals(dataFieldList.get(1).getKey(), context.getString(R.string.stats_custom_layout_distance_key)); + assertEquals(dataFieldList.get(2).getKey(), context.getString(R.string.stats_custom_layout_altitude_key)); + assertEquals(dataFieldList.get(3).getKey(), context.getString(R.string.stats_custom_layout_gain_key)); + assertEquals(dataFieldList.get(0).getTitle(), context.getString(R.string.stats_speed)); + assertEquals(dataFieldList.get(1).getTitle(), context.getString(R.string.stats_distance)); + assertEquals(dataFieldList.get(2).getTitle(), context.getString(R.string.stats_altitude)); + assertEquals(dataFieldList.get(3).getTitle(), context.getString(R.string.stats_gain)); + assertTrue(dataFieldList.get(0).isVisible()); + assertTrue(dataFieldList.get(0).isPrimary()); + assertFalse(dataFieldList.get(0).isWide()); + assertTrue(dataFieldList.get(1).isVisible()); + assertFalse(dataFieldList.get(1).isPrimary()); + assertFalse(dataFieldList.get(1).isWide()); + assertFalse(dataFieldList.get(2).isVisible()); + assertTrue(dataFieldList.get(2).isPrimary()); + assertFalse(dataFieldList.get(2).isWide()); + assertFalse(dataFieldList.get(3).isVisible()); + assertFalse(dataFieldList.get(3).isPrimary()); + assertFalse(dataFieldList.get(3).isWide()); + + } + + @Test + public void testFromCsv_Wrong1() { + // given a csv line without number of columns + String csv1 = "Layout Name;speed,1,1;distance,0,0;"; + + // when create a layout from CSV line + Layout layout = Layout.fromCsv(csv1, resources); + List dataFieldList = layout.getFields(); + + // then layout and data fields are built correctly + assertEquals(layout.getName(), PreferencesUtils.getDefaultLayoutName()); + assertEquals(layout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault()); + assertEquals(layout.getFields().size(), 0); + } + + @Test + public void testFromCsv_Wrong2() { + // given a csv line without any field + String csv1 = "Layout Name;2;"; + + // when create a layout from CSV line + Layout layout = Layout.fromCsv(csv1, resources); + List dataFieldList = layout.getFields(); + + // then layout and data fields are built correctly + assertEquals(layout.getName(), PreferencesUtils.getDefaultLayoutName()); + assertEquals(layout.getColumnsPerRow(), PreferencesUtils.getLayoutColumnsByDefault()); + assertEquals(layout.getFields().size(), 0); + } + + @Test + public void testFromCsv_Wrong3() { + // given a csv line with wrong fields description + String csv1 = "Layout Name;2;speed,distance,total time;"; + + // when create a layout from CSV line + Layout layout = Layout.fromCsv(csv1, resources); + List dataFieldList = layout.getFields(); + + // then layout and data fields are built correctly + assertEquals(layout.getName(), "Layout Name"); + assertEquals(layout.getColumnsPerRow(), 2); + assertEquals(layout.getFields().size(), 0); + } + + @Test + public void testFromCsv_Wrong4() { + // given a csv line with the first field ok but not the others + String csv1 = "Layout Name;2;speed,1,0;distance;"; + + // when create a layout from CSV line + Layout layout = Layout.fromCsv(csv1, resources); + List dataFieldList = layout.getFields(); + + // then layout and data fields are built correctly + assertEquals(layout.getName(), "Layout Name"); + assertEquals(layout.getColumnsPerRow(), 2); + assertEquals(layout.getFields().size(), 1); + } + + @Test + public void testToCsv() { + // given a layout's object + Layout layout = new Layout("Test Layout", 2); + layout.addField("key1", "Title 1", false, false, false); + layout.addField("key2", "Title 2", false, true, false); + layout.addField("key3", "Title 3", true, false, false); + layout.addField("key4", "Title 4", true, true, false); + layout.addField("key5", "Title 5", true, true, true); + + // when converts it to CSV + String csv = layout.toCsv(); + + // then csv is well built + assertEquals(csv, "Test Layout;2;key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;"); + } + + @Test + public void testToCsv_columnsByDefault() { + // given a layout's object + Layout layout = new Layout("Test Layout"); + layout.addField("key1", "Title 1", false, false, false); + layout.addField("key2", "Title 2", false, true, false); + layout.addField("key3", "Title 3", true, false, false); + layout.addField("key4", "Title 4", true, true, false); + layout.addField("key5", "Title 5", true, true, true); + + // when converts it to CSV + String csv = layout.toCsv(); + + // then csv is well built + assertEquals(csv, "Test Layout;" + PreferencesUtils.getLayoutColumnsByDefault() + ";key1,0,0,0;key2,0,1,0;key3,1,0,0;key4,1,1,0;key5,1,1,1;"); + } +} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/settings/PreferencesOpenHelperTest.java b/src/androidTest/java/de/dennisguse/opentracks/settings/PreferencesOpenHelperTest.java new file mode 100644 index 000000000..e19372093 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/settings/PreferencesOpenHelperTest.java @@ -0,0 +1,118 @@ +package de.dennisguse.opentracks.settings; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; + +import androidx.preference.PreferenceManager; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.content.data.Layout; + +@RunWith(AndroidJUnit4.class) +public class PreferencesOpenHelperTest { + + private final Context context = ApplicationProvider.getApplicationContext(); + private final Resources resources = ApplicationProvider.getApplicationContext().getResources(); + + @Test + public void test_upgradeFrom0To1_withoutStatsCustomLayouts() { + // given the version 0 + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(context.getString(R.string.stats_custom_layouts_key)); + editor.putInt(context.getString(R.string.prefs_last_version_key), 0); + editor.commit(); + + // when update to version 1 + PreferencesOpenHelper.newInstance(1).check(); + + // then there should be one layout with old custom layout that has the new CSV value. + List layouts = PreferencesUtils.getAllCustomLayouts(); + assertNotNull(layouts); + assertEquals(layouts.size(), 1); + assertEquals(layouts.get(0).toCsv(), PreferencesUtils.getCustomLayout().toCsv()); + } + + @Test + public void test_upgradeFrom1To2_withOldVersion() { + // given the version 1 + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = sharedPreferences.edit(); + + String oldCustomLayoutCsv = context.getString(R.string.activity_type_unknown) + ";distance,1,1,0;speed,1,1,0;"; + editor.putString(context.getString(R.string.stats_custom_layouts_key), oldCustomLayoutCsv); + editor.putInt(context.getString(R.string.prefs_last_version_key), 1); + + editor.commit(); + + // when update to version 2 + PreferencesOpenHelper.newInstance(2).check(); + + String updatedOldCustomLayoutCsv = context.getString(R.string.activity_type_unknown) + ";" + + PreferencesUtils.getLayoutColumnsByDefault() + ";distance,1,1,0;speed,1,1,0;"; + + // then there should be one layout with old custom layout that has the new CSV value. + List layouts = PreferencesUtils.getAllCustomLayouts(); + assertNotNull(layouts); + assertEquals(layouts.size(), 1); + assertEquals(layouts.get(0).toCsv(), updatedOldCustomLayoutCsv); + } + + @Test + public void test_upgradeFrom1To2_withNewVersion() { + // given the version 1 + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = sharedPreferences.edit(); + + String oldCustomLayoutCsv = "whatever;3;distance,1,1,0;speed,1,1,0;"; + editor.putString(context.getString(R.string.stats_custom_layouts_key), oldCustomLayoutCsv); + editor.putInt(context.getString(R.string.prefs_last_version_key), 1); + + editor.commit(); + + // when update to version 2 + PreferencesOpenHelper.newInstance(2).check(); + + String updatedOldCustomLayoutCsv = "whatever;3;distance,1,1,0;speed,1,1,0;"; + + // then there should be one layout with old custom layout that has the new CSV value. + List layouts = PreferencesUtils.getAllCustomLayouts(); + assertNotNull(layouts); + assertEquals(layouts.size(), 1); + assertEquals(layouts.get(0).toCsv(), updatedOldCustomLayoutCsv); + } + + @Test + public void test_downgrade() { + // given version 2 + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = sharedPreferences.edit(); + + String customLayoutCsv = "whatever;3;distance,1,1,0;speed,1,1,0;"; + editor.putString(context.getString(R.string.stats_custom_layouts_key), customLayoutCsv); + editor.putInt(context.getString(R.string.prefs_last_version_key), 2); + + editor.commit(); + + // when downgrade to version 1 + PreferencesOpenHelper.newInstance(1).check(); + + // then custom layout should be equals to default layout. + Layout defaultLayout = Layout.fromCsv(PreferencesUtils.buildDefaultLayout(), resources); + List customLayout = PreferencesUtils.getAllCustomLayouts(); + + assertEquals(customLayout.size(), 1); + assertEquals(defaultLayout.toCsv(), customLayout.get(0).toCsv()); + } +} diff --git a/src/androidTest/java/de/dennisguse/opentracks/util/CsvLayoutUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/util/CsvLayoutUtilsTest.java new file mode 100644 index 000000000..e09c9c134 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/util/CsvLayoutUtilsTest.java @@ -0,0 +1,93 @@ +package de.dennisguse.opentracks.util; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import junit.framework.TestCase; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class CsvLayoutUtilsTest extends TestCase { + + @Test + public void testGetCsvLineParts() { + // given + String csvLineOk = "Layout Name;2;distance,1,1;speed,1,1;"; + String csvLineWrong1 = "Layout Name;distance,1,1;speed,1,1;"; + String csvLineWrong2 = "Layout Name;1;"; + + // when + List partsOk = CsvLayoutUtils.getCsvLineParts(csvLineOk); + List partsWrong1 = CsvLayoutUtils.getCsvLineParts(csvLineWrong1); + List partsWrong2 = CsvLayoutUtils.getCsvLineParts(csvLineWrong2); + + // then + assertNotNull(partsOk); + assertEquals(partsOk.size(), 4); + assertEquals(partsOk.get(0), "Layout Name"); + assertEquals(partsOk.get(1), "2"); + assertEquals(partsOk.get(2), "distance,1,1"); + assertEquals(partsOk.get(3), "speed,1,1"); + assertNull(partsWrong1); + assertNull(partsWrong2); + } + + @Test + public void testGetCsvFieldParts() { + // given + String csvFieldOk1 = "distance,0,0"; + String csvFieldOk2 = "speed,0,1"; + String csvFieldOk3 = "time,1,0"; + String csvFieldOk4 = "moving time,1,1"; + String csvFieldOk5 = ";moving time,1,1"; + String csvLineWrong1 = ""; + String csvLineWrong2 = "distance,speed,time"; + String csvLineWrong3 = "distance,0,1o"; + String csvLineWrong4 = "distance,0z,1"; + String csvLineWrong5 = "distance"; + String csvLineWrong6 = "distance,0"; + String csvLineWrong7 = "distance,1,1;"; + + // when + String[] ok1 = CsvLayoutUtils.getCsvFieldParts(csvFieldOk1); + String[] ok2 = CsvLayoutUtils.getCsvFieldParts(csvFieldOk2); + String[] ok3 = CsvLayoutUtils.getCsvFieldParts(csvFieldOk3); + String[] ok4 = CsvLayoutUtils.getCsvFieldParts(csvFieldOk4); + String[] ok5 = CsvLayoutUtils.getCsvFieldParts(csvFieldOk5); + String[] wrong1 = CsvLayoutUtils.getCsvFieldParts(csvLineWrong1); + String[] wrong2 = CsvLayoutUtils.getCsvFieldParts(csvLineWrong2); + String[] wrong3 = CsvLayoutUtils.getCsvFieldParts(csvLineWrong3); + String[] wrong4 = CsvLayoutUtils.getCsvFieldParts(csvLineWrong4); + String[] wrong5 = CsvLayoutUtils.getCsvFieldParts(csvLineWrong5); + String[] wrong6 = CsvLayoutUtils.getCsvFieldParts(csvLineWrong6); + String[] wrong7 = CsvLayoutUtils.getCsvFieldParts(csvLineWrong7); + + // then + assertNotNull(ok1); + assertNotNull(ok2); + assertNotNull(ok3); + assertNotNull(ok4); + assertNotNull(ok5); + assertFieldOk(ok1, "distance", "0", "0"); + assertFieldOk(ok2, "speed", "0", "1"); + assertFieldOk(ok3, "time", "1", "0"); + assertFieldOk(ok4, "moving time", "1", "1"); + assertFieldOk(ok5, ";moving time", "1", "1"); + assertNull(wrong1); + assertNull(wrong2); + assertNull(wrong3); + assertNull(wrong4); + assertNull(wrong5); + assertNull(wrong6); + assertNull(wrong7); + } + + private void assertFieldOk(String[] ok, String name, String val1, String val2) { + assertEquals(ok[0], name); + assertEquals(ok[1], val1); + assertEquals(ok[2], val2); + } +} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/util/PreferencesUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/util/PreferencesUtilsTest.java index f88658773..cdfebe233 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/util/PreferencesUtilsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/util/PreferencesUtilsTest.java @@ -2,6 +2,7 @@ package de.dennisguse.opentracks.util; import android.content.Context; import android.content.SharedPreferences; +import android.content.res.Resources; import androidx.preference.PreferenceManager; import androidx.test.core.app.ApplicationProvider; @@ -21,10 +22,14 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import java.util.ArrayList; +import java.util.List; + @RunWith(AndroidJUnit4.class) public class PreferencesUtilsTest { private final Context context = ApplicationProvider.getApplicationContext(); + private final Resources resources = ApplicationProvider.getApplicationContext().getResources(); @Test public void ExportTrackFileFormat_ok() { @@ -72,6 +77,24 @@ public class PreferencesUtilsTest { assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat); } + @Test + public void testGetAllCustomLayouts_default() { + // given + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.clear(); + editor.commit(); + + // when + List layouts = PreferencesUtils.getAllCustomLayouts(); + + // then + assertEquals(layouts.size(), 1); + assertTrue(layouts.get(0).getFields().size() > 0); + assertEquals(layouts.get(0).getName(), context.getString(R.string.stats_custom_layout_default_layout)); + assertTrue(layouts.get(0).getFields().stream().anyMatch(DataField::isVisible)); + } + @Test public void testGetCustomLayout_default() { // given @@ -85,7 +108,7 @@ public class PreferencesUtilsTest { // then assertTrue(layout.getFields().size() > 0); - assertEquals(layout.getProfile(), context.getString(R.string.default_activity_default)); + assertEquals(layout.getName(), context.getString(R.string.stats_custom_layout_default_layout)); assertTrue(layout.getFields().stream().anyMatch(DataField::isVisible)); } @@ -95,8 +118,8 @@ public class PreferencesUtilsTest { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString( - context.getString(R.string.stats_custom_layout_fields_key), - "run;" + context.getString(R.string.stats_custom_layouts_key), + "run;2;" + 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;" @@ -108,7 +131,8 @@ public class PreferencesUtilsTest { // then assertEquals(layout.getFields().size(), 4); - assertEquals(layout.getProfile(), "run"); + assertEquals(layout.getName(), "run"); + assertEquals(layout.getColumnsPerRow(), 2); assertEquals(layout.getFields().get(0).getKey(), context.getString(R.string.stats_custom_layout_moving_time_key)); assertTrue(layout.getFields().get(0).isVisible()); @@ -133,8 +157,8 @@ public class PreferencesUtilsTest { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString( - context.getString(R.string.stats_custom_layout_fields_key), - "walking;" + context.getString(R.string.stats_custom_layouts_key), + "walking;2;" + 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;" @@ -146,7 +170,8 @@ public class PreferencesUtilsTest { // then assertEquals(layout.getFields().size(), 4); - assertEquals(layout.getProfile(), "walking"); + assertEquals(layout.getName(), "walking"); + assertEquals(layout.getColumnsPerRow(), 2); assertEquals(layout.getFields().get(0).getKey(), context.getString(R.string.stats_custom_layout_moving_time_key)); assertTrue(layout.getFields().get(0).isVisible()); @@ -177,20 +202,22 @@ public class PreferencesUtilsTest { 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(layoutSrc); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(context.getString(R.string.stats_custom_layouts_key), layoutSrc.toCsv()); + editor.commit(); // then - String csv = sharedPreferences.getString(context.getString(R.string.stats_custom_layout_fields_key), null); + String csv = sharedPreferences.getString(context.getString(R.string.stats_custom_layouts_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;"); + "road cycling;" + PreferencesUtils.getLayoutColumnsByDefault() + ";" + + context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1,0;" + + context.getString(R.string.stats_custom_layout_distance_key) + ",1,0,0;" + + context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,1,0;" + + context.getString(R.string.stats_custom_layout_speed_key) + ",0,0,0;"); Layout layoutDst = PreferencesUtils.getCustomLayout(); - assertEquals(layoutSrc.getProfile(), layoutDst.getProfile()); + assertEquals(layoutSrc.getName(), layoutDst.getName()); 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()); @@ -198,4 +225,122 @@ public class PreferencesUtilsTest { assertEquals(layoutSrc.getFields().get(i).isPrimary(), layoutDst.getFields().get(i).isPrimary()); } } + + @Test + public void testEditCustomLayouts() { + // update all custom layouts + + // given a custom layout with two profiles + String cyclingProfile = "cycling;2;" + + context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_distance_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_speed_key) + ",1,1;"; + + String runningProfile = "running;2;" + + context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_distance_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_average_pace_key) + ",0,0;" + + context.getString(R.string.stats_custom_layout_pace_key) + ",0,0;"; + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(context.getString(R.string.stats_custom_layouts_key), cyclingProfile + "\n" + runningProfile); + editor.apply(); + + List layoutsBefore = PreferencesUtils.getAllCustomLayouts(); + + // when cyling profile is updated + String cyclingProfileUpdated = "cycling;2;" + + context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_distance_key) + ",0,0;" + + context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,0;" + + context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;"; + + List layoutsToBeUpdated = new ArrayList<>(); + layoutsToBeUpdated.add(Layout.fromCsv(cyclingProfileUpdated, resources)); + layoutsToBeUpdated.add(Layout.fromCsv(runningProfile, resources)); + + PreferencesUtils.updateCustomLayouts(layoutsToBeUpdated); + + // then only updated profile is modified in the custom layouts + List layoutsAfter = PreferencesUtils.getAllCustomLayouts(); + + assertEquals(layoutsBefore.size(), 2); + assertEquals(layoutsAfter.size(), 2); + + assertEquals(layoutsBefore.get(0).getFields().stream().filter(DataField::isVisible).count(), 4); + assertEquals(layoutsAfter.get(0).getFields().stream().filter(DataField::isVisible).count(), 1); + } + + @Test + public void testEditCustomLayout() { + // Update only one custom layout + + // given a custom layout with two profiles + String cyclingProfile = "cycling;2;" + + context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_distance_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_speed_key) + ",1,1;"; + + String runningProfile = "running;2;" + + context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_distance_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_average_pace_key) + ",0,0;" + + context.getString(R.string.stats_custom_layout_pace_key) + ",0,0;"; + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(context.getString(R.string.stats_custom_layouts_key), cyclingProfile + "\n" + runningProfile); + editor.apply(); + + List layoutsBefore = PreferencesUtils.getAllCustomLayouts(); + + // when cyling profile is updated + String cyclingProfileUpdated = "cycling;2;" + + context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_distance_key) + ",0,0;" + + context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",0,0;" + + context.getString(R.string.stats_custom_layout_speed_key) + ",0,0;"; + Layout layoutToBeUpdated = Layout.fromCsv(cyclingProfileUpdated, resources); + PreferencesUtils.updateCustomLayout(layoutToBeUpdated); + + // then only updated profile is modified in the custom layouts + List layoutsAfter = PreferencesUtils.getAllCustomLayouts(); + + assertEquals(layoutsBefore.size(), 2); + assertEquals(layoutsAfter.size(), 2); + + assertEquals(layoutsBefore.get(0).getFields().stream().filter(DataField::isVisible).count(), 4); + assertEquals(layoutsAfter.get(0).getFields().stream().filter(DataField::isVisible).count(), 1); + } + + @Test + public void testGetCustomLayout_whenSelectedOneNotExists() { + // given a custom layout with two profiles and not existing custom layout selected + String cyclingProfile = "cycling;2;" + + context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_distance_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_average_moving_speed_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_speed_key) + ",1,1;"; + + String runningProfile = "running;2;" + + context.getString(R.string.stats_custom_layout_moving_time_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_distance_key) + ",1,1;" + + context.getString(R.string.stats_custom_layout_average_pace_key) + ",0,0;" + + context.getString(R.string.stats_custom_layout_pace_key) + ",0,0;"; + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(context.getString(R.string.stats_custom_layouts_key), cyclingProfile + "\n" + runningProfile); + editor.putString(context.getString(R.string.stats_custom_layout_selected_layout_key), "Not Exists"); + editor.apply(); + + // when it gets the custom layout + Layout layout = PreferencesUtils.getCustomLayout(); + + // then the first one was returned + assertEquals(layout.getName(), "cycling"); + } } diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index d5aa126a1..c4bf753d6 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -324,7 +324,8 @@ limitations under the License. - + + layoutNames = PreferencesUtils.getAllCustomLayoutNames(); + builder.setTitle(getString(R.string.custom_layout_select_layout)).setItems(layoutNames.toArray(new String[0]), (dialog, which) -> PreferencesUtils.setDefaultLayout(layoutNames.get(which))); + builder.create().show(); return true; } diff --git a/src/main/java/de/dennisguse/opentracks/adapters/SettingsCustomLayoutAdapter.java b/src/main/java/de/dennisguse/opentracks/adapters/SettingsCustomLayoutEditAdapter.java similarity index 90% rename from src/main/java/de/dennisguse/opentracks/adapters/SettingsCustomLayoutAdapter.java rename to src/main/java/de/dennisguse/opentracks/adapters/SettingsCustomLayoutEditAdapter.java index 5bcdb2f7a..68a1a8527 100644 --- a/src/main/java/de/dennisguse/opentracks/adapters/SettingsCustomLayoutAdapter.java +++ b/src/main/java/de/dennisguse/opentracks/adapters/SettingsCustomLayoutEditAdapter.java @@ -18,13 +18,13 @@ 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 { +public class SettingsCustomLayoutEditAdapter extends RecyclerView.Adapter { private Layout layout; private final Context context; private final SettingsCustomLayoutItemClickListener itemClickListener; - public SettingsCustomLayoutAdapter(Context context, SettingsCustomLayoutItemClickListener itemClickListener, Layout layout) { + public SettingsCustomLayoutEditAdapter(Context context, SettingsCustomLayoutItemClickListener itemClickListener, Layout layout) { this.context = context; this.itemClickListener = itemClickListener; this.layout = layout; @@ -34,12 +34,12 @@ public class SettingsCustomLayoutAdapter extends RecyclerView.Adapter { + + private List layoutList; + private final Context context; + private SettingsCustomLayoutProfileClickListener itemClickListener; + + public SettingsCustomLayoutListAdapter(Context context, SettingsCustomLayoutProfileClickListener itemClickListener) { + this.context = context; + this.itemClickListener = itemClickListener; + layoutList = PreferencesUtils.getAllCustomLayouts(); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(context).inflate(R.layout.custom_layout_item, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + SettingsCustomLayoutListAdapter.ViewHolder viewHolder = (SettingsCustomLayoutListAdapter.ViewHolder) holder; + Layout layout = layoutList.get(position); + viewHolder.itemView.setTag(layout.getName()); + viewHolder.title.setText(layout.getName()); + } + + @Override + public int getItemCount() { + if (layoutList == null) { + return 0; + } + return layoutList.size(); + } + + public List getLayouts() { + return layoutList; + } + + public void reloadLayouts() { + layoutList = PreferencesUtils.getAllCustomLayouts(); + notifyDataSetChanged(); + } + + public void removeLayout(int position) { + layoutList.remove(position); + PreferencesUtils.updateCustomLayouts(layoutList); + notifyDataSetChanged(); + } + + public void restoreItem(Layout layout, int position) { + layoutList.add(position, layout); + PreferencesUtils.updateCustomLayouts(layoutList); + notifyDataSetChanged(); + } + + private class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + final TextView title; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + title = itemView.findViewById(R.id.custom_layout_title); + itemView.setOnClickListener(this); + } + + @Override + public void onClick(View view) { + String profile = (String) view.getTag(); + Optional optionalLayout = layoutList.stream().filter(layout -> layout.sameName(new Layout(profile))).findFirst(); + optionalLayout.ifPresent(itemClickListener::onSettingsCustomLayoutProfileClicked); + } + } + + public interface SettingsCustomLayoutProfileClickListener { + void onSettingsCustomLayoutProfileClicked(@NonNull Layout layout); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/content/data/DataField.java b/src/main/java/de/dennisguse/opentracks/content/data/DataField.java index 83b43cedf..0e62df087 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/DataField.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/DataField.java @@ -2,11 +2,13 @@ package de.dennisguse.opentracks.content.data; import android.content.Context; import android.content.res.Resources; +import android.os.Parcel; +import android.os.Parcelable; import de.dennisguse.opentracks.R; -import de.dennisguse.opentracks.util.CsvConstants; +import de.dennisguse.opentracks.util.CsvLayoutUtils; -public class DataField { +public class DataField implements Parcelable { public static final String YES_VALUE = "1"; public static final String NOT_VALUE = "0"; @@ -24,6 +26,26 @@ public class DataField { this.isWide = isWide; } + protected DataField(Parcel in) { + key = in.readString(); + title = in.readString(); + isVisible = in.readByte() != 0; + isPrimary = in.readByte() != 0; + isWide = in.readByte() != 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public DataField createFromParcel(Parcel in) { + return new DataField(in); + } + + @Override + public DataField[] newArray(int size) { + return new DataField[size]; + } + }; + public String getKey() { return key; } @@ -63,7 +85,8 @@ public class DataField { 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; + String wide = this.isWide ? YES_VALUE : NOT_VALUE; + return key + CsvLayoutUtils.PROPERTY_SEPARATOR + visible + CsvLayoutUtils.PROPERTY_SEPARATOR + primary + CsvLayoutUtils.PROPERTY_SEPARATOR + wide; } public static String getTitleByKey(Resources resources, String key) { @@ -109,4 +132,18 @@ public class DataField { throw new RuntimeException("It doesn't exists a field with key: " + key); } } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeString(key); + parcel.writeString(title); + parcel.writeByte((byte) (isVisible ? 1 : 0)); + parcel.writeByte((byte) (isPrimary ? 1 : 0)); + parcel.writeByte((byte) (isWide ? 1 : 0)); + } } diff --git a/src/main/java/de/dennisguse/opentracks/content/data/Layout.java b/src/main/java/de/dennisguse/opentracks/content/data/Layout.java index fdc7d7701..dcdd5ca5f 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/Layout.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/Layout.java @@ -1,16 +1,75 @@ package de.dennisguse.opentracks.content.data; +import android.content.res.Resources; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import androidx.annotation.NonNull; + import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; -public class Layout { - private final String profile; +import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.settings.PreferencesUtils; +import de.dennisguse.opentracks.util.CsvLayoutUtils; + +public class Layout implements Parcelable { + private static final String TAG = Layout.class.getSimpleName(); + + // User-generated layout's name. + private final String name; + private int columnsPerRow; private final List dataFields = new ArrayList<>(); - public Layout(String profile) { - this.profile = profile; + public static Layout fromCsv(@NonNull String csvLine, @NonNull Resources resources) { + List csvParts = CsvLayoutUtils.getCsvLineParts(csvLine); + if (csvParts == null) { + Log.e(TAG, "Invalid CSV layout. It shouldn't happen: " + csvLine); + return new Layout(PreferencesUtils.getDefaultLayoutName()); + } + + Layout layout = new Layout(csvParts.get(0), Integer.parseInt(csvParts.get(1))); + for (int i = 2; i < csvParts.size(); i++) { + String[] fieldParts = CsvLayoutUtils.getCsvFieldParts(csvParts.get(i)); + if (fieldParts == null) { + Log.e(TAG, "Invalid CSV layout. It shouldn't happen: " + csvLine); + return layout; + } + layout.addField(fieldParts[0], DataField.getTitleByKey(resources, fieldParts[0]), fieldParts[1].equals(DataField.YES_VALUE), fieldParts[2].equals(DataField.YES_VALUE), fieldParts[0].equals(resources.getString(R.string.stats_custom_layout_coordinates_key))); + } + return layout; } + public Layout(String name) { + this.name = name; + this.columnsPerRow = PreferencesUtils.getLayoutColumnsByDefault(); + } + + public Layout(String name, int columnsPerRow) { + this.name = name; + this.columnsPerRow = columnsPerRow; + } + + protected Layout(Parcel in) { + name = in.readString(); + columnsPerRow = in.readInt(); + in.readList(dataFields, DataField.class.getClassLoader()); + } + + public static final Creator CREATOR = new Creator() { + @Override + public Layout createFromParcel(Parcel in) { + return new Layout(in); + } + + @Override + public Layout[] newArray(int size) { + return new Layout[size]; + } + }; + public void addField(String key, String title, boolean visible, boolean primary, boolean isWide) { dataFields.add(new DataField(key, title, visible, primary, isWide)); } @@ -27,6 +86,11 @@ public class Layout { dataFields.remove(dataField); } + public void replaceAllFields(List newFields) { + dataFields.clear(); + addFields(newFields); + } + public List getFields() { return new ArrayList<>(dataFields); } @@ -36,7 +100,46 @@ public class Layout { dataFields.add(to, dataFieldToMove); } - public String getProfile() { - return profile; + public String getName() { + return name; + } + + public int getColumnsPerRow() { + return columnsPerRow; + } + + public void setColumnsPerRow(int columnsPerRow) { + this.columnsPerRow = columnsPerRow; + } + + public boolean sameName(Layout layout) { + return this.name.equalsIgnoreCase(layout.getName()); + } + + public boolean sameName(String name) { + return this.name.equalsIgnoreCase(name); + } + + public String toCsv() { + List fields = getFields(); + if (fields.isEmpty()) { + return ""; + } + + return getName() + CsvLayoutUtils.ITEM_SEPARATOR + getColumnsPerRow() + CsvLayoutUtils.ITEM_SEPARATOR + + fields.stream().map(DataField::toCsv).collect(Collectors.joining(CsvLayoutUtils.ITEM_SEPARATOR)) + + CsvLayoutUtils.ITEM_SEPARATOR; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeString(name); + parcel.writeInt(columnsPerRow); + parcel.writeList(dataFields); } } diff --git a/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordingFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordingFragment.java index 81e3b4421..b9f236350 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordingFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordingFragment.java @@ -65,13 +65,10 @@ public class StatisticsRecordingFragment extends Fragment { preferenceMetricUnits = PreferencesUtils.isMetricUnits(); } - if (PreferencesUtils.isKey(R.string.stats_custom_layout_fields_key, key)) { + if (PreferencesUtils.isKey(R.string.stats_custom_layouts_key, key) || PreferencesUtils.isKey(R.string.stats_custom_layout_selected_layout_key, key)) { updateUInecessary = true; layout = PreferencesUtils.getCustomLayout(); - } - - if (PreferencesUtils.isKey(R.string.stats_custom_layout_columns_key, key)) { - gridLayoutManager.setSpanCount(PreferencesUtils.getLayoutColumns()); + gridLayoutManager.setSpanCount(layout.getColumnsPerRow()); } if (key != null && updateUInecessary && isResumed()) { @@ -107,13 +104,13 @@ public class StatisticsRecordingFragment extends Fragment { RecyclerView recyclerView = viewBinding.statsRecyclerView; recyclerView.addItemDecoration(new DividerItemDecoration(getContext(), RecyclerView.VERTICAL)); - final int numColumns = PreferencesUtils.getLayoutColumns(); - gridLayoutManager = new GridLayoutManager(getContext(), numColumns); + layout = PreferencesUtils.getCustomLayout(); + gridLayoutManager = new GridLayoutManager(getContext(), layout.getColumnsPerRow()); gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { if (statisticsAdapter.isItemWide(position)) { - return numColumns; + return layout.getColumnsPerRow(); } return 1; } diff --git a/src/main/java/de/dennisguse/opentracks/settings/PreferencesOpenHelper.java b/src/main/java/de/dennisguse/opentracks/settings/PreferencesOpenHelper.java index 13958a53e..e95f1bd45 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/PreferencesOpenHelper.java +++ b/src/main/java/de/dennisguse/opentracks/settings/PreferencesOpenHelper.java @@ -1,34 +1,73 @@ package de.dennisguse.opentracks.settings; +import java.util.ArrayList; +import java.util.Collections; +import java.util.stream.Collectors; + import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.util.CsvLayoutUtils; class PreferencesOpenHelper { - private static final int PREFERENCES_VERSION = 1; + private final int version; - private PreferencesOpenHelper() { + private PreferencesOpenHelper(int version) { + this.version = version; } - static PreferencesOpenHelper newInstance() { - return new PreferencesOpenHelper(); + static PreferencesOpenHelper newInstance(int version) { + return new PreferencesOpenHelper(version); } - void checkForUpgrade() { + void check() { int lastVersion = PreferencesUtils.getInt(R.string.prefs_last_version_key, 0); - if (PREFERENCES_VERSION > lastVersion) { + if (version > lastVersion) { onUpgrade(); + } else if (version < lastVersion) { + onDowngrade(); } } private void onUpgrade() { - PreferencesUtils.setInt(R.string.prefs_last_version_key, PREFERENCES_VERSION); - switch (PREFERENCES_VERSION) { - case 1: - upgradeFrom0to1(); + PreferencesUtils.setInt(R.string.prefs_last_version_key, version); + for (int i = 1; i <= version; i++) { + switch (i) { + case 1: + upgradeFrom0to1(); + break; + case 2: + upgradeFrom1to2(); + break; + default: + throw new RuntimeException("Not implemented: upgrade to " + version); + } } } private void upgradeFrom0to1() { - PreferencesUtils.setString(R.string.stats_custom_layout_fields_key, PreferencesUtils.buildDefaultLayout()); + String preferenceValue = PreferencesUtils.getString(R.string.stats_custom_layouts_key, ""); + if (preferenceValue.isEmpty()) { + PreferencesUtils.setString(R.string.stats_custom_layouts_key, PreferencesUtils.buildDefaultLayout()); + } + } + + private void upgradeFrom1to2() { + String csvVersion1CustomLayout = PreferencesUtils.getString(R.string.stats_custom_layouts_key, PreferencesUtils.buildDefaultLayout()); + ArrayList parts = new ArrayList<>(); + Collections.addAll(parts, csvVersion1CustomLayout.split(CsvLayoutUtils.ITEM_SEPARATOR)); + + if (parts.size() < 2) { + PreferencesUtils.setString(R.string.stats_custom_layouts_key, PreferencesUtils.buildDefaultLayout()); + return; + } + + if (!parts.get(1).matches("\\d+")) { + parts.add(1, String.valueOf(PreferencesUtils.getLayoutColumnsByDefault())); + } + PreferencesUtils.setString(R.string.stats_custom_layouts_key, parts.stream().collect(Collectors.joining(CsvLayoutUtils.ITEM_SEPARATOR))); + } + + private void onDowngrade() { + PreferencesUtils.setString(R.string.stats_custom_layouts_key, PreferencesUtils.buildDefaultLayout()); } } diff --git a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java index 79acef13a..b30bf554c 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java +++ b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java @@ -25,6 +25,7 @@ import android.content.res.TypedArray; import android.net.Uri; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AppCompatDelegate; import androidx.documentfile.provider.DocumentFile; @@ -34,17 +35,16 @@ import java.time.Duration; import java.lang.reflect.Field; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; +import java.util.Optional; 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.content.data.Speed; import de.dennisguse.opentracks.io.file.TrackFileFormat; -import de.dennisguse.opentracks.util.CsvConstants; +import de.dennisguse.opentracks.util.CsvLayoutUtils; import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.UnitConversions; @@ -57,6 +57,8 @@ public class PreferencesUtils { private final static String TAG = PreferencesUtils.class.getSimpleName(); + private static final int PREFERENCES_VERSION = 2; + private PreferencesUtils() { } @@ -71,7 +73,7 @@ public class PreferencesUtils { PreferencesUtils.resources = resources; PreferencesUtils.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - PreferencesOpenHelper.newInstance().checkForUpgrade(); + PreferencesOpenHelper.newInstance(PREFERENCES_VERSION).check(); } public static void registerOnSharedPreferenceChangeListener(SharedPreferences.OnSharedPreferenceChangeListener changeListener) { @@ -546,12 +548,8 @@ public class PreferencesUtils { return getDefaultExportDirectoryUri(context) != null; } - public static int getLayoutColumns() { - return getInt(R.string.stats_custom_layout_columns_key, resources.getInteger(R.integer.stats_custom_layout_columns_default)); - } - - public static void setLayoutColumns(int columns) { - setInt(R.string.stats_custom_layout_columns_key, columns); + public static int getLayoutColumnsByDefault() { + return resources.getInteger(R.integer.stats_custom_layout_columns_default); } private static List getMultiTypedArray(String key) { @@ -576,46 +574,84 @@ public class PreferencesUtils { } @SuppressLint("ResourceType") - static String buildDefaultLayout() { + private static String buildDefaultFields() { List fieldsArrays = getMultiTypedArray("stats_custom_layout_fields_default_value"); - return resources.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; + return fieldsArrays.stream().map(i -> i.getString(0) + CsvLayoutUtils.PROPERTY_SEPARATOR + i.getString(1)).collect(Collectors.joining(CsvLayoutUtils.ITEM_SEPARATOR)) + + CsvLayoutUtils.ITEM_SEPARATOR; } + static String buildDefaultLayout() { + return resources.getString(R.string.stats_custom_layout_default_layout) + CsvLayoutUtils.ITEM_SEPARATOR + getLayoutColumnsByDefault() + CsvLayoutUtils.ITEM_SEPARATOR + buildDefaultFields(); + } + + public static String getDefaultLayoutName() { + return resources.getString(R.string.stats_custom_layout_default_layout); + } + + /** + * @return custom layout selected or the first one if any has been selected or the one selected is not exists anymore. + */ public static Layout getCustomLayout() { - String csvCustomLayout = getString(R.string.stats_custom_layout_fields_key, buildDefaultLayout()); - 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(resources, fieldParts[0]), fieldParts[1].equals(DataField.YES_VALUE), fieldParts[2].equals(DataField.YES_VALUE), fieldParts[0].equals(resources.getString(R.string.stats_custom_layout_coordinates_key))); + String csvCustomLayouts = getString(R.string.stats_custom_layouts_key, buildDefaultLayout()); + String[] csvLines = csvCustomLayouts.split(CsvLayoutUtils.LINE_SEPARATOR); + String layoutSelected = getString(R.string.stats_custom_layout_selected_layout_key, null); + if (layoutSelected == null) { + return Layout.fromCsv(csvLines[0], resources); } - return layout; + for (String line : csvLines) { + Layout layout = Layout.fromCsv(line, resources); + if (layout.sameName(layoutSelected)) { + return layout; + } + } + + return Layout.fromCsv(csvLines[0], resources); } - public static void setCustomLayout(Layout layout) { - List fields = layout.getFields(); - if (fields.isEmpty()) { - return; + public static void updateCustomLayouts(@NonNull List layouts) { + setString(R.string.stats_custom_layouts_key, layouts.stream().map(Layout::toCsv).collect(Collectors.joining(CsvLayoutUtils.LINE_SEPARATOR))); + } + + public static void updateCustomLayout(@NonNull Layout layout) { + List preferenceLayouts = PreferencesUtils.getAllCustomLayouts(); + Optional layoutToBeUpdated = preferenceLayouts.stream().filter(l -> l.sameName(layout)).findFirst(); + if (layoutToBeUpdated.isPresent()) { + layoutToBeUpdated.get().replaceAllFields(layout.getFields()); + layoutToBeUpdated.get().setColumnsPerRow(layout.getColumnsPerRow()); + PreferencesUtils.updateCustomLayouts(preferenceLayouts); + } + } + + public static void addCustomLayout(@NonNull String layoutName) { + String newLayoutCsv = layoutName + CsvLayoutUtils.ITEM_SEPARATOR + getLayoutColumnsByDefault() + CsvLayoutUtils.ITEM_SEPARATOR + buildDefaultFields(); + String customLayoutCsv = getString(R.string.stats_custom_layouts_key, buildDefaultLayout()) + CsvLayoutUtils.LINE_SEPARATOR + newLayoutCsv; + setString(R.string.stats_custom_layouts_key, customLayoutCsv); + } + + public static void setDefaultLayout(String layoutName) { + setString(R.string.stats_custom_layout_selected_layout_key, layoutName); + } + + public static List getAllCustomLayouts() { + List layouts = new ArrayList<>(); + String csvCustomLayout = getString(R.string.stats_custom_layouts_key, buildDefaultLayout()); + String[] csvLines = csvCustomLayout.split(CsvLayoutUtils.LINE_SEPARATOR); + for (String line : csvLines) { + layouts.add(Layout.fromCsv(line, resources)); } - String csv = layout.getProfile() + CsvConstants.LINE_SEPARATOR - + fields.stream().map(DataField::toCsv).collect(Collectors.joining(CsvConstants.LINE_SEPARATOR)) - + CsvConstants.LINE_SEPARATOR; - setString(R.string.stats_custom_layout_fields_key, csv); + return layouts; + } + + public static List getAllCustomLayoutNames() { + return getAllCustomLayouts().stream().map(Layout::getName).collect(Collectors.toList()); } public static void resetCustomLayoutPreferences() { - if (sharedPreferences.contains(resources.getString(R.string.stats_custom_layout_fields_key))) { + if (sharedPreferences.contains(resources.getString(R.string.stats_custom_layouts_key))) { SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.remove(resources.getString(R.string.stats_custom_layout_fields_key)); - editor.commit(); - } - if (sharedPreferences.contains(resources.getString(R.string.stats_custom_layout_columns_key))) { - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.remove(resources.getString(R.string.stats_custom_layout_columns_key)); + editor.remove(resources.getString(R.string.stats_custom_layouts_key)); editor.commit(); } } diff --git a/src/main/java/de/dennisguse/opentracks/settings/SettingsCustomLayoutActivity.java b/src/main/java/de/dennisguse/opentracks/settings/SettingsCustomLayoutEditActivity.java similarity index 82% rename from src/main/java/de/dennisguse/opentracks/settings/SettingsCustomLayoutActivity.java rename to src/main/java/de/dennisguse/opentracks/settings/SettingsCustomLayoutEditActivity.java index bb6cba8ab..829228049 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/SettingsCustomLayoutActivity.java +++ b/src/main/java/de/dennisguse/opentracks/settings/SettingsCustomLayoutEditActivity.java @@ -18,18 +18,20 @@ 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.adapters.SettingsCustomLayoutEditAdapter; 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.StatisticsUtils; -public class SettingsCustomLayoutActivity extends AbstractActivity implements SettingsCustomLayoutAdapter.SettingsCustomLayoutItemClickListener { +public class SettingsCustomLayoutEditActivity extends AbstractActivity implements SettingsCustomLayoutEditAdapter.SettingsCustomLayoutItemClickListener { + public static final String EXTRA_LAYOUT = "extraLayout"; private ActivitySettingsCustomLayoutBinding viewBinding; private GridLayoutManager gridLayoutManager; - private SettingsCustomLayoutAdapter adapterFieldsVisible; - private SettingsCustomLayoutAdapter adapterFieldsHidden; + private SettingsCustomLayoutEditAdapter adapterFieldsVisible; + private SettingsCustomLayoutEditAdapter adapterFieldsHidden; + private String profile; private Layout layoutFieldsVisible; private Layout layoutFieldsHidden; private int numColumns; @@ -38,12 +40,13 @@ public class SettingsCustomLayoutActivity extends AbstractActivity implements Se protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Recycler view with visible stats. - layoutFieldsVisible = StatisticsUtils.filterVisible(PreferencesUtils.getCustomLayout(), true); - adapterFieldsVisible = new SettingsCustomLayoutAdapter(this, this, layoutFieldsVisible); + Layout layout = getIntent().getParcelableExtra(EXTRA_LAYOUT); + profile = layout.getName(); + layoutFieldsVisible = StatisticsUtils.filterVisible(layout, true); + adapterFieldsVisible = new SettingsCustomLayoutEditAdapter(this, this, layoutFieldsVisible); - numColumns = PreferencesUtils.getLayoutColumns(); + numColumns = layout.getColumnsPerRow(); RecyclerView recyclerViewVisible = viewBinding.recyclerViewVisible; gridLayoutManager = new GridLayoutManager(this, numColumns); gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @@ -86,7 +89,6 @@ public class SettingsCustomLayoutActivity extends AbstractActivity implements Se public void onItemSelected(AdapterView parent, View view, int position, long id) { numColumns = position + 1; gridLayoutManager.setSpanCount(numColumns); - PreferencesUtils.setLayoutColumns(position + 1); } @Override @@ -94,24 +96,26 @@ public class SettingsCustomLayoutActivity extends AbstractActivity implements Se } }); - viewBinding.spinnerOptions.setSelection(PreferencesUtils.getLayoutColumns() - 1); + viewBinding.spinnerOptions.setSelection(numColumns - 1); // Recycler view with not visible stats. - layoutFieldsHidden = StatisticsUtils.filterVisible(PreferencesUtils.getCustomLayout(), false); - adapterFieldsHidden = new SettingsCustomLayoutAdapter(this, this, layoutFieldsHidden); + layoutFieldsHidden = StatisticsUtils.filterVisible(layout, false); + adapterFieldsHidden = new SettingsCustomLayoutEditAdapter(this, this, layoutFieldsHidden); RecyclerView recyclerViewNotVisible = viewBinding.recyclerViewNotVisible; recyclerViewNotVisible.setLayoutManager(new LinearLayoutManager(this)); recyclerViewNotVisible.setAdapter(adapterFieldsHidden); + + setTitle(profile); } @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(newLayout); + Layout layout = new Layout(profile, numColumns); + layout.addFields(layoutFieldsVisible.getFields()); + layout.addFields(layoutFieldsHidden.getFields()); + PreferencesUtils.updateCustomLayout(layout); } } diff --git a/src/main/java/de/dennisguse/opentracks/settings/SettingsCustomLayoutListActivity.java b/src/main/java/de/dennisguse/opentracks/settings/SettingsCustomLayoutListActivity.java new file mode 100644 index 000000000..55975054a --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/settings/SettingsCustomLayoutListActivity.java @@ -0,0 +1,163 @@ +package de.dennisguse.opentracks.settings; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.textfield.TextInputEditText; +import com.google.android.material.textfield.TextInputLayout; + +import de.dennisguse.opentracks.AbstractActivity; +import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.adapters.SettingsCustomLayoutListAdapter; +import de.dennisguse.opentracks.content.data.Layout; +import de.dennisguse.opentracks.databinding.ActivitySettingsCustomLayoutListBinding; +import de.dennisguse.opentracks.util.RecyclerViewSwipeDeleteCallback; + +public class SettingsCustomLayoutListActivity extends AbstractActivity implements SettingsCustomLayoutListAdapter.SettingsCustomLayoutProfileClickListener { + + private ActivitySettingsCustomLayoutListBinding viewBinding; + private SettingsCustomLayoutListAdapter adapter; + private RecyclerView recyclerView; + private View addProfileLayout; + private TextInputEditText addProfileEditText; + private TextInputLayout addProfileInputLayout; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setTitle(getString(R.string.custom_layout_list_title)); + + adapter = new SettingsCustomLayoutListAdapter(this, this); + recyclerView = viewBinding.recyclerView; + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.setAdapter(adapter); + + Button cancelButton = findViewById(R.id.custom_layout_list_cancel_button); + Button okButton = findViewById(R.id.custom_layout_list_ok_button); + + cancelButton.setOnClickListener(view -> clearAndHideEditLayout()); + + okButton.setEnabled(false); + okButton.setOnClickListener(view -> { + PreferencesUtils.addCustomLayout(addProfileEditText.getText().toString()); + clearAndHideEditLayout(); + adapter.reloadLayouts(); + }); + + addProfileLayout = findViewById(R.id.custom_layout_list_add_linear_layout); + addProfileInputLayout = findViewById(R.id.custom_layout_list_input_layout); + addProfileEditText = findViewById(R.id.custom_layout_list_edit_name); + addProfileEditText.addTextChangedListener(new TextWatcher() { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (s == null || s.toString().isEmpty()) { + okButton.setEnabled(false); + return; + } + + if (adapter.getLayouts().stream().anyMatch(layout -> layout.sameName(s.toString()))) { + okButton.setEnabled(false); + addProfileInputLayout.setError(getString(R.string.custom_layout_list_edit_already_exists)); + } else { + okButton.setEnabled(true); + addProfileInputLayout.setError(""); + } + } + + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + + RecyclerViewSwipeDeleteCallback recyclerViewSwipeDeleteCallback = new RecyclerViewSwipeDeleteCallback(this) { + @Override + public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + // When there's only one profile it cannot be deleted (so, "disable" movements: drag flags and swipe flags). + return adapter.getItemCount() > 1 ? makeMovementFlags(0, ItemTouchHelper.LEFT) : makeMovementFlags(0, 0); + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) { + final int position = viewHolder.getAdapterPosition(); + final Layout item = adapter.getLayouts().get(position); + + adapter.removeLayout(position); + + Snackbar snackbar = Snackbar.make(recyclerView, getString(R.string.custom_layout_list_layout_removed), Snackbar.LENGTH_LONG); + snackbar.setAction(getString(R.string.generic_undo).toUpperCase(), view -> { + adapter.restoreItem(item, position); + recyclerView.scrollToPosition(position); + }); + + snackbar.show(); + } + }; + + ItemTouchHelper itemTouchhelper = new ItemTouchHelper(recyclerViewSwipeDeleteCallback); + itemTouchhelper.attachToRecyclerView(recyclerView); + } + + @Override + protected void onResume() { + super.onResume(); + adapter.reloadLayouts(); + } + + @Override + protected View getRootView() { + PreferencesUtils.getCustomLayout(); + viewBinding = ActivitySettingsCustomLayoutListBinding.inflate(getLayoutInflater()); + return viewBinding.getRoot(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.custom_layout_edit, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.custom_layout_edit_add_profile) { + addProfileLayout.setVisibility(View.VISIBLE); + addProfileEditText.requestFocus(); + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(addProfileEditText, InputMethodManager.SHOW_IMPLICIT); + return true; + } + + return super.onOptionsItemSelected(item); + } + + @Override + public void onSettingsCustomLayoutProfileClicked(@NonNull Layout layout) { + Intent intent = new Intent(this, SettingsCustomLayoutEditActivity.class); + intent.putExtra(SettingsCustomLayoutEditActivity.EXTRA_LAYOUT, layout); + startActivity(intent); + } + + private void clearAndHideEditLayout() { + addProfileEditText.setText(""); + addProfileInputLayout.setError(""); + addProfileLayout.setVisibility(View.GONE); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/settings/UserInterfaceSettingsFragment.java b/src/main/java/de/dennisguse/opentracks/settings/UserInterfaceSettingsFragment.java index f4c9cf61f..d1727329d 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/UserInterfaceSettingsFragment.java +++ b/src/main/java/de/dennisguse/opentracks/settings/UserInterfaceSettingsFragment.java @@ -24,7 +24,7 @@ public class UserInterfaceSettingsFragment extends PreferenceFragmentCompat { Preference customLayoutPreference = findPreference(getString(R.string.stats_custom_layout_key)); customLayoutPreference.setOnPreferenceClickListener((preference) -> { - Intent intent = new Intent(getContext(), SettingsCustomLayoutActivity.class); + Intent intent = new Intent(getContext(), SettingsCustomLayoutListActivity.class); startActivity(intent); return true; }); diff --git a/src/main/java/de/dennisguse/opentracks/util/CsvConstants.java b/src/main/java/de/dennisguse/opentracks/util/CsvConstants.java deleted file mode 100644 index 2835b5aae..000000000 --- a/src/main/java/de/dennisguse/opentracks/util/CsvConstants.java +++ /dev/null @@ -1,10 +0,0 @@ -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/CsvLayoutUtils.java b/src/main/java/de/dennisguse/opentracks/util/CsvLayoutUtils.java new file mode 100644 index 000000000..559c492b0 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/util/CsvLayoutUtils.java @@ -0,0 +1,77 @@ +package de.dennisguse.opentracks.util; + +import androidx.annotation.Nullable; + +import com.google.android.material.navigation.NavigationBarView; + +import java.util.Arrays; +import java.util.List; + +public final class CsvLayoutUtils { + public static final String LINE_SEPARATOR = "\n"; + public static final String ITEM_SEPARATOR = ";"; + public static final String PROPERTY_SEPARATOR = ","; + + private CsvLayoutUtils() { + + } + + /** + * @param csvLine Layout description in a CSV format. + * @return All CSV parts from the csvLine or null if it's malformed. + */ + @Nullable + public static List getCsvLineParts(String csvLine) { + if (csvLine == null) { + return null; + } + + // The line must have three items, at least: layout's name, number of columns and one field. + List csvParts = Arrays.asList(csvLine.split(ITEM_SEPARATOR)); + if (csvParts.size() < 3 || !hasValue(csvParts.get(0)) || !isInt(csvParts.get(1))) { + return null; + } + + return csvParts; + } + + /** + * @param csvField Layout's field in a CSV format. + * @return List of field parts or null if it's malformed. + */ + @Nullable + public static String[] getCsvFieldParts(@Nullable String csvField) { + if (csvField == null) { + return null; + } + + // Field must have three items: key, is visible (0 or 1), is primary (0 or 1). + String[] fieldParts = csvField.split(CsvLayoutUtils.PROPERTY_SEPARATOR); + if (fieldParts.length < 3 || !hasValue(fieldParts[0]) || !hasZeroOneValue(fieldParts[1]) || !hasZeroOneValue(fieldParts[2])) { + return null; + } + + return fieldParts; + } + + private static boolean hasValue(String value) { + return value != null && !value.isEmpty(); + } + + private static boolean isInt(String value) { + if (value == null || value.isEmpty()) { + return false; + } + + try { + Integer.parseInt(value); + } catch (NumberFormatException nfe) { + return false; + } + return true; + } + + private static boolean hasZeroOneValue(String value) { + return isInt(value) && (Integer.parseInt(value) == 0 || Integer.parseInt(value) == 1); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/util/RecyclerViewSwipeDeleteCallback.java b/src/main/java/de/dennisguse/opentracks/util/RecyclerViewSwipeDeleteCallback.java new file mode 100644 index 000000000..62064aef2 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/util/RecyclerViewSwipeDeleteCallback.java @@ -0,0 +1,88 @@ +package de.dennisguse.opentracks.util; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + +import de.dennisguse.opentracks.R; + +abstract public class RecyclerViewSwipeDeleteCallback extends ItemTouchHelper.Callback { + + private final Paint clearPaint; + private final ColorDrawable background; + private final int backgroundColor; + private final Drawable deleteDrawable; + private final int intrinsicWidth; + private final int intrinsicHeight; + + public RecyclerViewSwipeDeleteCallback(Context context) { + background = new ColorDrawable(); + backgroundColor = context.getResources().getColor(R.color.colorBackgroundDelete); + clearPaint = new Paint(); + clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + deleteDrawable = ContextCompat.getDrawable(context, R.drawable.ic_delete_forever_24dp); + deleteDrawable.setTint(context.getResources().getColor(R.color.colorForegroundDelete)); + intrinsicWidth = deleteDrawable.getIntrinsicWidth(); + intrinsicHeight = deleteDrawable.getIntrinsicHeight(); + } + + @Override + public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + return makeMovementFlags(0, ItemTouchHelper.LEFT); + } + + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder viewHolder1) { + return false; + } + + @Override + public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + + View itemView = viewHolder.itemView; + int itemHeight = itemView.getHeight(); + + boolean isCancelled = dX == 0 && !isCurrentlyActive; + + if (isCancelled) { + clearCanvas(c, itemView.getRight() + dX, (float) itemView.getTop(), (float) itemView.getRight(), (float) itemView.getBottom()); + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + return; + } + + background.setColor(backgroundColor); + background.setBounds(itemView.getRight() + (int) dX, itemView.getTop(), itemView.getRight(), itemView.getBottom()); + background.draw(c); + + int deleteIconTop = itemView.getTop() + (itemHeight - intrinsicHeight) / 2; + int deleteIconMargin = (itemHeight - intrinsicHeight) / 2; + int deleteIconLeft = itemView.getRight() - deleteIconMargin - intrinsicWidth; + int deleteIconRight = itemView.getRight() - deleteIconMargin; + int deleteIconBottom = deleteIconTop + intrinsicHeight; + + deleteDrawable.setBounds(deleteIconLeft, deleteIconTop, deleteIconRight, deleteIconBottom); + deleteDrawable.draw(c); + + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + } + + private void clearCanvas(Canvas c, Float left, Float top, Float right, Float bottom) { + c.drawRect(left, top, right, bottom, clearPaint); + } + + @Override + public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) { + return 0.7f; + } +} diff --git a/src/main/java/de/dennisguse/opentracks/util/StatisticsUtils.java b/src/main/java/de/dennisguse/opentracks/util/StatisticsUtils.java index 72502a00d..a22dcb8f2 100644 --- a/src/main/java/de/dennisguse/opentracks/util/StatisticsUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/StatisticsUtils.java @@ -25,7 +25,7 @@ public class StatisticsUtils { } public static Layout filterVisible(Layout layout, boolean visible) { - Layout result = new Layout(layout.getProfile()); + Layout result = new Layout(layout.getName()); result.addFields(layout.getFields().stream().filter(f -> f.isVisible() == visible).collect(Collectors.toList())); return result; } diff --git a/src/main/res/drawable/ic_baseline_add_24.xml b/src/main/res/drawable/ic_baseline_add_24.xml new file mode 100644 index 000000000..eb232541d --- /dev/null +++ b/src/main/res/drawable/ic_baseline_add_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/main/res/layout/activity_settings_custom_layout_list.xml b/src/main/res/layout/activity_settings_custom_layout_list.xml new file mode 100644 index 000000000..f9c7771dc --- /dev/null +++ b/src/main/res/layout/activity_settings_custom_layout_list.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + +