From 7f9057c51988de0697afa2631c3d2dfd6770b094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rom=C3=A1n=20Mart=C3=ADnez?= Date: Sat, 23 Oct 2021 10:04:02 +0200 Subject: [PATCH] Added profiles for custom layouts. Users can add severals custom layouts and select one of them in recording fragment. The last selected layout is the one that will be use in the next recording. Fixes #938. --- .../opentracks/content/data/LayoutTest.java | 154 +++++++++++++++ .../settings/PreferencesOpenHelperTest.java | 118 ++++++++++++ .../opentracks/util/CsvLayoutUtilsTest.java | 93 ++++++++++ .../opentracks/util/PreferencesUtilsTest.java | 175 ++++++++++++++++-- src/main/AndroidManifest.xml | 3 +- .../opentracks/TrackRecordingActivity.java | 15 +- ...a => SettingsCustomLayoutEditAdapter.java} | 8 +- .../SettingsCustomLayoutListAdapter.java | 95 ++++++++++ .../opentracks/content/data/DataField.java | 43 ++++- .../opentracks/content/data/Layout.java | 115 +++++++++++- .../StatisticsRecordingFragment.java | 13 +- .../settings/PreferencesOpenHelper.java | 61 ++++-- .../opentracks/settings/PreferencesUtils.java | 108 +++++++---- ... => SettingsCustomLayoutEditActivity.java} | 36 ++-- .../SettingsCustomLayoutListActivity.java | 163 ++++++++++++++++ .../UserInterfaceSettingsFragment.java | 2 +- .../opentracks/util/CsvConstants.java | 10 - .../opentracks/util/CsvLayoutUtils.java | 77 ++++++++ .../util/RecyclerViewSwipeDeleteCallback.java | 88 +++++++++ .../opentracks/util/StatisticsUtils.java | 2 +- src/main/res/drawable/ic_baseline_add_24.xml | 10 + .../activity_settings_custom_layout_list.xml | 66 +++++++ src/main/res/layout/custom_layout_item.xml | 29 +++ src/main/res/menu/custom_layout_edit.xml | 13 ++ src/main/res/menu/track_record.xml | 12 +- src/main/res/values/colors.xml | 3 + src/main/res/values/settings.xml | 5 +- src/main/res/values/strings.xml | 10 + 28 files changed, 1408 insertions(+), 119 deletions(-) create mode 100644 src/androidTest/java/de/dennisguse/opentracks/content/data/LayoutTest.java create mode 100644 src/androidTest/java/de/dennisguse/opentracks/settings/PreferencesOpenHelperTest.java create mode 100644 src/androidTest/java/de/dennisguse/opentracks/util/CsvLayoutUtilsTest.java rename src/main/java/de/dennisguse/opentracks/adapters/{SettingsCustomLayoutAdapter.java => SettingsCustomLayoutEditAdapter.java} (90%) create mode 100644 src/main/java/de/dennisguse/opentracks/adapters/SettingsCustomLayoutListAdapter.java rename src/main/java/de/dennisguse/opentracks/settings/{SettingsCustomLayoutActivity.java => SettingsCustomLayoutEditActivity.java} (82%) create mode 100644 src/main/java/de/dennisguse/opentracks/settings/SettingsCustomLayoutListActivity.java delete mode 100644 src/main/java/de/dennisguse/opentracks/util/CsvConstants.java create mode 100644 src/main/java/de/dennisguse/opentracks/util/CsvLayoutUtils.java create mode 100644 src/main/java/de/dennisguse/opentracks/util/RecyclerViewSwipeDeleteCallback.java create mode 100644 src/main/res/drawable/ic_baseline_add_24.xml create mode 100644 src/main/res/layout/activity_settings_custom_layout_list.xml create mode 100644 src/main/res/layout/custom_layout_item.xml create mode 100644 src/main/res/menu/custom_layout_edit.xml 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 @@ + + + + + + + + + + + + + + + +